From 73104e16df24334ce3ba97145d043ab51c23b39c Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 25 May 2026 09:50:23 +0200 Subject: [PATCH 001/142] chore: remove legacy Python implementation --- .github/workflows/integrity-check.yml | 32 - CONTRIBUTING.md | 46 -- MANIFEST.in | 1 - Pipfile | 24 - Pipfile.lock | 645 ------------------ TODO.md | 9 - brain_brew/__init__.py | 0 brain_brew/build_tasks/__init__.py | 0 brain_brew/build_tasks/crowd_anki/__init__.py | 0 .../crowd_anki/crowd_anki_generate.py | 81 --- .../crowd_anki/headers_from_crowdanki.py | 69 -- .../crowd_anki/headers_to_crowd_anki.py | 42 -- .../crowd_anki/media_group_from_crowd_anki.py | 39 -- .../crowd_anki/media_group_to_crowd_anki.py | 40 -- .../note_model_single_from_crowd_anki.py | 62 -- .../note_models_all_from_crowd_anki.py | 52 -- .../crowd_anki/note_models_to_crowd_anki.py | 91 --- .../crowd_anki/notes_from_crowd_anki.py | 81 --- .../crowd_anki/notes_to_crowd_anki.py | 96 --- .../crowd_anki/shared_base_notes.py | 20 - brain_brew/build_tasks/csvs/__init__.py | 0 brain_brew/build_tasks/csvs/csvs_generate.py | 101 --- .../csvs/generate_guids_in_csvs.py | 88 --- .../build_tasks/csvs/notes_from_csvs.py | 92 --- .../build_tasks/csvs/shared_base_csvs.py | 63 -- brain_brew/build_tasks/deck_parts/__init__.py | 0 .../build_tasks/deck_parts/from_yaml_part.py | 61 -- .../deck_parts/headers_from_yaml_part.py | 67 -- .../deck_parts/media_group_from_folder.py | 58 -- .../deck_parts/note_model_from_html_parts.py | 78 --- .../deck_parts/note_model_from_yaml_part.py | 45 -- .../deck_parts/save_media_group_to_folder.py | 55 -- .../deck_parts/save_note_models_to_folder.py | 57 -- brain_brew/build_tasks/overrides/__init__.py | 0 .../build_tasks/overrides/headers_override.py | 55 -- .../build_tasks/overrides/notes_override.py | 40 -- brain_brew/commands/__init__.py | 0 brain_brew/commands/argument_reader.py | 117 ---- brain_brew/commands/init_repo/__init__.py | 0 brain_brew/commands/init_repo/init_repo.py | 208 ------ brain_brew/commands/run_recipe/__init__.py | 0 brain_brew/commands/run_recipe/build_task.py | 45 -- .../commands/run_recipe/parts_builder.py | 64 -- .../commands/run_recipe/recipe_builder.py | 82 --- brain_brew/commands/run_recipe/run_recipe.py | 18 - .../commands/run_recipe/top_level_builder.py | 87 --- brain_brew/configuration/__init__.py | 0 brain_brew/configuration/anki_field.py | 16 - brain_brew/configuration/file_manager.py | 58 -- brain_brew/configuration/part_holder.py | 45 -- .../configuration/representation_base.py | 28 - brain_brew/configuration/yaml_verifier.py | 40 -- brain_brew/front_matter.py | 2 - brain_brew/interfaces/__init__.py | 0 brain_brew/interfaces/command.py | 7 - brain_brew/interfaces/media_container.py | 8 - brain_brew/interfaces/yamale_verifyable.py | 22 - brain_brew/main.py | 23 - brain_brew/representation/__init__.py | 0 brain_brew/representation/generic/__init__.py | 0 brain_brew/representation/generic/csv_file.py | 116 ---- .../representation/generic/html_file.py | 38 -- .../representation/generic/media_file.py | 31 - .../representation/generic/source_file.py | 41 -- brain_brew/representation/json/__init__.py | 0 .../representation/json/crowd_anki_export.py | 63 -- brain_brew/representation/json/json_file.py | 25 - .../json/wrappers_for_crowd_anki.py | 117 ---- brain_brew/representation/yaml/__init__.py | 0 brain_brew/representation/yaml/headers.py | 44 -- brain_brew/representation/yaml/media_group.py | 57 -- brain_brew/representation/yaml/note_model.py | 270 -------- .../representation/yaml/note_model_field.py | 86 --- .../yaml/note_model_template.py | 196 ------ brain_brew/representation/yaml/notes.py | 184 ----- brain_brew/representation/yaml/yaml_object.py | 55 -- brain_brew/schemas/__init__.py | 0 brain_brew/schemas/recipe.yaml | 173 ----- brain_brew/transformers/__init__.py | 0 .../create_media_group_from_location.py | 24 - brain_brew/transformers/file_mapping.py | 194 ------ brain_brew/transformers/note_model_mapping.py | 178 ----- .../save_media_group_to_location.py | 36 - .../save_note_model_to_location.py | 46 -- brain_brew/utils.py | 129 ---- scripts/__init__.py | 0 scripts/build.bash | 4 - scripts/dist.bash | 9 - scripts/yamale_build.py | 13 - setup.py | 36 - tests/__init__.py | 0 tests/build_tasks/__init__.py | 0 .../test_source_crowd_anki_json.py | 113 --- tests/build_tasks/test_source_csv.py | 140 ---- tests/representation/__init__.py | 0 .../representation/configuration/__init__.py | 0 .../configuration/test_csv_file_mapping.py | 160 ----- .../configuration/test_note_model_mapping.py | 146 ---- tests/representation/generic/__init__.py | 0 tests/representation/generic/test_csv_file.py | 117 ---- .../representation/generic/test_media_file.py | 37 - tests/representation/json/__init__.py | 0 .../json/test_crowd_anki_export.py | 49 -- tests/representation/yaml/__init__.py | 0 .../yaml/test_note_model_repr.py | 128 ---- tests/representation/yaml/test_note_repr.py | 392 ----------- tests/test_argument_reader.py | 51 -- tests/test_builder.py | 12 - tests/test_file_manager.py | 32 - tests/test_files.py | 72 -- tests/test_files/build_files/builder1.yaml | 42 -- .../crowd_anki/crowdanki_example_1/deck.json | 398 ----------- tests/test_files/csv/test1.csv | 16 - tests/test_files/csv/test1_split1.csv | 7 - tests/test_files/csv/test1_split2.csv | 10 - tests/test_files/csv/test2.csv | 10 - tests/test_files/csv/test2_missing_guids.csv | 10 - tests/test_files/csv/test3.csv | 2 - .../note_models/LL Word No Defaults.json | 143 ---- .../note_models/LL Word Only Required.json | 87 --- .../deck_parts/note_models/LL Word.json | 165 ----- .../deck_parts/note_models/Test-Model.json | 144 ---- .../yaml/note_models/LL-Word-No-Defaults.yaml | 144 ---- .../note_models/LL-Word-Only-Required.yaml | 79 --- .../deck_parts/yaml/notes/note1.yaml | 31 - .../media_files/buried/even_more/signals2.png | Bin 15118 -> 0 bytes tests/test_files/media_files/signals.png | Bin 15118 -> 0 bytes tests/test_files/tsv/test1.tsv | 16 - tests/test_helpers.py | 7 - tests/test_utils.py | 84 --- 130 files changed, 8169 deletions(-) delete mode 100644 .github/workflows/integrity-check.yml delete mode 100644 CONTRIBUTING.md delete mode 100644 MANIFEST.in delete mode 100644 Pipfile delete mode 100644 Pipfile.lock delete mode 100644 TODO.md delete mode 100644 brain_brew/__init__.py delete mode 100644 brain_brew/build_tasks/__init__.py delete mode 100644 brain_brew/build_tasks/crowd_anki/__init__.py delete mode 100644 brain_brew/build_tasks/crowd_anki/crowd_anki_generate.py delete mode 100644 brain_brew/build_tasks/crowd_anki/headers_from_crowdanki.py delete mode 100644 brain_brew/build_tasks/crowd_anki/headers_to_crowd_anki.py delete mode 100644 brain_brew/build_tasks/crowd_anki/media_group_from_crowd_anki.py delete mode 100644 brain_brew/build_tasks/crowd_anki/media_group_to_crowd_anki.py delete mode 100644 brain_brew/build_tasks/crowd_anki/note_model_single_from_crowd_anki.py delete mode 100644 brain_brew/build_tasks/crowd_anki/note_models_all_from_crowd_anki.py delete mode 100644 brain_brew/build_tasks/crowd_anki/note_models_to_crowd_anki.py delete mode 100644 brain_brew/build_tasks/crowd_anki/notes_from_crowd_anki.py delete mode 100644 brain_brew/build_tasks/crowd_anki/notes_to_crowd_anki.py delete mode 100644 brain_brew/build_tasks/crowd_anki/shared_base_notes.py delete mode 100644 brain_brew/build_tasks/csvs/__init__.py delete mode 100644 brain_brew/build_tasks/csvs/csvs_generate.py delete mode 100644 brain_brew/build_tasks/csvs/generate_guids_in_csvs.py delete mode 100644 brain_brew/build_tasks/csvs/notes_from_csvs.py delete mode 100644 brain_brew/build_tasks/csvs/shared_base_csvs.py delete mode 100644 brain_brew/build_tasks/deck_parts/__init__.py delete mode 100644 brain_brew/build_tasks/deck_parts/from_yaml_part.py delete mode 100644 brain_brew/build_tasks/deck_parts/headers_from_yaml_part.py delete mode 100644 brain_brew/build_tasks/deck_parts/media_group_from_folder.py delete mode 100644 brain_brew/build_tasks/deck_parts/note_model_from_html_parts.py delete mode 100644 brain_brew/build_tasks/deck_parts/note_model_from_yaml_part.py delete mode 100644 brain_brew/build_tasks/deck_parts/save_media_group_to_folder.py delete mode 100644 brain_brew/build_tasks/deck_parts/save_note_models_to_folder.py delete mode 100644 brain_brew/build_tasks/overrides/__init__.py delete mode 100644 brain_brew/build_tasks/overrides/headers_override.py delete mode 100644 brain_brew/build_tasks/overrides/notes_override.py delete mode 100644 brain_brew/commands/__init__.py delete mode 100644 brain_brew/commands/argument_reader.py delete mode 100644 brain_brew/commands/init_repo/__init__.py delete mode 100644 brain_brew/commands/init_repo/init_repo.py delete mode 100644 brain_brew/commands/run_recipe/__init__.py delete mode 100644 brain_brew/commands/run_recipe/build_task.py delete mode 100644 brain_brew/commands/run_recipe/parts_builder.py delete mode 100644 brain_brew/commands/run_recipe/recipe_builder.py delete mode 100644 brain_brew/commands/run_recipe/run_recipe.py delete mode 100644 brain_brew/commands/run_recipe/top_level_builder.py delete mode 100644 brain_brew/configuration/__init__.py delete mode 100644 brain_brew/configuration/anki_field.py delete mode 100644 brain_brew/configuration/file_manager.py delete mode 100644 brain_brew/configuration/part_holder.py delete mode 100644 brain_brew/configuration/representation_base.py delete mode 100644 brain_brew/configuration/yaml_verifier.py delete mode 100644 brain_brew/front_matter.py delete mode 100644 brain_brew/interfaces/__init__.py delete mode 100644 brain_brew/interfaces/command.py delete mode 100644 brain_brew/interfaces/media_container.py delete mode 100644 brain_brew/interfaces/yamale_verifyable.py delete mode 100644 brain_brew/main.py delete mode 100644 brain_brew/representation/__init__.py delete mode 100644 brain_brew/representation/generic/__init__.py delete mode 100644 brain_brew/representation/generic/csv_file.py delete mode 100644 brain_brew/representation/generic/html_file.py delete mode 100644 brain_brew/representation/generic/media_file.py delete mode 100644 brain_brew/representation/generic/source_file.py delete mode 100644 brain_brew/representation/json/__init__.py delete mode 100644 brain_brew/representation/json/crowd_anki_export.py delete mode 100644 brain_brew/representation/json/json_file.py delete mode 100644 brain_brew/representation/json/wrappers_for_crowd_anki.py delete mode 100644 brain_brew/representation/yaml/__init__.py delete mode 100644 brain_brew/representation/yaml/headers.py delete mode 100644 brain_brew/representation/yaml/media_group.py delete mode 100644 brain_brew/representation/yaml/note_model.py delete mode 100644 brain_brew/representation/yaml/note_model_field.py delete mode 100644 brain_brew/representation/yaml/note_model_template.py delete mode 100644 brain_brew/representation/yaml/notes.py delete mode 100644 brain_brew/representation/yaml/yaml_object.py delete mode 100644 brain_brew/schemas/__init__.py delete mode 100644 brain_brew/schemas/recipe.yaml delete mode 100644 brain_brew/transformers/__init__.py delete mode 100644 brain_brew/transformers/create_media_group_from_location.py delete mode 100644 brain_brew/transformers/file_mapping.py delete mode 100644 brain_brew/transformers/note_model_mapping.py delete mode 100644 brain_brew/transformers/save_media_group_to_location.py delete mode 100644 brain_brew/transformers/save_note_model_to_location.py delete mode 100644 brain_brew/utils.py delete mode 100644 scripts/__init__.py delete mode 100755 scripts/build.bash delete mode 100755 scripts/dist.bash delete mode 100755 scripts/yamale_build.py delete mode 100644 setup.py delete mode 100644 tests/__init__.py delete mode 100644 tests/build_tasks/__init__.py delete mode 100644 tests/build_tasks/test_source_crowd_anki_json.py delete mode 100644 tests/build_tasks/test_source_csv.py delete mode 100644 tests/representation/__init__.py delete mode 100644 tests/representation/configuration/__init__.py delete mode 100644 tests/representation/configuration/test_csv_file_mapping.py delete mode 100644 tests/representation/configuration/test_note_model_mapping.py delete mode 100644 tests/representation/generic/__init__.py delete mode 100644 tests/representation/generic/test_csv_file.py delete mode 100644 tests/representation/generic/test_media_file.py delete mode 100644 tests/representation/json/__init__.py delete mode 100644 tests/representation/json/test_crowd_anki_export.py delete mode 100644 tests/representation/yaml/__init__.py delete mode 100644 tests/representation/yaml/test_note_model_repr.py delete mode 100644 tests/representation/yaml/test_note_repr.py delete mode 100644 tests/test_argument_reader.py delete mode 100644 tests/test_builder.py delete mode 100644 tests/test_file_manager.py delete mode 100644 tests/test_files.py delete mode 100644 tests/test_files/build_files/builder1.yaml delete mode 100644 tests/test_files/crowd_anki/crowdanki_example_1/deck.json delete mode 100644 tests/test_files/csv/test1.csv delete mode 100644 tests/test_files/csv/test1_split1.csv delete mode 100644 tests/test_files/csv/test1_split2.csv delete mode 100644 tests/test_files/csv/test2.csv delete mode 100644 tests/test_files/csv/test2_missing_guids.csv delete mode 100644 tests/test_files/csv/test3.csv delete mode 100644 tests/test_files/deck_parts/note_models/LL Word No Defaults.json delete mode 100644 tests/test_files/deck_parts/note_models/LL Word Only Required.json delete mode 100644 tests/test_files/deck_parts/note_models/LL Word.json delete mode 100644 tests/test_files/deck_parts/note_models/Test-Model.json delete mode 100644 tests/test_files/deck_parts/yaml/note_models/LL-Word-No-Defaults.yaml delete mode 100644 tests/test_files/deck_parts/yaml/note_models/LL-Word-Only-Required.yaml delete mode 100644 tests/test_files/deck_parts/yaml/notes/note1.yaml delete mode 100644 tests/test_files/media_files/buried/even_more/signals2.png delete mode 100644 tests/test_files/media_files/signals.png delete mode 100644 tests/test_files/tsv/test1.tsv delete mode 100644 tests/test_helpers.py delete mode 100644 tests/test_utils.py diff --git a/.github/workflows/integrity-check.yml b/.github/workflows/integrity-check.yml deleted file mode 100644 index 5cbe257..0000000 --- a/.github/workflows/integrity-check.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Python application - -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v1 - - - name: Set up Python 3.7 - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - - name: Install dependencies - run: | - python3 -m pip install --upgrade pipenv - pipenv install --dev - - - name: Run tests - run: | - pipenv run unit_tests - - - name: Build Yamale Recipe - run: | - pipenv run build_yamale - - - name: Check Yamale Recipe for changes - run: git diff --quiet -- || (echo "::error file=yamale,line=0,col=0::You need to run 'python scripts/yamale_build.py'" && exit 1) - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 5700b5f..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ - -### Install Brain Brew package - -https://pypi.org/project/Brain-Brew/ - -```shell -pipenv install brain-brew -``` - -### Run Local Version - -Fork/Clone this repo onto your computer, then in a different repository you wish to run Brain Brew you can point it to this version in a 2 ways: - -#### Install development folder for live updates - -Point your installation to this folder. Run the following (change the path to match yours): - -```shell -pipenv install -e ../brain-brew -``` - -This should result in your Pipfile updating to: - -``` -[packages] -brain-brew = {file = "../brain-brew", editable = true} -``` - -#### Install a locally built package - -Build Brain Brew using the `scripts/build.bash` script. This will generate dist and build folders. Install the generated wheel by running: - -``` -pip install ../brain-brew/dist/Brain_Brew-0.3.11-py3-none-any.whl -``` - -This should result in your Pipfile updating to: - -``` -[packages] -brain-brew = {file = "../brain-brew/dist/Brain_Brew-0.3.11-py3-none-any.whl"} -``` - -Change to match the wheel version number, which is set in `brain_brew/front_matter.py` if you wish to change it. - - diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 4d17a07..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include brain_brew/schemas/recipe.yaml diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 3f71eac..0000000 --- a/Pipfile +++ /dev/null @@ -1,24 +0,0 @@ -[[source]] -name = "Brain Brew" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -pytest = "==5.4.1" -twine = "*" -coverage = "==4.5.4" -typing-extensions = "==3.10.0.0" - -[packages] -"ruamel.yaml" = "==0.16.10" -yamale = "==3.0.8" - -[requires] -python_version = "3.7" - -[scripts] -build_yamale = "python scripts/yamale_build.py" -check_for_changes = ''' - git diff --quiet -- || (echo "::error file=yamale,line=0,col=0::You need to run `python scripts/yamale_build.py`" && exit 1) -''' -unit_tests = "py.test" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 5415fc1..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,645 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "0d4532926edfd30fdf44d37fea09d682ba1b3bab705b425f29745ec3cee0f7ac" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "Brain Brew", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "pyyaml": { - "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" - ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" - }, - "ruamel.yaml": { - "hashes": [ - "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b", - "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954" - ], - "index": "Brain Brew", - "version": "==0.16.10" - }, - "ruamel.yaml.clib": { - "hashes": [ - "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d", - "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001", - "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462", - "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9", - "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe", - "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b", - "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b", - "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615", - "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62", - "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15", - "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b", - "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1", - "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9", - "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675", - "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899", - "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7", - "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7", - "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312", - "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa", - "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91", - "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b", - "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6", - "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3", - "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334", - "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5", - "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3", - "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe", - "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c", - "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed", - "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337", - "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880", - "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f", - "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d", - "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248", - "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d", - "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf", - "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512", - "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069", - "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb", - "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942", - "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d", - "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31", - "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92", - "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5", - "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28", - "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d", - "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1", - "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2", - "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875", - "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412" - ], - "markers": "python_version < '3.9' and platform_python_implementation == 'CPython'", - "version": "==0.2.8" - }, - "yamale": { - "hashes": [ - "sha256:1468f90f5019a82a77ff3f101bb001930765ef4b5e8d7fe658c1d2967e775f9a", - "sha256:9e9d6946d2f68926822d0df400dafb5e75b34bc7f482237393db29e697d5bbad" - ], - "index": "Brain Brew", - "markers": "python_version >= '3.6'", - "version": "==3.0.8" - } - }, - "develop": { - "attrs": { - "hashes": [ - "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", - "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" - ], - "markers": "python_version >= '3.7'", - "version": "==24.2.0" - }, - "bleach": { - "hashes": [ - "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", - "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.0" - }, - "certifi": { - "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.7.4" - }, - "cffi": { - "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" - ], - "markers": "platform_python_implementation != 'PyPy'", - "version": "==1.15.1" - }, - "charset-normalizer": { - "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "commonmark": { - "hashes": [ - "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", - "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" - ], - "version": "==0.9.1" - }, - "coverage": { - "hashes": [ - "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", - "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", - "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", - "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", - "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", - "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", - "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", - "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", - "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", - "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", - "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", - "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", - "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", - "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", - "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", - "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", - "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", - "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", - "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", - "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", - "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", - "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", - "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", - "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", - "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", - "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", - "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", - "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", - "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", - "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", - "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", - "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" - ], - "index": "Brain Brew", - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", - "version": "==4.5.4" - }, - "cryptography": { - "hashes": [ - "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709", - "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069", - "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2", - "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b", - "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e", - "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70", - "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778", - "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22", - "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895", - "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf", - "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431", - "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f", - "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947", - "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74", - "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc", - "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66", - "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66", - "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf", - "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f", - "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5", - "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e", - "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f", - "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55", - "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1", - "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47", - "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5", - "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0" - ], - "markers": "python_version >= '3.7'", - "version": "==43.0.0" - }, - "docutils": { - "hashes": [ - "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", - "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b" - ], - "markers": "python_version >= '3.7'", - "version": "==0.20.1" - }, - "idna": { - "hashes": [ - "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", - "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" - ], - "markers": "python_version >= '3.6'", - "version": "==3.8" - }, - "importlib-metadata": { - "hashes": [ - "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", - "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5" - ], - "markers": "python_version < '3.8'", - "version": "==6.7.0" - }, - "importlib-resources": { - "hashes": [ - "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6", - "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a" - ], - "markers": "python_version < '3.9'", - "version": "==5.12.0" - }, - "jaraco.classes": { - "hashes": [ - "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158", - "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a" - ], - "markers": "python_version >= '3.7'", - "version": "==3.2.3" - }, - "jeepney": { - "hashes": [ - "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", - "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.8.0" - }, - "keyring": { - "hashes": [ - "sha256:3d44a48fa9a254f6c72879d7c88604831ebdaac6ecb0b214308b02953502c510", - "sha256:bc402c5e501053098bcbd149c4ddbf8e36c6809e572c2d098d4961e88d4c270d" - ], - "markers": "python_version >= '3.7'", - "version": "==24.1.1" - }, - "more-itertools": { - "hashes": [ - "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d", - "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3" - ], - "markers": "python_version >= '3.7'", - "version": "==9.1.0" - }, - "packaging": { - "hashes": [ - "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", - "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" - ], - "markers": "python_version >= '3.7'", - "version": "==24.0" - }, - "pkginfo": { - "hashes": [ - "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", - "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097" - ], - "markers": "python_version >= '3.6'", - "version": "==1.10.0" - }, - "pluggy": { - "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "version": "==2.21" - }, - "pygments": { - "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" - ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" - }, - "pytest": { - "hashes": [ - "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", - "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" - ], - "index": "Brain Brew", - "markers": "python_version >= '3.5'", - "version": "==5.4.1" - }, - "readme-renderer": { - "hashes": [ - "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273", - "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343" - ], - "markers": "python_version >= '3.7'", - "version": "==37.3" - }, - "requests": { - "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" - ], - "markers": "python_version >= '3.7'", - "version": "==2.31.0" - }, - "requests-toolbelt": { - "hashes": [ - "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", - "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, - "rfc3986": { - "hashes": [ - "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", - "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "rich": { - "hashes": [ - "sha256:3fba9dd15ebe048e2795a02ac19baee79dc12cc50b074ef70f2958cd651b59a9", - "sha256:ce5c714e984a2d185399e4e1dd1f8b2feacb7cecfc576f1522425643a36a57ea" - ], - "markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'", - "version": "==12.0.1" - }, - "secretstorage": { - "hashes": [ - "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", - "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99" - ], - "markers": "sys_platform == 'linux'", - "version": "==3.3.3" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "twine": { - "hashes": [ - "sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8", - "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8" - ], - "index": "Brain Brew", - "markers": "python_version >= '3.7'", - "version": "==4.0.2" - }, - "typing-extensions": { - "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" - ], - "index": "Brain Brew", - "version": "==3.10.0.0" - }, - "urllib3": { - "hashes": [ - "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", - "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.7" - }, - "wcwidth": { - "hashes": [ - "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", - "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" - ], - "version": "==0.2.13" - }, - "webencodings": { - "hashes": [ - "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", - "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" - ], - "version": "==0.5.1" - }, - "zipp": { - "hashes": [ - "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", - "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" - ], - "markers": "python_version >= '3.7'", - "version": "==3.15.0" - } - } -} diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 1dbfcb7..0000000 --- a/TODO.md +++ /dev/null @@ -1,9 +0,0 @@ -# Todos - -- [ ] Better error messages - - No stack trace, unless they add -v -- [ ] Save note model to yaml - - Respect the current positions of the child files - - Be able to do it for multiple note models at a time, while checking their shared components are the same -- [ ] Save headers build task - - Remove the save_to_file diff --git a/brain_brew/__init__.py b/brain_brew/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/build_tasks/__init__.py b/brain_brew/build_tasks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/build_tasks/crowd_anki/__init__.py b/brain_brew/build_tasks/crowd_anki/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/build_tasks/crowd_anki/crowd_anki_generate.py b/brain_brew/build_tasks/crowd_anki/crowd_anki_generate.py deleted file mode 100644 index b9d99e2..0000000 --- a/brain_brew/build_tasks/crowd_anki/crowd_anki_generate.py +++ /dev/null @@ -1,81 +0,0 @@ -from dataclasses import dataclass, field -from typing import Union, Optional, List, Set - -from brain_brew.build_tasks.crowd_anki.headers_to_crowd_anki import HeadersToCrowdAnki -from brain_brew.build_tasks.crowd_anki.media_group_to_crowd_anki import MediaGroupToCrowdAnki -from brain_brew.build_tasks.crowd_anki.note_models_to_crowd_anki import NoteModelsToCrowdAnki -from brain_brew.build_tasks.crowd_anki.notes_to_crowd_anki import NotesToCrowdAnki -from brain_brew.commands.run_recipe.build_task import TopLevelBuildTask -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.generic.media_file import MediaFile -from brain_brew.representation.json.crowd_anki_export import CrowdAnkiExport -from brain_brew.representation.json.wrappers_for_crowd_anki import CrowdAnkiJsonWrapper - - -@dataclass -class CrowdAnkiGenerate(TopLevelBuildTask): - @classmethod - def task_name(cls) -> str: - return r'generate_crowd_anki' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - folder: str() - headers: str() - notes: include('{NotesToCrowdAnki.task_name()}') - note_models: include('{NoteModelsToCrowdAnki.task_name()}') - media: include('{MediaGroupToCrowdAnki.task_name()}', required=False) - ''' - - @classmethod - def yamale_dependencies(cls) -> set: - return {NotesToCrowdAnki, NoteModelsToCrowdAnki, MediaGroupToCrowdAnki} - - @dataclass - class Representation(RepresentationBase): - folder: str - notes: dict - note_models: dict - headers: dict - media: Optional[dict] = field(default_factory=lambda: dict()) - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - crowd_anki_export=CrowdAnkiExport.create_or_get(rep.folder), - notes_transform=NotesToCrowdAnki.from_repr(rep.notes), - note_model_transform=NoteModelsToCrowdAnki.from_repr(rep.note_models), - headers_transform=HeadersToCrowdAnki.from_repr(rep.headers), - media_transform=MediaGroupToCrowdAnki.from_repr(rep.media) if rep.media else None - ) - - rep: Representation - crowd_anki_export: CrowdAnkiExport - notes_transform: NotesToCrowdAnki - note_model_transform: NoteModelsToCrowdAnki - headers_transform: HeadersToCrowdAnki - media_transform: Optional[MediaGroupToCrowdAnki] - - def execute(self): - headers = self.headers_transform.execute() - ca_wrapper = CrowdAnkiJsonWrapper(headers) - - note_models: List[dict] = self.note_model_transform.execute() - - nm_name_to_id: dict = {model.part_id: model.part.id for model in self.note_model_transform.note_models} - notes = self.notes_transform.execute(nm_name_to_id) - - media_files: Set[MediaFile] = set() - if self.media_transform: - media_files = self.media_transform.execute(self.crowd_anki_export.media_loc) - - ca_wrapper.media_files = sorted([m.filename for m in media_files]) - ca_wrapper.name = self.headers_transform.headers.name - ca_wrapper.note_models = note_models - ca_wrapper.notes = notes - - # Set to CrowdAnkiExport - self.crowd_anki_export.write_to_files(ca_wrapper.data) diff --git a/brain_brew/build_tasks/crowd_anki/headers_from_crowdanki.py b/brain_brew/build_tasks/crowd_anki/headers_from_crowdanki.py deleted file mode 100644 index 3f36ba7..0000000 --- a/brain_brew/build_tasks/crowd_anki/headers_from_crowdanki.py +++ /dev/null @@ -1,69 +0,0 @@ -from dataclasses import dataclass, field -from typing import Union, Optional - -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.json.crowd_anki_export import CrowdAnkiExport -from brain_brew.representation.json.wrappers_for_crowd_anki import CA_NOTE_MODELS, CA_NOTES, CA_MEDIA_FILES, \ - CA_CHILDREN, CA_TYPE -from brain_brew.representation.json.wrappers_for_crowd_anki import CrowdAnkiJsonWrapper -from brain_brew.representation.yaml.headers import Headers - -headers_skip_keys = [CA_NOTE_MODELS, CA_NOTES, CA_MEDIA_FILES] -headers_default_values = { - CA_TYPE: "Deck", - CA_CHILDREN: [], -} - - -@dataclass -class HeadersFromCrowdAnki(BuildPartTask): - @classmethod - def task_name(cls) -> str: - return r'headers_from_crowd_anki' - - @classmethod - def task_regex(cls) -> str: - return r'headers?_from_crowd_anki' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - source: str() - save_to_file: str(required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - part_id: str - source: str - save_to_file: Optional[str] = field(default=None) - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - ca_export=CrowdAnkiExport.create_or_get(rep.source), - part_id=rep.part_id, - save_to_file=rep.save_to_file - ) - - rep: Representation - part_id: str - ca_export: CrowdAnkiExport - save_to_file: Optional[str] - - def execute(self): - ca_wrapper: CrowdAnkiJsonWrapper = self.ca_export.json_data - - headers = Headers(self.crowd_anki_to_headers(ca_wrapper.data)) - - return PartHolder.override_or_create(self.part_id, self.save_to_file, headers) - - @staticmethod - def crowd_anki_to_headers(ca_data: dict): - return {key: value for key, value in ca_data.items() - if key not in headers_skip_keys and key not in headers_default_values.keys()} diff --git a/brain_brew/build_tasks/crowd_anki/headers_to_crowd_anki.py b/brain_brew/build_tasks/crowd_anki/headers_to_crowd_anki.py deleted file mode 100644 index 9a85a7e..0000000 --- a/brain_brew/build_tasks/crowd_anki/headers_to_crowd_anki.py +++ /dev/null @@ -1,42 +0,0 @@ -from dataclasses import dataclass -from typing import Union - -from brain_brew.build_tasks.crowd_anki.headers_from_crowdanki import headers_default_values -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.yaml.headers import Headers - - -@dataclass -class HeadersToCrowdAnki: - @dataclass - class Representation(RepresentationBase): - part_id: str - - @classmethod - def from_repr(cls, data: Union[Representation, dict, str]): - rep: cls.Representation - if isinstance(data, cls.Representation): - rep = data - elif isinstance(data, dict): - rep = cls.Representation.from_dict(data) - else: - rep = cls.Representation(part_id=data) # Support single string being passed in - - return cls( - rep=rep, - headers=PartHolder.from_file_manager(rep.part_id).part - ) - - rep: Representation - headers: Headers - - def execute(self) -> dict: - headers = self.headers_to_crowd_anki(self.headers.data_without_name) - - return headers - - @staticmethod - def headers_to_crowd_anki(headers_data: dict): - return {**headers_default_values, **headers_data} - diff --git a/brain_brew/build_tasks/crowd_anki/media_group_from_crowd_anki.py b/brain_brew/build_tasks/crowd_anki/media_group_from_crowd_anki.py deleted file mode 100644 index 29cf421..0000000 --- a/brain_brew/build_tasks/crowd_anki/media_group_from_crowd_anki.py +++ /dev/null @@ -1,39 +0,0 @@ -from dataclasses import dataclass -from typing import Union - -from brain_brew.build_tasks.deck_parts.media_group_from_folder import MediaGroupFromFolder -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.representation.json.crowd_anki_export import CrowdAnkiExport -from brain_brew.representation.yaml.media_group import MediaGroup -from brain_brew.transformers.create_media_group_from_location import create_media_group_from_location - - -@dataclass -class MediaGroupFromCrowdAnki(MediaGroupFromFolder): - @classmethod - def task_name(cls) -> str: - return r"media_group_from_crowd_anki" - - @classmethod - def from_repr(cls, data: Union[MediaGroupFromFolder.Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - - cae: CrowdAnkiExport = CrowdAnkiExport.create_or_get(rep.source) - return cls( - rep=rep, - part=create_media_group_from_location( - part_id=rep.part_id, - save_to_file=rep.save_to_file, - media_group=MediaGroup.from_directory(cae.media_loc, rep.recursive), - groups_to_blacklist=list(holder.part for holder in - map(PartHolder.from_file_manager, rep.filter_blacklist_from_parts)), - groups_to_whitelist=list(holder.part for holder in - map(PartHolder.from_file_manager, rep.filter_whitelist_from_parts)) - ) - ) - - rep: MediaGroupFromFolder.Representation - part: MediaGroup - - def execute(self): - pass diff --git a/brain_brew/build_tasks/crowd_anki/media_group_to_crowd_anki.py b/brain_brew/build_tasks/crowd_anki/media_group_to_crowd_anki.py deleted file mode 100644 index 705d3a0..0000000 --- a/brain_brew/build_tasks/crowd_anki/media_group_to_crowd_anki.py +++ /dev/null @@ -1,40 +0,0 @@ -from dataclasses import dataclass -from typing import Union, List, Set - -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr -from brain_brew.representation.generic.media_file import MediaFile -from brain_brew.representation.yaml.media_group import MediaGroup -from brain_brew.transformers.save_media_group_to_location import save_media_groups_to_location - - -@dataclass -class MediaGroupToCrowdAnki(YamlRepr): - @classmethod - def task_name(cls) -> str: - return r'media_group_to_crowd_anki' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - parts: list(str()) - ''' - - @dataclass - class Representation(RepresentationBase): - parts: List[str] - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - parts=list(holder.part for holder in map(PartHolder.from_file_manager, rep.parts)) - ) - - rep: Representation - parts: List[MediaGroup] - - def execute(self, ca_media_folder: str) -> Set[MediaFile]: - return save_media_groups_to_location(self.parts, ca_media_folder, True, False) diff --git a/brain_brew/build_tasks/crowd_anki/note_model_single_from_crowd_anki.py b/brain_brew/build_tasks/crowd_anki/note_model_single_from_crowd_anki.py deleted file mode 100644 index 583ad5f..0000000 --- a/brain_brew/build_tasks/crowd_anki/note_model_single_from_crowd_anki.py +++ /dev/null @@ -1,62 +0,0 @@ -from dataclasses import dataclass, field -from typing import Optional, Union - -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.json.crowd_anki_export import CrowdAnkiExport -from brain_brew.representation.json.wrappers_for_crowd_anki import CrowdAnkiJsonWrapper -from brain_brew.representation.yaml.note_model import NoteModel - - -@dataclass -class NoteModelSingleFromCrowdAnki(BuildPartTask): - @classmethod - def task_name(cls) -> str: - return r'note_model_from_crowd_anki' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - source: str() - model_name: str(required=False) - save_to_file: str(required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - part_id: str - source: str - model_name: Optional[str] = field(default=None) - save_to_file: Optional[str] = field(default=None) - # TODO: fields: Optional[List[str]] - # TODO: templates: Optional[List[str]] - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - ca_export=CrowdAnkiExport.create_or_get(rep.source), - part_id=rep.part_id, - model_name=rep.model_name or rep.part_id, - save_to_file=rep.save_to_file - ) - - rep: Representation - part_id: str - ca_export: CrowdAnkiExport - model_name: str - save_to_file: Optional[str] - - def execute(self): - ca_wrapper: CrowdAnkiJsonWrapper = self.ca_export.json_data - - note_models_dict = {model.get('name'): model for model in ca_wrapper.note_models} - - if self.model_name not in note_models_dict: - raise ReferenceError(f"Missing Note Model '{self.model_name}' in CrowdAnki file") - - part = NoteModel.from_crowdanki(note_models_dict[self.model_name]) - return PartHolder.override_or_create(self.part_id, self.save_to_file, part) diff --git a/brain_brew/build_tasks/crowd_anki/note_models_all_from_crowd_anki.py b/brain_brew/build_tasks/crowd_anki/note_models_all_from_crowd_anki.py deleted file mode 100644 index 17e2950..0000000 --- a/brain_brew/build_tasks/crowd_anki/note_models_all_from_crowd_anki.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -from dataclasses import dataclass, field -from typing import Optional, Union, List - -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.json.crowd_anki_export import CrowdAnkiExport -from brain_brew.representation.json.wrappers_for_crowd_anki import CrowdAnkiJsonWrapper -from brain_brew.representation.yaml.note_model import NoteModel - - -@dataclass -class NoteModelsAllFromCrowdAnki(BuildPartTask): - @classmethod - def task_name(cls) -> str: - return r'note_models_all_from_crowd_anki' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - source: str() - ''' - - @dataclass - class Representation(RepresentationBase): - source: str - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - ca_export=CrowdAnkiExport.create_or_get(rep.source) - ) - - rep: Representation - ca_export: CrowdAnkiExport - - def execute(self) -> List[PartHolder[NoteModel]]: - ca_wrapper: CrowdAnkiJsonWrapper = self.ca_export.json_data - - note_models_dict = {model.get('name'): model for model in ca_wrapper.note_models} - - parts = [] - for name, model in note_models_dict.items(): - parts.append(PartHolder.override_or_create(name, None, NoteModel.from_crowdanki(model))) - - logging.info(f"Found {len(parts)} note model{'s' if len(parts) > 1 else ''} in CrowdAnki Export: '" - + "', '".join(note_models_dict.keys()) + "'") - - return parts diff --git a/brain_brew/build_tasks/crowd_anki/note_models_to_crowd_anki.py b/brain_brew/build_tasks/crowd_anki/note_models_to_crowd_anki.py deleted file mode 100644 index a08e613..0000000 --- a/brain_brew/build_tasks/crowd_anki/note_models_to_crowd_anki.py +++ /dev/null @@ -1,91 +0,0 @@ -from dataclasses import dataclass, field -from typing import Union, List - -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr -from brain_brew.representation.yaml.note_model import NoteModel - - -@dataclass -class NoteModelsToCrowdAnki(YamlRepr): - @classmethod - def task_name(cls) -> str: - return r'note_models_to_crowd_anki' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - parts: list(include('{cls.NoteModelListItem.task_name()}')) - ''' - - @classmethod - def yamale_dependencies(cls) -> set: - return {cls.NoteModelListItem} - - @dataclass - class NoteModelListItem(YamlRepr): - @classmethod - def task_name(cls) -> str: - return r'note_models_to_crowd_anki_item' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - ''' - - @dataclass - class Representation(RepresentationBase): - part_id: str - # TODO: fields: Optional[List[str]] - # TODO: templates: Optional[List[str]] - - @classmethod - def from_repr(cls, data: Union[Representation, dict, str]): - rep: cls.Representation - if isinstance(data, cls.Representation): - rep = data - elif isinstance(data, dict): - rep = cls.Representation.from_dict(data) - else: - rep = cls.Representation(part_id=data) # Support string - - return cls( - rep=rep, - part_to_read=rep.part_id - ) - - def get_note_model(self) -> PartHolder[NoteModel]: - self.part = PartHolder.from_file_manager(self.part_to_read) - return self.part # Todo: add filters in here - - rep: Representation - part: PartHolder[NoteModel] = field(init=False) - part_to_read: str - - @dataclass - class Representation(RepresentationBase): - parts: List[Union[dict, str]] - - @classmethod - def from_repr(cls, data: Union[Representation, dict, List[str]]): - rep: cls.Representation - if isinstance(data, cls.Representation): - rep = data - elif isinstance(data, dict): - rep = cls.Representation.from_dict(data) - else: - rep = cls.Representation(parts=data) # Support list of Note Models - - note_model_items = list(map(cls.NoteModelListItem.from_repr, rep.parts)) - return cls( - rep=rep, - note_models=[nm.get_note_model() for nm in note_model_items] - ) - - rep: Representation - note_models: List[PartHolder[NoteModel]] - - def execute(self) -> List[dict]: - return [model.part.encode_as_crowdanki() for model in self.note_models] diff --git a/brain_brew/build_tasks/crowd_anki/notes_from_crowd_anki.py b/brain_brew/build_tasks/crowd_anki/notes_from_crowd_anki.py deleted file mode 100644 index d38fc27..0000000 --- a/brain_brew/build_tasks/crowd_anki/notes_from_crowd_anki.py +++ /dev/null @@ -1,81 +0,0 @@ -import logging -from dataclasses import dataclass, field -from typing import Union, Optional, List - -from brain_brew.build_tasks.crowd_anki.shared_base_notes import SharedBaseNotes -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.json.crowd_anki_export import CrowdAnkiExport -from brain_brew.representation.json.wrappers_for_crowd_anki import CrowdAnkiJsonWrapper, CrowdAnkiNoteWrapper -from brain_brew.representation.yaml.notes import Notes, Note - - -@dataclass -class NotesFromCrowdAnki(SharedBaseNotes, BuildPartTask): - @classmethod - def task_name(cls) -> str: - return r'notes_from_crowd_anki' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - source: str() - sort_order: list(str(), required=False) - save_to_file: str(required=False) - reverse_sort: str(required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - part_id: str - source: str - sort_order: Optional[List[str]] = field(default_factory=lambda: None) - reverse_sort: Optional[bool] = field(default_factory=lambda: None) - save_to_file: Optional[str] = field(default=None) - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - ca_export=CrowdAnkiExport.create_or_get(rep.source), - part_id=rep.part_id, - sort_order=SharedBaseNotes._get_sort_order(rep.sort_order), - reverse_sort=SharedBaseNotes._get_reverse_sort(rep.reverse_sort), - save_to_file=rep.save_to_file - ) - - rep: Representation - part_id: str - ca_export: CrowdAnkiExport - sort_order: Optional[List[str]] - reverse_sort: Optional[bool] - save_to_file: Optional[str] - - def execute(self) -> PartHolder[Notes]: - ca_wrapper: CrowdAnkiJsonWrapper = self.ca_export.json_data - if ca_wrapper.children: - logging.warning("Child Decks / Sub-decks are not currently supported.") - - ca_models = self.ca_export.note_models - ca_notes = ca_wrapper.notes - - nm_id_to_name: dict = {model.id: model.name for model in ca_models} - note_list = [self.ca_note_to_note(note, nm_id_to_name) for note in ca_notes] - - notes = Notes.from_list_of_notes(note_list) # TODO: pass in sort method - return PartHolder.override_or_create(self.part_id, self.save_to_file, notes) - - @staticmethod - def ca_note_to_note(note: dict, nm_id_to_name: dict) -> Note: - wrapper = CrowdAnkiNoteWrapper(note) - - return Note( - note_model=nm_id_to_name[wrapper.note_model], - tags=wrapper.tags, - guid=wrapper.guid, - fields=wrapper.fields, - flags=wrapper.flags - ) diff --git a/brain_brew/build_tasks/crowd_anki/notes_to_crowd_anki.py b/brain_brew/build_tasks/crowd_anki/notes_to_crowd_anki.py deleted file mode 100644 index 3a249ce..0000000 --- a/brain_brew/build_tasks/crowd_anki/notes_to_crowd_anki.py +++ /dev/null @@ -1,96 +0,0 @@ -from dataclasses import dataclass, field -from typing import Optional, Union, List - -from brain_brew.build_tasks.crowd_anki.shared_base_notes import SharedBaseNotes -from brain_brew.build_tasks.overrides.notes_override import NotesOverride -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr -from brain_brew.representation.json.wrappers_for_crowd_anki import CrowdAnkiNoteWrapper -from brain_brew.representation.yaml.notes import Notes, Note -from brain_brew.utils import blank_str_if_none - - -@dataclass -class NotesToCrowdAnki(YamlRepr, SharedBaseNotes): - @classmethod - def task_name(cls) -> str: - return r'notes_to_crowd_anki' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - sort_order: list(str(), required=False) - reverse_sort: bool(required=False) - additional_items_to_add: map(str(), key=str(), required=False) - override: include('{NotesOverride.task_name()}', required=False) - case_insensitive_sort: bool(required=False) - ''' - - @classmethod - def yamale_dependencies(cls) -> set: - return {NotesOverride} - - @dataclass - class Representation(RepresentationBase): - part_id: str - additional_items_to_add: Optional[dict] = field(default_factory=lambda: None) - sort_order: Optional[List[str]] = field(default_factory=lambda: None) - reverse_sort: Optional[bool] = field(default_factory=lambda: None) - override: Optional[dict] = field(default_factory=lambda: None) - case_insensitive_sort: Optional[bool] = field(default_factory=lambda: None) - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - notes=PartHolder.from_file_manager(rep.part_id).part, - sort_order=SharedBaseNotes._get_sort_order(rep.sort_order), - reverse_sort=SharedBaseNotes._get_reverse_sort(rep.reverse_sort), - additional_items_to_add=rep.additional_items_to_add or {}, - override=NotesOverride.from_repr(rep.override) if rep.override else None, - case_insensitive_sort=rep.case_insensitive_sort or True - ) - - rep: Representation - notes: Notes - additional_items_to_add: dict - sort_order: Optional[List[str]] = field(default_factory=lambda: None) - reverse_sort: Optional[bool] = field(default_factory=lambda: None) - override: Optional[NotesOverride] = field(default_factory=lambda: None) - case_insensitive_sort: bool = field(default=True) - - def execute(self, nm_name_to_id: dict) -> List[dict]: - - notes = self.notes.get_sorted_notes_copy( - sort_by_keys=self.sort_order, - reverse_sort=self.reverse_sort, - case_insensitive_sort=self.case_insensitive_sort - ) - - if self.override: - notes = [self.override.override(note) for note in notes] - - note_dicts = [self.note_to_ca_note(note, nm_name_to_id, self.additional_items_to_add) for note in notes] - - return note_dicts - - @staticmethod - def note_to_ca_note(note: Note, nm_name_to_id: dict, additional_items_to_add: dict) -> dict: - wrapper = CrowdAnkiNoteWrapper({ - "__type__": "Note", - "data": "" - }) - - for key, value in additional_items_to_add.items(): - wrapper.data[key] = blank_str_if_none(value) - - wrapper.fields = note.fields - wrapper.flags = note.flags - wrapper.guid = note.guid - wrapper.note_model = nm_name_to_id[note.note_model] - wrapper.tags = note.tags - - return wrapper.data diff --git a/brain_brew/build_tasks/crowd_anki/shared_base_notes.py b/brain_brew/build_tasks/crowd_anki/shared_base_notes.py deleted file mode 100644 index 9ad016f..0000000 --- a/brain_brew/build_tasks/crowd_anki/shared_base_notes.py +++ /dev/null @@ -1,20 +0,0 @@ -from dataclasses import dataclass -from typing import Optional, Union, List - - -@dataclass -class SharedBaseNotes: - @staticmethod - def _get_sort_order(sort_order: Optional[Union[str, List[str]]]): - if isinstance(sort_order, list): - return sort_order - elif isinstance(sort_order, str): - return [sort_order] - return [] - - @staticmethod - def _get_reverse_sort(reverse_sort: Optional[bool]): - return reverse_sort or False - - # sort_order: Optional[List[str]] - # reverse_sort: Optional[bool] diff --git a/brain_brew/build_tasks/csvs/__init__.py b/brain_brew/build_tasks/csvs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/build_tasks/csvs/csvs_generate.py b/brain_brew/build_tasks/csvs/csvs_generate.py deleted file mode 100644 index 3cdfba1..0000000 --- a/brain_brew/build_tasks/csvs/csvs_generate.py +++ /dev/null @@ -1,101 +0,0 @@ -from dataclasses import dataclass -import logging -from typing import List, Dict, Union - -from brain_brew.build_tasks.csvs.shared_base_csvs import SharedBaseCsvs -from brain_brew.commands.run_recipe.build_task import TopLevelBuildTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.representation.yaml.notes import Notes, Note -from brain_brew.transformers.file_mapping import FileMapping -from brain_brew.transformers.note_model_mapping import NoteModelMapping -from brain_brew.utils import join_tags - - -@dataclass -class CsvsGenerate(SharedBaseCsvs, TopLevelBuildTask): - @classmethod - def task_name(cls) -> str: - return r'generate_csvs' - - @classmethod - def task_regex(cls) -> str: - return r'generate_csvs?' - - @classmethod - def yamale_schema(cls) -> str: # TODO: Use NotesOverride here, just as in NotesToCrowdAnki - return f'''\ - notes: str() - note_model_mappings: list(include('{NoteModelMapping.task_name()}')) - file_mappings: list(include('{FileMapping.task_name()}')) - ''' - - @classmethod - def yamale_dependencies(cls) -> set: - return {NoteModelMapping, FileMapping} - - @dataclass - class Representation(SharedBaseCsvs.Representation): - notes: str - - def encode(self): - return { - "notes": self.notes, - "file_mappings": [fm.encode() for fm in self.file_mappings], - "note_model_mappings": [nmm.encode() for nmm in self.note_model_mappings] - } - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - notes=PartHolder.from_file_manager(rep.notes), - file_mappings=rep.get_file_mappings(), - note_model_mappings={k: v for nm in rep.note_model_mappings for k, v in cls.map_nmm(nm).items()} - ) - - rep: Representation - notes: PartHolder[Notes] # TODO: Accept Multiple Note Parts - - def execute(self): - self.verify_contents() - - notes: List[Note] = self.notes.part.get_sorted_notes_copy( - sort_by_keys=[], - reverse_sort=False, - case_insensitive_sort=True - ) - self.verify_notes_match_note_model_mappings(notes) - - if not self.file_mappings[0].csv_file.column_headers: - logging.warning("Empty top level csv found. Populating headers automatically.") - model_name = self.file_mappings[0].note_model - self.file_mappings[0].csv_file.set_data_from_superset({}, column_header_override=list(f.value for f in self.note_model_mappings[model_name].columns_manually_mapped)) - - for fm in self.file_mappings: - csv_data: List[dict] = [self.note_to_csv_row(note, self.note_model_mappings) for note in notes - if note.note_model in fm.get_used_note_model_names()] - rows_by_guid = {row["guid"]: row for row in csv_data} - - fm.compile_data() - fm.set_relevant_data(rows_by_guid) - fm.write_file_on_close() - - def verify_notes_match_note_model_mappings(self, notes: List[Note]): - note_models_used = {note.note_model for note in notes} - errors = [TypeError(f"Unknown note model type '{model}' in deck part '{self.notes.part_id}'. " - f"Add mapping for that model.") - for model in note_models_used if model not in self.note_model_mappings.keys()] - - if errors: - raise Exception(errors) - - @staticmethod - def note_to_csv_row(note: Note, note_model_mappings: Dict[str, NoteModelMapping]) -> dict: - nm_name = note.note_model - row = note_model_mappings[nm_name].note_models[nm_name].part.zip_field_to_data(note.fields) - row["guid"] = note.guid - row["tags"] = join_tags(note.tags) - # TODO: Flags? - - return note_model_mappings[nm_name].note_fields_map_to_csv_row(row) diff --git a/brain_brew/build_tasks/csvs/generate_guids_in_csvs.py b/brain_brew/build_tasks/csvs/generate_guids_in_csvs.py deleted file mode 100644 index 493a3e1..0000000 --- a/brain_brew/build_tasks/csvs/generate_guids_in_csvs.py +++ /dev/null @@ -1,88 +0,0 @@ -import logging -from dataclasses import dataclass, field -from typing import List, Union, Optional - -from brain_brew.commands.run_recipe.build_task import TopLevelBuildTask -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.generic.csv_file import CsvFile -from brain_brew.utils import single_item_to_list, generate_anki_guid - - -@dataclass -class GenerateGuidsInCsvs(TopLevelBuildTask): - execute_immediately = True - - @classmethod - def task_name(cls) -> str: - return r'generate_guids_in_csvs' - - @classmethod - def task_regex(cls) -> str: - return r'generate_guids_in_csvs?' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - source: any(str(), list(str())) - columns: any(str(), list(str())) - delimiter: str(required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - source: Union[str, List[str]] - columns: Union[str, List[str]] - delimiter: Optional[str] = field(default=None) - - def encode_filter(self, key, value): - if not super().encode_filter(key, value): - return False - if key == 'delimiter' and all(CsvFile.delimiter_matches_file_type(value, f) for f in single_item_to_list(self.source)): - return False - return True - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - csv_files = [CsvFile.create_or_get(csv) for csv in single_item_to_list(rep.source)] - for c in csv_files: - c.set_delimiter(rep.delimiter) - c.read_file() - return cls( - rep=rep, - sources=csv_files, - columns=rep.columns - ) - - rep: Representation - sources: List[CsvFile] - columns: List[str] - - def execute(self): - logging.info("Attempting to generate Guids") - - errors = [] - - # Make sure the columns exist on all - for source in self.sources: - missing = [c for c in self.columns if c not in source.column_headers] - if any(missing): - errors.append(f"Csv '{source.file_location}' does not contain all the specified columns: {missing}") - - if errors: - raise KeyError(errors) - - for source in self.sources: - guids_generated = 0 - data = source.get_data() - for row in data: - for column_name in row.keys(): - if column_name in self.columns and not row[column_name]: - row[column_name] = generate_anki_guid() - guids_generated += 1 - if guids_generated > 0: - logging.info(f"Generated {guids_generated} guids in csv '{source.file_location}'") - source.set_data(data) - source.write_file() - - logging.info("Generate guids complete") diff --git a/brain_brew/build_tasks/csvs/notes_from_csvs.py b/brain_brew/build_tasks/csvs/notes_from_csvs.py deleted file mode 100644 index bd2beb4..0000000 --- a/brain_brew/build_tasks/csvs/notes_from_csvs.py +++ /dev/null @@ -1,92 +0,0 @@ -from dataclasses import dataclass, field -from typing import Dict, List, Union, Optional - -from brain_brew.build_tasks.csvs.shared_base_csvs import SharedBaseCsvs -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.representation.yaml.notes import Note, Notes -from brain_brew.transformers.file_mapping import FileMapping -from brain_brew.transformers.note_model_mapping import NoteModelMapping -from brain_brew.utils import split_tags - - -@dataclass -class NotesFromCsvs(SharedBaseCsvs, BuildPartTask): - @classmethod - def task_name(cls) -> str: - return r'notes_from_csvs' - - @classmethod - def task_regex(cls) -> str: - return r'notes_from_csvs?' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - save_to_file: str(required=False) - note_model_mappings: list(include('{NoteModelMapping.task_name()}')) - file_mappings: list(include('{FileMapping.task_name()}')) - ''' - - @classmethod - def yamale_dependencies(cls) -> set: - return {NoteModelMapping, FileMapping} - - @dataclass - class Representation(SharedBaseCsvs.Representation): - part_id: str - save_to_file: Optional[str] = field(default=None) - - def encode(self): - return { - "part_id": self.part_id, - "save_to_file": self.save_to_file, - "file_mappings": [fm.encode() for fm in self.file_mappings], - "note_model_mappings": [nmm.encode() for nmm in self.note_model_mappings] - } - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - part_id=rep.part_id, - save_to_file=rep.save_to_file, - file_mappings=rep.get_file_mappings(), - note_model_mappings={k: v for nm in rep.note_model_mappings for k, v in cls.map_nmm(nm).items()} - ) - - rep: Representation - part_id: str - save_to_file: Optional[str] - - def execute(self): - self.verify_contents() - - csv_data_by_guid: Dict[str, dict] = {} - for csv_map in self.file_mappings: - csv_map.compile_data() - csv_data_by_guid = {**csv_data_by_guid, **csv_map.compiled_data} - csv_map.write_file_on_close() - csv_rows: List[dict] = list(csv_data_by_guid.values()) - - notes_part: List[Note] = [self.csv_row_to_note(row, self.note_model_mappings) for row in csv_rows] - - notes = Notes.from_list_of_notes(notes_part) - PartHolder.override_or_create(self.part_id, self.save_to_file, notes) - - @staticmethod - def csv_row_to_note(row: dict, note_model_mappings: Dict[str, NoteModelMapping]) -> Note: - note_model_name = row["note_model"] # TODO: Use object - row_nm: NoteModelMapping = note_model_mappings[note_model_name] - - filtered_fields = row_nm.csv_row_map_to_note_fields(row) - - guid = filtered_fields.pop("guid") - tags = split_tags(filtered_fields.pop("tags")) - flags = filtered_fields.pop("flags") if "flags" in filtered_fields else 0 - - fields = row_nm.field_values_in_note_model_order(note_model_name, filtered_fields) - - return Note(guid=guid, tags=tags, note_model=note_model_name, fields=fields, flags=flags) diff --git a/brain_brew/build_tasks/csvs/shared_base_csvs.py b/brain_brew/build_tasks/csvs/shared_base_csvs.py deleted file mode 100644 index 916b61d..0000000 --- a/brain_brew/build_tasks/csvs/shared_base_csvs.py +++ /dev/null @@ -1,63 +0,0 @@ -import logging -from dataclasses import dataclass -from typing import List, Dict - -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.transformers.file_mapping import FileMapping -from brain_brew.transformers.note_model_mapping import NoteModelMapping - - -@dataclass -class SharedBaseCsvs: - @dataclass(init=False) - class Representation(RepresentationBase): - file_mappings: List[FileMapping.Representation] - note_model_mappings: List[NoteModelMapping.Representation] - - def __init__(self, file_mappings, note_model_mappings): - self.file_mappings = list(map(FileMapping.Representation.from_dict, file_mappings)) - self.note_model_mappings = list(map(NoteModelMapping.Representation.from_dict, note_model_mappings)) - - def get_file_mappings(self) -> List[FileMapping]: - return list(map(FileMapping.from_repr, self.file_mappings)) - - file_mappings: List[FileMapping] - note_model_mappings: Dict[str, NoteModelMapping] - - @classmethod - def map_nmm(cls, nmm_to_map): - nmm = NoteModelMapping.from_repr(nmm_to_map) - return nmm.get_note_model_mapping_dict() - - def verify_contents(self): - errors = [] - - for nm in self.note_model_mappings.values(): - try: - nm.verify_contents() - except KeyError as e: - errors.append(e) - - # Check all referenced note models have a mapping - for csv_map in self.file_mappings: - for nm in csv_map.get_used_note_model_names(): - if nm not in self.note_model_mappings.keys(): - errors.append(f"Missing Note Model Map for {nm}") - - # Check each of the Csvs (or their derivatives) contain all the necessary columns for their stated note model - for cfm in self.file_mappings: - note_model_names = cfm.get_used_note_model_names() - available_columns = cfm.get_available_columns() - - referenced_note_models_maps = [value for key, value in self.note_model_mappings.items() - if key in note_model_names] - for nm_map in referenced_note_models_maps: - for holder in nm_map.note_models.values(): - if holder.part.name in note_model_names: - missing_columns = [col for col in holder.part.field_names_lowercase if - col not in nm_map.csv_headers_map_to_note_fields(available_columns)] - if missing_columns: - logging.warning(f"Csvs are missing columns from {holder.part_id} {missing_columns}") - - if errors: - raise Exception(errors) diff --git a/brain_brew/build_tasks/deck_parts/__init__.py b/brain_brew/build_tasks/deck_parts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/build_tasks/deck_parts/from_yaml_part.py b/brain_brew/build_tasks/deck_parts/from_yaml_part.py deleted file mode 100644 index a87b54d..0000000 --- a/brain_brew/build_tasks/deck_parts/from_yaml_part.py +++ /dev/null @@ -1,61 +0,0 @@ -from abc import ABCMeta -from dataclasses import dataclass -from typing import Union - -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.yaml.media_group import MediaGroup -from brain_brew.representation.yaml.notes import Notes -from brain_brew.representation.yaml.yaml_object import YamlObject - - -@dataclass -class FromYamlPartBase(BuildPartTask, metaclass=ABCMeta): - part_type = None - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - file: str() - ''' - - @dataclass - class Representation(RepresentationBase): - part_id: str - file: str - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - - return cls( - rep=rep, - part=PartHolder.override_or_create( - part_id=rep.part_id, save_to_file=None, part=cls.part_type.from_yaml_file(rep.file)) - ) - - def execute(self): - pass - - rep: Representation - part: YamlObject - - -@dataclass -class NotesFromYamlPart(FromYamlPartBase): - @classmethod - def task_name(cls) -> str: - return r'notes_from_yaml_part' - - part_type = Notes - - -@dataclass -class MediaGroupFromYamlPart(FromYamlPartBase, BuildPartTask): - @classmethod - def task_name(cls) -> str: - return r'media_group_from_yaml_part' - - part_type = MediaGroup diff --git a/brain_brew/build_tasks/deck_parts/headers_from_yaml_part.py b/brain_brew/build_tasks/deck_parts/headers_from_yaml_part.py deleted file mode 100644 index b58c16f..0000000 --- a/brain_brew/build_tasks/deck_parts/headers_from_yaml_part.py +++ /dev/null @@ -1,67 +0,0 @@ -from dataclasses import dataclass, field -from typing import Union, Optional - -from brain_brew.build_tasks.overrides.headers_override import HeadersOverride -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.yaml.headers import Headers - - -@dataclass -class HeadersFromYamlPart(BuildPartTask): - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - file: str() - override: include('{HeadersOverride.task_name()}', required=False) - ''' - - @classmethod - def yamale_dependencies(cls) -> set: - return {HeadersOverride} - - @classmethod - def task_name(cls) -> str: - return r'headers_from_yaml_part' - - @classmethod - def task_regex(cls) -> str: - return r'headers?_from_yaml_part' - - @dataclass - class Representation(RepresentationBase): - part_id: str - file: str - override: Optional[dict] = field(default_factory=lambda: None) - - def encode(self): - d = { - "part_id": self.part_id, - "file": self.file - } - if self.override: - d.setdefault("override", self.override) - return d - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - headers=PartHolder.override_or_create( - part_id=rep.part_id, - save_to_file=None, - part=Headers.from_yaml_file(rep.file) - ).part, - override=HeadersOverride.from_repr(rep.override) if rep.override else None - ) - - rep: Representation - headers: Headers - override: Optional[HeadersOverride] - - def execute(self): - if self.override: - self.headers = self.override.override(self.headers) diff --git a/brain_brew/build_tasks/deck_parts/media_group_from_folder.py b/brain_brew/build_tasks/deck_parts/media_group_from_folder.py deleted file mode 100644 index 4df8644..0000000 --- a/brain_brew/build_tasks/deck_parts/media_group_from_folder.py +++ /dev/null @@ -1,58 +0,0 @@ -from dataclasses import dataclass, field -from typing import Optional, Union, List - -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.yaml.media_group import MediaGroup -from brain_brew.transformers.create_media_group_from_location import create_media_group_from_location - - -@dataclass -class MediaGroupFromFolder(BuildPartTask): - @classmethod - def task_name(cls) -> str: - return r"media_group_from_folder" - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - source: str() - save_to_file: str(required=False) - recursive: bool(required=False) - filter_whitelist_from_parts: list(str(), required=False) - filter_blacklist_from_parts: list(str(), required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - part_id: str - source: str - filter_blacklist_from_parts: List[str] = field(default_factory=list) - filter_whitelist_from_parts: List[str] = field(default_factory=list) - recursive: Optional[bool] = field(default=True) - save_to_file: Optional[str] = field(default=None) - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - part=create_media_group_from_location( - part_id=rep.part_id, - save_to_file=rep.save_to_file, - media_group=MediaGroup.from_directory(rep.source, rep.recursive), - groups_to_blacklist=list(holder.part for holder in - map(PartHolder.from_file_manager, rep.filter_blacklist_from_parts)), - groups_to_whitelist=list(holder.part for holder in - map(PartHolder.from_file_manager, rep.filter_whitelist_from_parts)) - # match criteria - ) - ) - - rep: Representation - part: MediaGroup - - def execute(self): - pass diff --git a/brain_brew/build_tasks/deck_parts/note_model_from_html_parts.py b/brain_brew/build_tasks/deck_parts/note_model_from_html_parts.py deleted file mode 100644 index cff5da2..0000000 --- a/brain_brew/build_tasks/deck_parts/note_model_from_html_parts.py +++ /dev/null @@ -1,78 +0,0 @@ -from dataclasses import dataclass, field -from typing import Optional, Union, List - -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.generic.html_file import HTMLFile -from brain_brew.representation.yaml.note_model import NoteModel -from brain_brew.representation.yaml.note_model_field import Field -from brain_brew.representation.yaml.note_model_template import Template - - -@dataclass -class NoteModelFromHTMLParts(BuildPartTask): - @classmethod - def task_name(cls) -> str: - return r'note_model_from_html_parts' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - model_id: str() - css_file: str() - fields: list(include('{Field.task_name()}')) - templates: list(str()) - model_name: str(required=False) - save_to_file: str(required=False) - ''' - - @classmethod - def yamale_dependencies(cls) -> set: - return {Field} - - @dataclass - class Representation(RepresentationBase): - part_id: str - model_id: str - css_file: str - fields: List[dict] - templates: List[dict] - model_name: Optional[str] = field(default=None) - save_to_file: Optional[str] = field(default=None) - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - part_id=rep.part_id, - model_id=rep.model_id, - css=HTMLFile.create_or_get(rep.css_file).get_data(deep_copy=True), - fields=list(map(Field.from_dict, rep.fields)), - templates=list(holder.part for holder in map(PartHolder.from_file_manager, rep.templates)), - model_name=rep.model_name or rep.part_id, - save_to_file=rep.save_to_file - ) - - rep: Representation - part_id: str - model_id: str - css: str - fields: List[Field] - templates: List[Template] - model_name: str - save_to_file: Optional[str] - - def execute(self): - part = NoteModel( - name=self.model_name, - id=self.model_id, - css=self.css, - fields=self.fields, - templates=self.templates, - required_fields_per_template=[] - ) - - PartHolder.override_or_create(self.part_id, self.save_to_file, part) diff --git a/brain_brew/build_tasks/deck_parts/note_model_from_yaml_part.py b/brain_brew/build_tasks/deck_parts/note_model_from_yaml_part.py deleted file mode 100644 index bdf5679..0000000 --- a/brain_brew/build_tasks/deck_parts/note_model_from_yaml_part.py +++ /dev/null @@ -1,45 +0,0 @@ -from dataclasses import dataclass -from typing import Union - -from brain_brew.build_tasks.deck_parts.from_yaml_part import FromYamlPartBase -from brain_brew.commands.run_recipe.build_task import BuildPartTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.yaml.note_model import NoteModel - - -@dataclass -class NoteModelsFromYamlPart(FromYamlPartBase, BuildPartTask): - @classmethod - def task_name(cls) -> str: - return r'note_models_from_yaml_part' - - @classmethod - def task_regex(cls) -> str: - return r'note_models?_from_yaml_part' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - part_id: str() - file: str() - ''' - - @dataclass - class Representation(RepresentationBase): - part_id: str - file: str - # TODO: Overrides - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - - return cls( - rep=rep, - part=PartHolder.override_or_create( - part_id=rep.part_id, save_to_file=None, part=NoteModel.from_yaml_file(rep.file)) - ) - - def execute(self): - pass diff --git a/brain_brew/build_tasks/deck_parts/save_media_group_to_folder.py b/brain_brew/build_tasks/deck_parts/save_media_group_to_folder.py deleted file mode 100644 index 4b1d707..0000000 --- a/brain_brew/build_tasks/deck_parts/save_media_group_to_folder.py +++ /dev/null @@ -1,55 +0,0 @@ -from dataclasses import dataclass, field -from typing import List, Union, Optional - -from brain_brew.commands.run_recipe.build_task import TopLevelBuildTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.yaml.media_group import MediaGroup -from brain_brew.transformers.save_media_group_to_location import save_media_groups_to_location - - -@dataclass -class SaveMediaGroupsToFolder(TopLevelBuildTask): - @classmethod - def task_name(cls) -> str: - return r'save_media_groups_to_folder' - - @classmethod - def task_regex(cls) -> str: - return r"save_media_groups?_to_folder" - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - parts: list(str()) - folder: str() - clear_folder: bool(required=False) - recursive: bool(required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - parts: List[str] - folder: str - clear_folder: Optional[bool] = field(default=None) - recursive: Optional[bool] = field(default=None) - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - parts=list(holder.part for holder in map(PartHolder.from_file_manager, rep.parts)), - folder=rep.folder, - clear_folder=rep.clear_folder or False, - recursive=rep.recursive or False - ) - - rep: Representation - parts: List[MediaGroup] - folder: str - clear_folder: bool - recursive: bool - - def execute(self): - save_media_groups_to_location(self.parts, self.folder, self.clear_folder, self.recursive) diff --git a/brain_brew/build_tasks/deck_parts/save_note_models_to_folder.py b/brain_brew/build_tasks/deck_parts/save_note_models_to_folder.py deleted file mode 100644 index a05ab04..0000000 --- a/brain_brew/build_tasks/deck_parts/save_note_models_to_folder.py +++ /dev/null @@ -1,57 +0,0 @@ -from dataclasses import dataclass, field -from typing import List, Union, Optional, Dict - -from brain_brew.commands.run_recipe.build_task import TopLevelBuildTask -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.representation.yaml.note_model import NoteModel -from brain_brew.transformers.save_note_model_to_location import save_note_model_to_location - - -@dataclass -class SaveNoteModelsToFolder(TopLevelBuildTask): - @classmethod - def task_name(cls) -> str: - return r'save_note_models_to_folder' - - @classmethod - def task_regex(cls) -> str: - return r"save_note_models?_to_folder" - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - parts: list(str()) - folder: str() - clear_existing: bool(required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - parts: List[str] - folder: str - clear_existing: Optional[bool] = field(default=None) - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - parts=list(holder.part for holder in map(PartHolder.from_file_manager, rep.parts)), - folder=rep.folder, - clear_existing=rep.clear_existing or False, - ) - - rep: Representation - parts: List[NoteModel] - folder: str - clear_existing: bool - - def execute(self) -> Dict[str, str]: - model_yaml_files: Dict[str, str] = {} - for model in self.parts: - model_yaml_files.setdefault( - model.name, - save_note_model_to_location(model, self.folder, self.clear_existing) - ) - return model_yaml_files diff --git a/brain_brew/build_tasks/overrides/__init__.py b/brain_brew/build_tasks/overrides/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/build_tasks/overrides/headers_override.py b/brain_brew/build_tasks/overrides/headers_override.py deleted file mode 100644 index 3f0b436..0000000 --- a/brain_brew/build_tasks/overrides/headers_override.py +++ /dev/null @@ -1,55 +0,0 @@ -from dataclasses import dataclass, field -from typing import Optional, Union - -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr -from brain_brew.representation.generic.html_file import HTMLFile -from brain_brew.representation.yaml.headers import Headers - - -@dataclass -class HeadersOverride(YamlRepr): - @classmethod - def task_name(cls) -> str: - return r"headers_override" - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - crowdanki_uuid: str(required=False) - deck_description_html_file: str(required=False) - name: str(required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - crowdanki_uuid: Optional[str] = field(default=None) - deck_description_html_file: Optional[str] = field(default=None) - name: Optional[str] = field(default=None) - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - crowdanki_uuid=rep.crowdanki_uuid, - deck_desc_html_file=HTMLFile.create_or_get(rep.deck_description_html_file), - name=rep.name - ) - - rep: Representation - crowdanki_uuid: Optional[str] - deck_desc_html_file: Optional[HTMLFile] - name: Optional[str] - - def override(self, header: Headers): - if self.deck_desc_html_file: - header.description = self.deck_desc_html_file.get_data(deep_copy=True) - - if self.crowdanki_uuid: - header.crowdanki_uuid = self.crowdanki_uuid - - if self.name: - header.name = self.name - - return header diff --git a/brain_brew/build_tasks/overrides/notes_override.py b/brain_brew/build_tasks/overrides/notes_override.py deleted file mode 100644 index 8ce4604..0000000 --- a/brain_brew/build_tasks/overrides/notes_override.py +++ /dev/null @@ -1,40 +0,0 @@ -from dataclasses import dataclass -from typing import Optional, Union - -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr -from brain_brew.representation.yaml.notes import Note - - -@dataclass -class NotesOverride(YamlRepr): - @classmethod - def task_name(cls) -> str: - return r"notes_override" - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - note_model: str(required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - note_model: Optional[str] - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - note_model=rep.note_model - ) - - rep: Representation - note_model: Optional[str] - - def override(self, note: Note): - if self.note_model: - note.note_model = self.note_model - - return note diff --git a/brain_brew/commands/__init__.py b/brain_brew/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/commands/argument_reader.py b/brain_brew/commands/argument_reader.py deleted file mode 100644 index 88d1371..0000000 --- a/brain_brew/commands/argument_reader.py +++ /dev/null @@ -1,117 +0,0 @@ -from enum import Enum - -import sys -from argparse import ArgumentParser - -from brain_brew.front_matter import latest_version_number -from brain_brew.commands.init_repo.init_repo import InitRepo -from brain_brew.commands.run_recipe.run_recipe import RunRecipe -from brain_brew.interfaces.command import Command - - -class Commands(Enum): - RUN_RECIPE = "run" - INIT_REPO = "init" - - -class BBArgumentReader(ArgumentParser): - def __init__(self, test_mode=False): - super().__init__( - prog="brainbrew", - description='Manage Flashcards by transforming them to various types.' - ) - - self._set_parser_arguments() - - if not test_mode and len(sys.argv) == 1: - self.print_help(sys.stderr) - sys.exit(1) - - def _set_parser_arguments(self): - - subparsers = self.add_subparsers(parser_class=ArgumentParser, help='Commands that can be run', dest="command") - - parser_run = subparsers.add_parser( - Commands.RUN_RECIPE.value, - help="Run a recipe file. This will convert some data to another format, based on the instructions in the recipe file." - ) - parser_run.add_argument( - "recipe", - metavar="recipe", - type=str, - help="Yaml file to use as the recipe" - ) - parser_run.add_argument( - "--verify", "-v", - action="store_true", - dest="verify_only", - default=False, - help="Only verify the recipe contents, without running it." - ) - - parser_init = subparsers.add_parser( - Commands.INIT_REPO.value, - help="Initialise a Brain Brew repository, using a CrowdAnki export as the base data." - ) - parser_init.add_argument( - "crowdanki_folder", - metavar="crowdanki_folder", - type=str, - help="The folder that stores the CrowdAnki files to build this repo from" - ) - parser_init.add_argument( - '--delimiter', - dest='delimiter', - action='store', - help="Set the delimiter for Csv files to specific character", - type=str - ) - parser_init.add_argument( - "--delimitertab", "--tab", - action="store_true", - dest="delimiter_tab", - default=False, - help="Use tabs as the delimiter for Csv files" - ) - - def get_parsed(self, override_args=None) -> Command: - parsed_args = self.parse_args(args=override_args) - - if parsed_args.command == Commands.RUN_RECIPE.value: - # Required - recipe = self.error_if_blank(parsed_args.recipe) - - # Optional - verify_only = parsed_args.verify_only - - return RunRecipe( - recipe_file_name=recipe, - verify_only=verify_only - ) - - if parsed_args.command == Commands.INIT_REPO.value: - # Required - crowdanki_folder = parsed_args.crowdanki_folder - delimiter = parsed_args.delimiter - delimiter_tab = parsed_args.delimiter_tab - - return InitRepo( - crowdanki_folder=crowdanki_folder, - delimiter="\t" if delimiter_tab else delimiter - ) - - raise KeyError("Unknown Command") - - def error_if_blank(self, arg): - if arg == "" or arg is None: - self.error("Required argument missing") - return arg - - def error(self, message): - sys.stderr.write('error: %s\n' % message) - self.print_help() - sys.exit(2) - - def print_help(self, message=None): - print(f"Brain Brew v{latest_version_number()}") - super().print_help(message) diff --git a/brain_brew/commands/init_repo/__init__.py b/brain_brew/commands/init_repo/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/commands/init_repo/init_repo.py b/brain_brew/commands/init_repo/init_repo.py deleted file mode 100644 index 3963376..0000000 --- a/brain_brew/commands/init_repo/init_repo.py +++ /dev/null @@ -1,208 +0,0 @@ -import os -from dataclasses import dataclass -from typing import List - -from brain_brew.build_tasks.crowd_anki.crowd_anki_generate import CrowdAnkiGenerate -from brain_brew.build_tasks.crowd_anki.headers_from_crowdanki import HeadersFromCrowdAnki -from brain_brew.build_tasks.crowd_anki.headers_to_crowd_anki import HeadersToCrowdAnki -from brain_brew.build_tasks.crowd_anki.media_group_from_crowd_anki import MediaGroupFromCrowdAnki -from brain_brew.build_tasks.crowd_anki.media_group_to_crowd_anki import MediaGroupToCrowdAnki -from brain_brew.build_tasks.crowd_anki.note_models_all_from_crowd_anki import NoteModelsAllFromCrowdAnki -from brain_brew.build_tasks.crowd_anki.note_models_to_crowd_anki import NoteModelsToCrowdAnki -from brain_brew.build_tasks.crowd_anki.notes_from_crowd_anki import NotesFromCrowdAnki -from brain_brew.build_tasks.crowd_anki.notes_to_crowd_anki import NotesToCrowdAnki -from brain_brew.build_tasks.csvs.csvs_generate import CsvsGenerate -from brain_brew.build_tasks.csvs.generate_guids_in_csvs import GenerateGuidsInCsvs -from brain_brew.build_tasks.csvs.notes_from_csvs import NotesFromCsvs -from brain_brew.build_tasks.deck_parts.headers_from_yaml_part import HeadersFromYamlPart -from brain_brew.build_tasks.deck_parts.media_group_from_folder import MediaGroupFromFolder -from brain_brew.build_tasks.deck_parts.note_model_from_yaml_part import NoteModelsFromYamlPart -from brain_brew.build_tasks.deck_parts.save_media_group_to_folder import SaveMediaGroupsToFolder -from brain_brew.build_tasks.deck_parts.save_note_models_to_folder import SaveNoteModelsToFolder -from brain_brew.commands.run_recipe.build_task import TopLevelBuildTask, BuildPartTask -from brain_brew.commands.run_recipe.parts_builder import PartsBuilder -from brain_brew.commands.run_recipe.top_level_builder import TopLevelBuilder -from brain_brew.interfaces.command import Command -from brain_brew.representation.generic.csv_file import CsvFile -from brain_brew.representation.yaml.note_model import NoteModel -from brain_brew.representation.yaml.yaml_object import YamlObject -from brain_brew.transformers.file_mapping import FileMapping -from brain_brew.transformers.note_model_mapping import NoteModelMapping -from brain_brew.utils import create_path_if_not_exists, filename_from_full_path, folder_name_from_full_path - -RECIPE_MEDIA = "deck_media" -RECIPE_HEADERS = "deck_headers" -RECIPE_NOTES = "deck_notes" - -LOC_RECIPES = "recipes/" -LOC_BUILD = "build/" -LOC_DATA = "src/data/" -LOC_HEADERS = "src/headers/" -LOC_NOTE_MODELS = "src/note_models/" -LOC_MEDIA = "src/media/" - - -@dataclass -class InitRepo(Command): - crowdanki_folder: str - delimiter: str - - def execute(self): - self.setup_repo_structure() - - # Create the Deck Parts used - headers_ca, note_models_all_ca, notes_ca, media_group_ca = self.parts_from_crowdanki(self.crowdanki_folder) - - headers = headers_ca.execute().part - headers_name = LOC_HEADERS + "header1.yaml" - headers.dump_to_yaml(headers_name) - # TODO: desc file - - note_models = [m.part for m in note_models_all_ca.execute()] - - notes = notes_ca.execute().part - used_note_models_in_notes = notes.get_all_known_note_model_names() - - media_group_ca.execute() - - note_model_mappings = [NoteModelMapping.Representation([model.name for model in note_models])] - file_mappings: List[FileMapping.Representation] = [] - - csv_files = [] - - for model in note_models: - if model.name in used_note_models_in_notes: - csv_file_path = os.path.join(LOC_DATA, CsvFile.to_filename_csv(model.name, self.delimiter)) - column_headers = ["guid"] + model.field_names_lowercase + ["tags"] - CsvFile.create_file_with_headers(csv_file_path, column_headers, delimiter=self.delimiter) - - file_mappings.append(FileMapping.Representation( - file=csv_file_path, - note_model=model.name, - delimiter=self.delimiter - )) - - csv_files.append(csv_file_path) - - deck_path = os.path.join(LOC_BUILD, folder_name_from_full_path(self.crowdanki_folder)) - - # Generate the Source files that will be kept in the repo - save_note_models_to_folder = SaveNoteModelsToFolder.from_repr(SaveNoteModelsToFolder.Representation( - [m.name for m in note_models], LOC_NOTE_MODELS, True - )) - model_name_to_file_dict = save_note_models_to_folder.execute() - - save_media_to_folder = SaveMediaGroupsToFolder.from_repr(SaveMediaGroupsToFolder.Representation( - parts=[RECIPE_MEDIA], folder=LOC_MEDIA, recursive=True, clear_folder=True - )) - save_media_to_folder.execute() - - generate_csvs = CsvsGenerate.from_repr({ - 'notes': RECIPE_NOTES, - 'note_model_mappings': note_model_mappings, - 'file_mappings': file_mappings - }) - generate_csvs.execute() - - # Create Recipes - - # Anki to Source - headers_recipe, note_models_all_recipe, notes_recipe, media_group_recipe = self.parts_from_crowdanki(deck_path) - - build_part_tasks: List[BuildPartTask] = [ - headers_recipe, - notes_recipe, - note_models_all_recipe, - media_group_recipe, - ] - dp_builder = PartsBuilder(build_part_tasks) - - top_level_tasks: List[TopLevelBuildTask] = [dp_builder, save_media_to_folder, generate_csvs] - self.create_yaml_from_top_level(top_level_tasks, os.path.join(LOC_RECIPES, "anki_to_source")) - - # Source to Anki - note_models_from_yaml = [ - NoteModelsFromYamlPart.from_repr(NoteModelsFromYamlPart.Representation(name, file)) - for name, file in model_name_to_file_dict.items() - ] - - media_group_from_folder = MediaGroupFromFolder.from_repr(MediaGroupFromFolder.Representation( - part_id=RECIPE_MEDIA, source=LOC_MEDIA, recursive=True - )) - - headers_from_yaml = HeadersFromYamlPart.from_repr(HeadersFromYamlPart.Representation( - part_id=RECIPE_HEADERS, file=headers_name - )) - - notes_from_csv = NotesFromCsvs.from_repr({ - 'part_id': RECIPE_NOTES, - 'note_model_mappings': note_model_mappings, - 'file_mappings': file_mappings - }) - - build_part_tasks: List[BuildPartTask] = note_models_from_yaml + [ - headers_from_yaml, - notes_from_csv, - media_group_from_folder, - ] - dp_builder = PartsBuilder(build_part_tasks) - - generate_guids_in_csv = GenerateGuidsInCsvs.from_repr(GenerateGuidsInCsvs.Representation( - source=csv_files, columns=["guid"], delimiter=self.delimiter - )) - - generate_crowdanki = CrowdAnkiGenerate.from_repr(CrowdAnkiGenerate.Representation( - folder=deck_path, - notes=NotesToCrowdAnki.Representation( - part_id=RECIPE_NOTES - ).encode(), - headers=RECIPE_HEADERS, - media=MediaGroupToCrowdAnki.Representation( - parts=[RECIPE_MEDIA] - ).encode(), - note_models=NoteModelsToCrowdAnki.Representation( - parts=[NoteModelsToCrowdAnki.NoteModelListItem.Representation(name).encode() - for name, file in model_name_to_file_dict.items()] - ).encode() - )) - - top_level_tasks: List[TopLevelBuildTask] = [generate_guids_in_csv, dp_builder, generate_crowdanki] - source_to_anki_path = os.path.join(LOC_RECIPES, "source_to_anki.yaml") - self.create_yaml_from_top_level(top_level_tasks, source_to_anki_path) - - print(f"\nRepo Init complete. You should now run `brainbrew run {source_to_anki_path}`") - - @staticmethod - def create_yaml_from_top_level(top_tasks: List[TopLevelBuildTask], filepath: str): - tl_builder = TopLevelBuilder(top_tasks) - - encoded_top_level_tasks = tl_builder.encode() - # print(encoded_top_level_tasks) - - model_yaml_file_name = YamlObject.to_filename_yaml(filepath) - YamlObject.dump_to_yaml_file(model_yaml_file_name, encoded_top_level_tasks) - - @staticmethod - def parts_from_crowdanki(folder: str): - headers_ca = HeadersFromCrowdAnki.from_repr(HeadersFromCrowdAnki.Representation( - source=folder, part_id=RECIPE_HEADERS - )) - note_models_all_ca = NoteModelsAllFromCrowdAnki.from_repr(NoteModelsAllFromCrowdAnki.Representation( - source=folder - )) - notes_ca = NotesFromCrowdAnki.from_repr(NotesFromCrowdAnki.Representation( - source=folder, part_id=RECIPE_NOTES - )) - media_group_ca = MediaGroupFromCrowdAnki.from_repr(MediaGroupFromFolder.Representation( - source=folder, part_id=RECIPE_MEDIA - )) - return headers_ca, note_models_all_ca, notes_ca, media_group_ca - - @staticmethod - def setup_repo_structure(): - create_path_if_not_exists(LOC_RECIPES) - create_path_if_not_exists(LOC_BUILD) - create_path_if_not_exists(LOC_DATA) - create_path_if_not_exists(LOC_HEADERS) - create_path_if_not_exists(LOC_NOTE_MODELS) - create_path_if_not_exists(LOC_MEDIA) diff --git a/brain_brew/commands/run_recipe/__init__.py b/brain_brew/commands/run_recipe/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/commands/run_recipe/build_task.py b/brain_brew/commands/run_recipe/build_task.py deleted file mode 100644 index a95d128..0000000 --- a/brain_brew/commands/run_recipe/build_task.py +++ /dev/null @@ -1,45 +0,0 @@ -from abc import ABCMeta, abstractmethod -from typing import Dict, Type, Set, Optional - -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr - - -class BuildTask(YamlRepr, object, metaclass=ABCMeta): - execute_immediately: bool = False - accepts_list_of_self: bool = True - rep: Optional[RepresentationBase] - - def encode_rep(self) -> Dict[str, any]: - return self.rep.encode() - - @abstractmethod - def execute(self): - pass - - @classmethod - def task_regex(cls) -> str: - return cls.task_name() - - @classmethod - def get_all_task_regex(cls, subclasses: Set[Type['BuildTask']]) -> Dict[str, Type['BuildTask']]: - task_regex_matches: Dict[str, Type[BuildTask]] = {} - - for sc in subclasses: - if sc.task_regex in task_regex_matches: - raise KeyError(f"Multiple instances of task regex '{sc.task_regex}'") - elif sc.task_regex == "" or sc.task_regex is None: - raise KeyError(f"Unknown task regex in {sc.__name__}") - - task_regex_matches.setdefault(sc.task_regex(), sc) - - # logging.debug(f"Known build tasks: {known_build_tasks}") - return task_regex_matches - - -class TopLevelBuildTask(BuildTask, metaclass=ABCMeta): - pass - - -class BuildPartTask(BuildTask, metaclass=ABCMeta): - execute_immediately: bool = True diff --git a/brain_brew/commands/run_recipe/parts_builder.py b/brain_brew/commands/run_recipe/parts_builder.py deleted file mode 100644 index 0b3fa23..0000000 --- a/brain_brew/commands/run_recipe/parts_builder.py +++ /dev/null @@ -1,64 +0,0 @@ -from dataclasses import dataclass -from typing import Dict, Type, List, Set - -from brain_brew.build_tasks.crowd_anki.headers_from_crowdanki import HeadersFromCrowdAnki -from brain_brew.build_tasks.crowd_anki.media_group_from_crowd_anki import MediaGroupFromCrowdAnki -from brain_brew.build_tasks.crowd_anki.note_model_single_from_crowd_anki import NoteModelSingleFromCrowdAnki -from brain_brew.build_tasks.crowd_anki.note_models_all_from_crowd_anki import NoteModelsAllFromCrowdAnki -from brain_brew.build_tasks.crowd_anki.notes_from_crowd_anki import NotesFromCrowdAnki -from brain_brew.build_tasks.csvs.notes_from_csvs import NotesFromCsvs -from brain_brew.build_tasks.deck_parts.from_yaml_part import NotesFromYamlPart, MediaGroupFromYamlPart -from brain_brew.build_tasks.deck_parts.note_model_from_yaml_part import NoteModelsFromYamlPart -from brain_brew.build_tasks.deck_parts.headers_from_yaml_part import HeadersFromYamlPart -from brain_brew.build_tasks.deck_parts.media_group_from_folder import MediaGroupFromFolder -from brain_brew.build_tasks.deck_parts.note_model_from_html_parts import NoteModelFromHTMLParts -from brain_brew.commands.run_recipe.build_task import BuildTask, BuildPartTask, TopLevelBuildTask -from brain_brew.commands.run_recipe.recipe_builder import RecipeBuilder - - -@dataclass -class PartsBuilder(RecipeBuilder, TopLevelBuildTask): - tasks: List[BuildPartTask] - accepts_list_of_self: bool = False - - @classmethod - def task_name(cls) -> str: - return r'build_parts' - - @classmethod - def task_regex(cls) -> str: - return r'build_parts?' - - @classmethod - def known_task_dict(cls) -> Dict[str, Type[BuildTask]]: - return BuildPartTask.get_all_task_regex(cls.yamale_dependencies()) - - @classmethod - def from_repr(cls, data: List[dict]): - if not isinstance(data, list): - raise TypeError(f"PartsBuilder needs a list") - return cls.from_list(data) - - def encode(self) -> dict: - pass - - def encode_rep(self) -> list: - return self.tasks_to_encoded() - - @classmethod - def from_yaml_file(cls, filename: str): - pass - - @classmethod - def yamale_schema(cls) -> str: - return cls.build_yamale_root_node(cls.yamale_dependencies()) - - @classmethod - def yamale_dependencies(cls) -> Set[Type[BuildPartTask]]: - return { - NotesFromCsvs, - NotesFromYamlPart, HeadersFromYamlPart, NoteModelsFromYamlPart, MediaGroupFromYamlPart, - MediaGroupFromFolder, - NoteModelFromHTMLParts, NoteModelsFromYamlPart, NoteModelSingleFromCrowdAnki, NoteModelsAllFromCrowdAnki, - HeadersFromCrowdAnki, MediaGroupFromCrowdAnki, NotesFromCrowdAnki - } diff --git a/brain_brew/commands/run_recipe/recipe_builder.py b/brain_brew/commands/run_recipe/recipe_builder.py deleted file mode 100644 index a4d2f87..0000000 --- a/brain_brew/commands/run_recipe/recipe_builder.py +++ /dev/null @@ -1,82 +0,0 @@ -import re -from abc import ABCMeta, abstractmethod -from dataclasses import dataclass -from textwrap import indent -from typing import Dict, List, Type, Set - -from brain_brew.commands.run_recipe.build_task import BuildTask -from brain_brew.representation.yaml.yaml_object import YamlObject - - -@dataclass -class RecipeBuilder(YamlObject, metaclass=ABCMeta): - tasks: List[BuildTask] - - def tasks_to_encoded(self) -> list: - return [{task.task_name(): task.encode_rep()} for task in self.tasks] - - @classmethod - def from_list(cls, data: List[dict]): - tasks = cls.read_tasks(data) - return cls( - tasks=tasks - ) - - @classmethod - @abstractmethod - def known_task_dict(cls) -> Dict[str, Type[BuildTask]]: - pass - - @classmethod - def build_yamale_root_node(cls, subclasses: Set[Type['BuildTask']]) -> str: - task_list = [] - for c in sorted(subclasses, key=lambda x: x.task_name()): - task_command = f"any(include('{c.task_name()}'), list(include('{c.task_name()}')))"\ - if c.accepts_list_of_self else f"include('{c.task_name()}')" - task_list.append(f"map({task_command}, key=regex('{c.task_regex()}', ignore_case=True))") - - final_tasks: str = "list(\n" + indent(",\n".join(task_list), ' ') + "\n)\n" - - return final_tasks - - @classmethod - def read_tasks(cls, tasks: List[dict]) -> list: - task_regex_matches = cls.known_task_dict() - build_tasks = [] - - def find_matching_task(task_n): - for regex, task_to_run in task_regex_matches.items(): - if re.match(regex, task_n, re.RegexFlag.IGNORECASE): - return task_to_run - return None - - # Tasks - for task in tasks: - task_keys = list(task.keys()) - if len(task_keys) != 1: - raise KeyError(f"Task should only contain 1 entry, but contains {task_keys} instead. " - f"Missing list separator '-'?", task) - - task_name = task_keys[0] - task_arguments = task[task_keys[0]] - - matching_task = find_matching_task(task_name) - if matching_task is not None: - if matching_task.accepts_list_of_self and isinstance(task_arguments, list): - task_or_tasks = [matching_task.from_repr(t_arg) for t_arg in task_arguments] - else: - task_or_tasks = [matching_task.from_repr(task_arguments)] - - for inner_task in task_or_tasks: - build_tasks.append(inner_task) - if inner_task.execute_immediately: - inner_task.execute() - else: - raise KeyError(f"Unknown task '{task_name}'") # TODO: check this first on all and return all errors - - return build_tasks - - def execute(self): - for task in self.tasks: - if not task.execute_immediately: - task.execute() diff --git a/brain_brew/commands/run_recipe/run_recipe.py b/brain_brew/commands/run_recipe/run_recipe.py deleted file mode 100644 index 0ed18f7..0000000 --- a/brain_brew/commands/run_recipe/run_recipe.py +++ /dev/null @@ -1,18 +0,0 @@ -from dataclasses import dataclass -from brain_brew.interfaces.command import Command -from brain_brew.commands.run_recipe.top_level_builder import TopLevelBuilder -from brain_brew.configuration.yaml_verifier import YamlVerifier - - -@dataclass -class RunRecipe(Command): - recipe_file_name: str - verify_only: bool - - def execute(self): - # Parse Build Config File - YamlVerifier() - recipe = TopLevelBuilder.parse_and_read(self.recipe_file_name, self.verify_only) - - if not self.verify_only: - recipe.execute() diff --git a/brain_brew/commands/run_recipe/top_level_builder.py b/brain_brew/commands/run_recipe/top_level_builder.py deleted file mode 100644 index 4650f54..0000000 --- a/brain_brew/commands/run_recipe/top_level_builder.py +++ /dev/null @@ -1,87 +0,0 @@ -from textwrap import indent, dedent -from typing import Dict, Type, List, Set - -from brain_brew.build_tasks.crowd_anki.crowd_anki_generate import CrowdAnkiGenerate -from brain_brew.build_tasks.csvs.csvs_generate import CsvsGenerate -from brain_brew.build_tasks.csvs.generate_guids_in_csvs import GenerateGuidsInCsvs -from brain_brew.build_tasks.deck_parts.save_media_group_to_folder import SaveMediaGroupsToFolder -from brain_brew.build_tasks.deck_parts.save_note_models_to_folder import SaveNoteModelsToFolder -from brain_brew.commands.run_recipe.build_task import BuildTask, TopLevelBuildTask -from brain_brew.commands.run_recipe.parts_builder import PartsBuilder -from brain_brew.commands.run_recipe.recipe_builder import RecipeBuilder -from brain_brew.interfaces.yamale_verifyable import YamlRepr - - -class TopLevelBuilder(YamlRepr, RecipeBuilder): - @classmethod - def known_task_dict(cls) -> Dict[str, Type[BuildTask]]: - values = TopLevelBuildTask.get_all_task_regex(cls.yamale_dependencies()) - return values - - @classmethod - def build_yamale(cls): - separator = '\n---\n' - top_level = cls.yamale_dependencies() - - builder: List[str] = [cls.build_yamale_root_node(top_level), separator] - - def to_sorted_yamale_string(lines: Set[Type[BuildTask]]): - return [f'''{line.task_name()}:\n{indent(dedent(line.yamale_schema()), ' ')}''' - for line in sorted(lines, key=lambda x: x.task_name())] - - # Schema - builder += to_sorted_yamale_string(top_level) - - builder.append(separator) - - # Dependencies - def resolve_dependencies(deps: Set[Type[BuildTask]]) -> Set[Type[BuildTask]]: - result = set() - for d in deps: - result.add(d) - result = result.union(resolve_dependencies(d.yamale_dependencies())) - return result - - children = resolve_dependencies(cls.yamale_dependencies()) - builder += to_sorted_yamale_string({dep for dep in children if dep not in top_level}) - - return '\n'.join(builder) - - @classmethod - def parse_and_read(cls, filename, verify_only: bool) -> 'TopLevelBuilder': - recipe_data = cls.read_to_dict(filename) - - from brain_brew.configuration.yaml_verifier import YamlVerifier - YamlVerifier.get_instance().verify_recipe(filename) - - if verify_only: - return None - - return cls.from_list(recipe_data) - - @classmethod - def task_name(cls) -> str: - pass - - @classmethod - def yamale_schema(cls) -> str: - pass - - @classmethod - def from_repr(cls, data: dict): - pass - - def encode(self) -> list: - return self.tasks_to_encoded() - - @classmethod - def from_yaml_file(cls, filename: str): - pass - - @classmethod - def yamale_dependencies(cls) -> Set[Type[TopLevelBuildTask]]: - return { - PartsBuilder, - CrowdAnkiGenerate, CsvsGenerate, - GenerateGuidsInCsvs, SaveMediaGroupsToFolder, SaveNoteModelsToFolder - } diff --git a/brain_brew/configuration/__init__.py b/brain_brew/configuration/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/configuration/anki_field.py b/brain_brew/configuration/anki_field.py deleted file mode 100644 index 878e0ac..0000000 --- a/brain_brew/configuration/anki_field.py +++ /dev/null @@ -1,16 +0,0 @@ -class AnkiField: - name: str - anki_name: str - default_value: any - - def __init__(self, anki_name, name=None, default_value=None): - self.anki_name = anki_name - self.name = name if name is not None else anki_name - self.default_value = default_value - - def append_name_if_differs(self, dict_to_add_to: dict, value): - if value != self.default_value: - dict_to_add_to.setdefault(self.name, value) - - def does_differ(self, value): - return value != self.default_value diff --git a/brain_brew/configuration/file_manager.py b/brain_brew/configuration/file_manager.py deleted file mode 100644 index f99e31f..0000000 --- a/brain_brew/configuration/file_manager.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import Dict, Union - -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.representation.generic.source_file import SourceFile -from brain_brew.representation.yaml.yaml_object import YamlObject - - -class FileManager: - __instance = None - - known_files_dict: Dict[str, SourceFile] - known_parts: Dict[str, PartHolder[YamlObject]] - - def __init__(self): - if FileManager.__instance is None: - FileManager.__instance = self - else: - raise Exception("Multiple FileManagers created") - - self.known_files_dict = {} - self.known_parts = {} - - @staticmethod - def get_instance() -> 'FileManager': - return FileManager.__instance - - @staticmethod - def clear_instance(): - if FileManager.__instance: - FileManager.__instance = None - - # Source Files - - def register_file(self, full_path, file): - if full_path in self.known_files_dict: - raise FileExistsError(f"File already known to FileManager, cannot be registered twice: {full_path}") - self.known_files_dict.setdefault(full_path, file) - - def file_if_exists(self, file_location) -> Union[SourceFile, None]: - if file_location in self.known_files_dict.keys(): - return self.known_files_dict[file_location] - return None - - # Deck Parts - - def register_part(self, dp: PartHolder) -> PartHolder: - if dp.part_id in self.known_parts: - raise KeyError(f"Cannot use same name '{dp.part_id}' for multiple Deck Parts") - self.known_parts.setdefault(dp.part_id, dp) - return dp - - def get_part_if_exists(self, dp_name) -> Union[PartHolder[YamlObject], None]: - return self.known_parts.get(dp_name) - - def get_part(self, name: str): - if name not in self.known_parts: - raise KeyError(f"Cannot find Deck Part '{name}'") - return self.known_parts[name] diff --git a/brain_brew/configuration/part_holder.py b/brain_brew/configuration/part_holder.py deleted file mode 100644 index 5140849..0000000 --- a/brain_brew/configuration/part_holder.py +++ /dev/null @@ -1,45 +0,0 @@ -import logging -from dataclasses import dataclass -from typing import Optional, TypeVar, Generic - -T = TypeVar('T') - - -@dataclass -class PartHolder(Generic[T]): - part_id: str - save_to_file: Optional[str] - part: T - - file_manager = None - - @classmethod - def get_file_manager(cls): - if not cls.file_manager: - from brain_brew.configuration.file_manager import FileManager - cls.file_manager = FileManager.get_instance() - return cls.file_manager - - @classmethod - def from_file_manager(cls, part_id: str) -> T: - return cls.get_file_manager().get_part(part_id) - - @classmethod - def override_or_create(cls, part_id: str, save_to_file: Optional[str], part: T): - fm = cls.get_file_manager() - - dp = fm.get_part_if_exists(part_id) - if dp is None: - dp = fm.register_part(PartHolder(part_id, save_to_file, part)) - else: - logging.warning(f"Overwriting existing Deck Part '{part_id}'") - dp.part = part - dp.save_to_file = save_to_file - - dp.write_to_file() - - return dp - - def write_to_file(self): - if self.save_to_file is not None: - self.part.dump_to_yaml(self.save_to_file) diff --git a/brain_brew/configuration/representation_base.py b/brain_brew/configuration/representation_base.py deleted file mode 100644 index 08453c9..0000000 --- a/brain_brew/configuration/representation_base.py +++ /dev/null @@ -1,28 +0,0 @@ -import inspect -import logging - - -class RepresentationBase: - @classmethod - def from_dict(cls, data: dict): - expected_values = { - k: v for k, v in data.items() - if k in inspect.signature(cls).parameters - } - - if len(expected_values) != len(data): - logging.warning(f"Unexpected values found when creating '{cls.__name__}': " - f"{[k for k, v in data.items() if k not in list(expected_values.keys())]}" - "\n!!! Please report this error if it seems strange") - - return cls(**expected_values) - - def encode(self): - return {key: value for key, value in self.__dict__.items() if self.encode_filter(key, value)} - - def encode_filter(self, key, value): - if value is None: - return False - if not value: - return False - return True diff --git a/brain_brew/configuration/yaml_verifier.py b/brain_brew/configuration/yaml_verifier.py deleted file mode 100644 index 89f1bbf..0000000 --- a/brain_brew/configuration/yaml_verifier.py +++ /dev/null @@ -1,40 +0,0 @@ -import logging -import os - -import yamale -from yamale import YamaleError -from yamale.schema import Schema -from yamale.validators import DefaultValidators - -validators = DefaultValidators.copy() - - -class YamlVerifier: - __instance = None - recipe_schema: Schema - - def __init__(self): - if YamlVerifier.__instance is None: - YamlVerifier.__instance = self - else: - raise Exception("Multiple YamlVerifiers created") - - path = os.path.join(os.path.dirname(__file__), "../schemas/recipe.yaml") - self.recipe_schema = yamale.make_schema(path, parser='ruamel', validators=validators) - - @staticmethod - def get_instance() -> 'YamlVerifier': - return YamlVerifier.__instance - - def verify_recipe(self, filename): - data = yamale.make_data(filename) - try: - yamale.validate(self.recipe_schema, data) - except YamaleError as e: - print('Validation failed!\n') - for result in e.results: - print("Error validating data '%s' with '%s'\n\t" % (result.data, result.schema)) - for error in result.errors: - print('\t%s' % error) - exit(1) - logging.info(f"Builder file {filename} is ✔ good") diff --git a/brain_brew/front_matter.py b/brain_brew/front_matter.py deleted file mode 100644 index e7b0e27..0000000 --- a/brain_brew/front_matter.py +++ /dev/null @@ -1,2 +0,0 @@ -def latest_version_number(): - return "0.3.11" diff --git a/brain_brew/interfaces/__init__.py b/brain_brew/interfaces/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/interfaces/command.py b/brain_brew/interfaces/command.py deleted file mode 100644 index 3c25a23..0000000 --- a/brain_brew/interfaces/command.py +++ /dev/null @@ -1,7 +0,0 @@ -from abc import ABC, abstractmethod - - -class Command(ABC): - @abstractmethod - def execute(self): - pass diff --git a/brain_brew/interfaces/media_container.py b/brain_brew/interfaces/media_container.py deleted file mode 100644 index 11c7acc..0000000 --- a/brain_brew/interfaces/media_container.py +++ /dev/null @@ -1,8 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Set - - -class MediaContainer(ABC): - @abstractmethod - def get_all_media_references(self) -> Set[str]: - pass diff --git a/brain_brew/interfaces/yamale_verifyable.py b/brain_brew/interfaces/yamale_verifyable.py deleted file mode 100644 index fdf99eb..0000000 --- a/brain_brew/interfaces/yamale_verifyable.py +++ /dev/null @@ -1,22 +0,0 @@ -from abc import ABC, abstractmethod - - -class YamlRepr(ABC): - @classmethod - @abstractmethod - def task_name(cls) -> str: - pass - - @classmethod - @abstractmethod - def yamale_schema(cls) -> str: - pass - - @classmethod - def yamale_dependencies(cls) -> set: - return set() - - @classmethod - @abstractmethod - def from_repr(cls, data: dict): - pass diff --git a/brain_brew/main.py b/brain_brew/main.py deleted file mode 100644 index 5a36fd8..0000000 --- a/brain_brew/main.py +++ /dev/null @@ -1,23 +0,0 @@ -import logging - -from brain_brew.commands.argument_reader import BBArgumentReader -# sys.path.append(os.path.join(os.path.dirname(__file__), "dist")) -# sys.path.append(os.path.dirname(__file__)) -from brain_brew.configuration.file_manager import FileManager - - -def main(): - logging.basicConfig(level=logging.DEBUG) - - # Read in Arguments - argument_reader = BBArgumentReader() - command = argument_reader.get_parsed() - - # Create Singleton FileManager - FileManager() - - command.execute() - - -if __name__ == "__main__": - main() diff --git a/brain_brew/representation/__init__.py b/brain_brew/representation/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/representation/generic/__init__.py b/brain_brew/representation/generic/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/representation/generic/csv_file.py b/brain_brew/representation/generic/csv_file.py deleted file mode 100644 index 1f0633d..0000000 --- a/brain_brew/representation/generic/csv_file.py +++ /dev/null @@ -1,116 +0,0 @@ -import csv -from pathlib import Path -import re -import logging -from enum import Enum -from typing import List, Optional - -from brain_brew.representation.generic.source_file import SourceFile -from brain_brew.utils import create_path_if_not_exists, list_of_str_to_lowercase, sort_dict - -_encoding = "utf-8" - - -class CsvKeys(Enum): - GUID = "guid" - TAGS = "tags" - - -class CsvFile(SourceFile): - file_location: str = "" - _data: List[dict] = [] - column_headers: list = [] - delimiter: str = ',' - - def __init__(self, file, delimiter=None): - self.file_location = file - self.set_delimiter(delimiter) - - def set_delimiter(self, delimiter: str): - if delimiter: - self.delimiter = delimiter - elif re.match(r'.*\.tsv', self.file_location, re.RegexFlag.IGNORECASE): - self.delimiter = '\t' - - @classmethod - def from_file_loc(cls, file_loc) -> 'CsvFile': - return cls(file_loc) - - def read_file(self, create_if_not_exists: Optional[bool] = True): - self._data = [] - - if create_if_not_exists: - create_path_if_not_exists(self.file_location) - Path(self.file_location).touch() - - with open(self.file_location, mode='r', newline='', encoding=_encoding) as csv_file: - csv_reader = csv.DictReader(csv_file, delimiter=self.delimiter) - - self.column_headers = list_of_str_to_lowercase(csv_reader.fieldnames) - - for row in csv_reader: - self._data.append({key.lower(): row[key] for key in row}) - - def write_file(self): - logging.info(f"Writing to Csv '{self.file_location}'") - with open(self.file_location, mode='w+', newline='', encoding=_encoding) as csv_file: - csv_writer = csv.DictWriter(csv_file, fieldnames=self.column_headers, lineterminator='\n', delimiter=self.delimiter) - - csv_writer.writeheader() - - for row in self._data: - csv_writer.writerow(row) - - def set_data(self, data_override): - self._data = data_override - self.column_headers = list(data_override[0].keys()) if data_override else [] - - def set_data_from_superset(self, superset: List[dict], column_header_override=None): - if column_header_override: - self.column_headers = column_header_override - - data_to_set: List[dict] = [] - for row in superset: - if not all(column in row for column in self.column_headers): - continue - new_row = {} - for column in self.column_headers: - new_row[column] = row[column] - data_to_set.append(new_row) - - self._data = data_to_set - - - def get_data(self, deep_copy=False) -> List[dict]: - return self.get_deep_copy(self._data) if deep_copy else self._data - - @staticmethod - def to_filename_csv(filename: str, delimiter: str = None) -> str: - if not re.match(r'.*\.(csv|tsv)', filename, re.RegexFlag.IGNORECASE): - if delimiter == '\t': - return filename + '.tsv' - else: - return filename + ".csv" - return filename - - @classmethod - def formatted_file_location(cls, location): - return cls.to_filename_csv(location) - - def sort_data(self, sort_by_keys, reverse_sort, case_insensitive_sort): - self._data = sort_dict(self._data, sort_by_keys, reverse_sort, case_insensitive_sort) - - @classmethod - def create_file_with_headers(cls, filepath: str, headers: List[str], delimiter: str = None): - with open(filepath, mode='w+', newline='', encoding=_encoding) as csv_file: - csv_writer = csv.DictWriter(csv_file, fieldnames=headers, lineterminator='\n', delimiter=delimiter or ",") - - csv_writer.writeheader() - - @staticmethod - def delimiter_matches_file_type(delimiter: str, filename: str) -> bool: - if delimiter == '\t' and re.match(r'.*\.tsv', filename, re.RegexFlag.IGNORECASE): - return True - if delimiter == ',' and re.match(r'.*\.csv', filename, re.RegexFlag.IGNORECASE): - return True - return False diff --git a/brain_brew/representation/generic/html_file.py b/brain_brew/representation/generic/html_file.py deleted file mode 100644 index 7db12e3..0000000 --- a/brain_brew/representation/generic/html_file.py +++ /dev/null @@ -1,38 +0,0 @@ -from dataclasses import dataclass - -from brain_brew.representation.generic.source_file import SourceFile - -_encoding = "utf-8" - -@dataclass -class HTMLFile(SourceFile): - file_location: str - _data: str - - def __init__(self, file): - self.file_location = file - self.read_file() - - @classmethod - def from_file_loc(cls, file_loc) -> 'HTMLFile': - return cls(file_loc) - - def read_file(self): - r = open(self.file_location, 'r', encoding=_encoding) - self._data = r.read() - - def get_data(self, deep_copy=False) -> str: - return self.get_deep_copy(self._data) if deep_copy else self._data - - @staticmethod - def write_file(file_location, data): - with open(file_location, "w+", encoding=_encoding) as file: - file.write(data) - - @staticmethod - def to_filename_html(filename: str) -> str: - return filename + ".html" if not filename.endswith(".html") else filename - - @classmethod - def formatted_file_location(cls, location): - return cls.to_filename_html(location) diff --git a/brain_brew/representation/generic/media_file.py b/brain_brew/representation/generic/media_file.py deleted file mode 100644 index d1e71e9..0000000 --- a/brain_brew/representation/generic/media_file.py +++ /dev/null @@ -1,31 +0,0 @@ -import os -import shutil -from dataclasses import dataclass, field - -from brain_brew.representation.generic.source_file import SourceFile -from brain_brew.utils import filename_from_full_path - - -@dataclass -class MediaFile(SourceFile): - file_path: str - filename: str = field(init=False) - - def __post_init__(self): - self.filename = filename_from_full_path(self.file_path) - - @classmethod - def from_file_loc(cls, file_loc) -> 'MediaFile': - return cls(file_loc) - - def __repr__(self): - return f"MediaFile({self.file_path})" - - def __hash__(self): - return hash(self.__repr__()) - - def copy_self_to_target(self, target: str): - shutil.copy2(self.file_path, target) - - def delete_self(self): - os.remove(self.file_path) diff --git a/brain_brew/representation/generic/source_file.py b/brain_brew/representation/generic/source_file.py deleted file mode 100644 index 96231eb..0000000 --- a/brain_brew/representation/generic/source_file.py +++ /dev/null @@ -1,41 +0,0 @@ -import copy -from pathlib import Path - - -class SourceFile(object): - @classmethod - def from_file_loc(cls, file_loc) -> 'SourceFile': - pass - - @classmethod - def is_file(cls, filename: str): - return Path(filename).is_file() - - @classmethod - def is_dir(cls, folder_name: str): - return Path(folder_name).is_dir() - - @classmethod - def get_deep_copy(cls, data): - return copy.deepcopy(data) - - @classmethod - def create_or_get(cls, location): - from brain_brew.configuration.file_manager import FileManager - _file_manager = FileManager.get_instance() - formatted_location = cls.formatted_file_location(location) - file = _file_manager.file_if_exists(formatted_location) - - if file is not None: - return file - - # if not cls.is_file(location) and not cls.is_dir(location): - # raise FileNotFoundError(f"No file or folder '{location}' exists") - - file = cls.from_file_loc(location) - _file_manager.register_file(formatted_location, file) - return file - - @classmethod - def formatted_file_location(cls, location): - return location diff --git a/brain_brew/representation/json/__init__.py b/brain_brew/representation/json/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/representation/json/crowd_anki_export.py b/brain_brew/representation/json/crowd_anki_export.py deleted file mode 100644 index 5652049..0000000 --- a/brain_brew/representation/json/crowd_anki_export.py +++ /dev/null @@ -1,63 +0,0 @@ -import glob -import logging -from typing import List - -from brain_brew.representation.generic.source_file import SourceFile -from brain_brew.representation.json.json_file import JsonFile -from brain_brew.representation.json.wrappers_for_crowd_anki import CrowdAnkiJsonWrapper -from brain_brew.representation.yaml.note_model import NoteModel -from brain_brew.utils import create_path_if_not_exists - - -class CrowdAnkiExport(SourceFile): - folder_location: str - json_file_location: str - # import_config: CrowdAnkiImportConfig # TODO: Make this - json_data: CrowdAnkiJsonWrapper - note_models: List[NoteModel] - - media_loc: str - - def __init__(self, folder_location): - self.folder_location = folder_location - if self.folder_location[-1] != "/": - self.folder_location = self.folder_location + "/" - - create_path_if_not_exists(self.folder_location) - - self.json_file_location = self.find_json_file_in_folder() - self._read_json_file() - - self.media_loc = self.folder_location + "media/" - - if not self.is_dir(self.media_loc): - create_path_if_not_exists(self.media_loc) - return - - @classmethod - def from_file_loc(cls, file_loc) -> 'CrowdAnkiExport': - return cls(file_loc) - - def find_json_file_in_folder(self): - files = glob.glob(f"{glob.escape(self.folder_location)}*.json") - - if len(files) == 1: - return files[0] - elif not files: - file_loc = self.folder_location + "deck.json" - logging.warning(f"Creating missing json file '{file_loc}'") - return file_loc - else: - logging.error(f"Multiple json files found in '{self.folder_location}': {files}") - raise FileExistsError() - - def write_to_files(self, json_data): # import_config_data - JsonFile.write_file(self.json_file_location, json_data) - - def _read_json_file(self): - if SourceFile.is_file(self.json_file_location): - self.json_data = CrowdAnkiJsonWrapper(JsonFile.read_file(self.json_file_location)) - self.note_models = list(map(NoteModel.from_crowdanki, self.json_data.note_models)) - else: - self.write_to_files({}) - self.json_data = CrowdAnkiJsonWrapper({}) diff --git a/brain_brew/representation/json/json_file.py b/brain_brew/representation/json/json_file.py deleted file mode 100644 index c1ccfba..0000000 --- a/brain_brew/representation/json/json_file.py +++ /dev/null @@ -1,25 +0,0 @@ -import json - -_encoding = "utf-8" - - -class JsonFile: - @staticmethod - def pretty_print(data): - return json.dumps(data, indent=4) - - @staticmethod - def to_filename_json(filename: str): - if filename[-5:] != ".json": - return filename + ".json" - return filename - - @staticmethod - def read_file(file_location): - with open(JsonFile.to_filename_json(file_location), "r", encoding=_encoding) as read_file: - return json.load(read_file) - - @staticmethod - def write_file(file_location, data): - with open(JsonFile.to_filename_json(file_location), "w+", encoding=_encoding) as write_file: - json.dump(data, write_file, indent=4, sort_keys=False, ensure_ascii=False) diff --git a/brain_brew/representation/json/wrappers_for_crowd_anki.py b/brain_brew/representation/json/wrappers_for_crowd_anki.py deleted file mode 100644 index 26c6e5b..0000000 --- a/brain_brew/representation/json/wrappers_for_crowd_anki.py +++ /dev/null @@ -1,117 +0,0 @@ -from typing import List - - -CA_NOTE_MODELS = "note_models" -CA_NOTES = "notes" -CA_MEDIA_FILES = "media_files" -CA_CHILDREN = "children" -CA_TYPE = "__type__" -CA_NAME = "name" -CA_DESCRIPTION = "desc" -CA_UUID = "crowdanki_uuid" - -NOTE_MODEL = "note_model_uuid" -FLAGS = "flags" -GUID = "guid" -TAGS = "tags" -FIELDS = "fields" - - -class CrowdAnkiJsonWrapper: - data: dict - - def __init__(self, data: dict = None): - self.data = data - - @property - def children(self) -> list: - return self.data.get(CA_CHILDREN, []) - - @property - def note_models(self) -> list: - return CrowdAnkiJsonWrapper.get_from_self_and_children_recursively(self.data, [], CA_NOTE_MODELS) - - - @note_models.setter - def note_models(self, value: list): - self.data[CA_NOTE_MODELS] = value - - @property - def notes(self) -> list: - return CrowdAnkiJsonWrapper.get_from_self_and_children_recursively(self.data, [], CA_NOTES) - - @notes.setter - def notes(self, value: list): - self.data[CA_NOTES] = value - - @property - def media_files(self) -> list: - return CrowdAnkiJsonWrapper.get_from_self_and_children_recursively(self.data, [], CA_MEDIA_FILES) - - @media_files.setter - def media_files(self, value: list): - self.data[CA_MEDIA_FILES] = value - - @property - def name(self) -> list: - return self.data.get(CA_NAME, []) - - @name.setter - def name(self, value: list): - self.data[CA_NAME] = value - - @staticmethod - def get_from_self_and_children_recursively(data: dict, running_data: list, key_name: str): - running_data += data.get(key_name, []) - children = data.get(CA_CHILDREN, []) - if isinstance(children, list): - for child in children: - running_data = CrowdAnkiJsonWrapper.get_from_self_and_children_recursively(child, running_data, key_name) - return running_data - - -class CrowdAnkiNoteWrapper: - data: dict - - def __init__(self, data: dict = None): - self.data = data - - @property - def note_model(self) -> str: - return self.data.get(NOTE_MODEL) - - @note_model.setter - def note_model(self, value: str): - self.data[NOTE_MODEL] = value - - @property - def flags(self) -> int: - return self.data.get(FLAGS) - - @flags.setter - def flags(self, value: int): - self.data[FLAGS] = value - - @property - def guid(self) -> str: - return self.data.get(GUID) - - @guid.setter - def guid(self, value: str): - self.data[GUID] = value - - @property - def tags(self) -> list: - return self.data.get(TAGS, []) - - @tags.setter - def tags(self, value: list): - self.data[TAGS] = value - - @property - def fields(self) -> List[str]: - return self.data.get(FIELDS, []) - - @fields.setter - def fields(self, value: List[str]): - self.data[FIELDS] = value diff --git a/brain_brew/representation/yaml/__init__.py b/brain_brew/representation/yaml/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/representation/yaml/headers.py b/brain_brew/representation/yaml/headers.py deleted file mode 100644 index dd27e7b..0000000 --- a/brain_brew/representation/yaml/headers.py +++ /dev/null @@ -1,44 +0,0 @@ -from dataclasses import dataclass - -from brain_brew.representation.json.wrappers_for_crowd_anki import CA_NAME, CA_DESCRIPTION, CA_UUID -from brain_brew.representation.yaml.yaml_object import YamlObject - - -@dataclass -class Headers(YamlObject): - data: dict - - @classmethod - def from_yaml_file(cls, filename: str): - return cls(data=cls.read_to_dict(filename)) - - def encode(self) -> dict: - return self.data - - @property - def name(self) -> str: - return self.data[CA_NAME] - - @name.setter - def name(self, desc: str): - self.data[CA_NAME] = desc - - @property - def description(self) -> str: - return self.data.get(CA_DESCRIPTION, "") - - @description.setter - def description(self, desc: str): - self.data[CA_DESCRIPTION] = desc - - @property - def crowdanki_uuid(self) -> str: - return self.data.get(CA_UUID, "") - - @crowdanki_uuid.setter - def crowdanki_uuid(self, desc: str): - self.data[CA_UUID] = desc - - @property - def data_without_name(self) -> dict: - return {k: v for k, v in sorted(self.data.items()) if k != CA_NAME} diff --git a/brain_brew/representation/yaml/media_group.py b/brain_brew/representation/yaml/media_group.py deleted file mode 100644 index eb26334..0000000 --- a/brain_brew/representation/yaml/media_group.py +++ /dev/null @@ -1,57 +0,0 @@ -from dataclasses import dataclass -from typing import Set, Dict, List, Tuple - -from brain_brew.representation.generic.media_file import MediaFile -from brain_brew.representation.yaml.yaml_object import YamlObject -from brain_brew.utils import find_all_files_in_directory - - -@dataclass -class MediaGroup(YamlObject): - media_files: Dict[str, MediaFile] - - def encode(self) -> list: - return list(m.file_path for m in self.media_files.values()) # TODO: Use relative path for directory? - - @classmethod - def from_yaml_file(cls, filename: str) -> 'MediaGroup': - return cls(media_files=cls.from_full_path_list(cls.read_to_dict(filename))) - - @classmethod - def from_directory(cls, directory: str, recursive: bool) -> 'MediaGroup': - return cls(media_files=cls.from_full_path_list(find_all_files_in_directory(directory, recursive=recursive))) - - @classmethod - def from_many(cls, groups: List['MediaGroup']) -> 'MediaGroup': - files = list(set(file.file_path for group in groups for file in group.media_files.values())) - return cls(media_files=cls.from_full_path_list(files)) - - @staticmethod - def from_full_path_list(known_files: list): - files: Dict[str, MediaFile] = dict() - - for full_path in known_files: - file = MediaFile.create_or_get(full_path) - if file.filename not in files.keys(): - files[file.filename] = file - else: - raise NameError(f"Duplicate files with same filename '{file.filename}' in group") - - return files - - def remove_by_filename(self, filename: str): - self.media_files.pop(filename, None) - - def filter_by_filenames(self, filenames: List[str], should_match: bool): - for media_filename in self.media_files.keys(): - is_match = media_filename in filenames - if is_match != should_match: - self.remove_by_filename(media_filename) - # TODO: Find all missing files - - def compare(self, other: 'MediaGroup') -> Tuple[Set[str], Set[str], Set[str]]: - - self_set = set(self.media_files) - other_set = set(other.media_files) - - return self_set.intersection(other_set), self_set - other_set, other_set - self_set diff --git a/brain_brew/representation/yaml/note_model.py b/brain_brew/representation/yaml/note_model.py deleted file mode 100644 index 74c5ee9..0000000 --- a/brain_brew/representation/yaml/note_model.py +++ /dev/null @@ -1,270 +0,0 @@ -from collections import OrderedDict -from dataclasses import dataclass, field -from typing import List, Union, Dict, Set - -from brain_brew.configuration.anki_field import AnkiField -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.media_container import MediaContainer -from brain_brew.interfaces.yamale_verifyable import YamlRepr -from brain_brew.representation.generic.html_file import HTMLFile -from brain_brew.representation.yaml.note_model_field import Field -from brain_brew.representation.yaml.note_model_template import Template -from brain_brew.representation.yaml.yaml_object import YamlObject -from brain_brew.utils import list_of_str_to_lowercase - -# CrowdAnki -CROWDANKI_ID = AnkiField("crowdanki_uuid", "id") -CROWDANKI_TYPE = AnkiField("__type__", default_value="NoteModel") - -# Shared -NAME = AnkiField("name") -ORDINAL = AnkiField("ord", "ordinal") - -# Note Model -CSS = AnkiField("css") -LATEX_PRE = AnkiField("latexPre", "latex_pre", - default_value="\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage{" - "amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{" - "document}\n") -LATEX_POST = AnkiField("latexPost", "latex_post", default_value="\\end{document}") -LATEX_SVG = AnkiField("latexsvg", "latex_svg", default_value=False) -REQUIRED_FIELDS_PER_TEMPLATE = AnkiField("req", "required_fields_per_template", default_value=[]) -FIELDS = AnkiField("flds", "fields") -TEMPLATES = AnkiField("tmpls", "templates") -TAGS = AnkiField("tags", default_value=[]) -SORT_FIELD_NUM = AnkiField("sortf", "sort_field_num", default_value=0) -IS_CLOZE = AnkiField("type", "is_cloze", default_value=False) -VERSION = AnkiField("vers", "version", default_value=[]) - -# Field -FONT = AnkiField("font", default_value="Liberation Sans") -MEDIA = AnkiField("media", default_value=[]) -IS_RIGHT_TO_LEFT = AnkiField("rtl", "is_right_to_left", default_value=False) -FONT_SIZE = AnkiField("size", "font_size", default_value=20) -IS_STICKY = AnkiField("sticky", "is_sticky", default_value=False) - -# Template -QUESTION_FORMAT = AnkiField("qfmt", "question_format") -ANSWER_FORMAT = AnkiField("afmt", "answer_format") -BROWSER_ANSWER_FORMAT = AnkiField("bafmt", "browser_answer_format", default_value="") -BROWSER_QUESTION_FORMAT = AnkiField("bqfmt", "browser_question_format", default_value="") -DECK_OVERRIDE_ID = AnkiField("did", "deck_override_id", default_value=None) - - -CSS_FILE = AnkiField("css_file") - - -@dataclass -class NoteModel(YamlObject, YamlRepr, MediaContainer): - @classmethod - def task_name(cls) -> str: - return r"note_model_from_yaml_repr_inner" - - @classmethod - def yamale_schema(cls) -> str: - return f"""\ - {NAME.name}: str() - {CROWDANKI_ID.name}: str() - {CSS_FILE.name}: str() - {FIELDS.name}: include({Field.task_name()}, required=False) - {TEMPLATES.name}: include({Template.task_name()}, required=False) - {REQUIRED_FIELDS_PER_TEMPLATE.name}: list(required=False) - {LATEX_POST.name}: str(required=False) - {LATEX_PRE.name}: str(required=False) - {SORT_FIELD_NUM.name}: int(required=False) - {IS_CLOZE.name}: bool(required=False) - {CROWDANKI_TYPE.name}: str(required=False) - {TAGS.name}: str(required=False) - {VERSION.name}: list(required=False) - """ - - @classmethod - def yamale_dependencies(cls) -> set: - return {Field, Template} - - @dataclass - class Representation(RepresentationBase): - name: str - id: str - css_file: str - fields: List[dict] - templates: List[dict] - - required_fields_per_template: List[list] = field(default_factory=lambda: []) - latex_post: str = field(default=LATEX_POST.default_value) - latex_pre: str = field(default=LATEX_PRE.default_value) - latex_svg: bool = field(default=LATEX_SVG.default_value) - sort_field_num: int = field(default=SORT_FIELD_NUM.default_value) - is_cloze: bool = field(default=IS_CLOZE.default_value) - crowdanki_type: str = field(default=CROWDANKI_TYPE.default_value) # Should always be "NoteModel" - tags: List[str] = field(default_factory=lambda: TAGS.default_value) # Tags of the last added note - version: list = field(default_factory=lambda: VERSION.default_value) # Legacy version number. Deprecated in Anki - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - return cls( - rep=rep, - fields=[Field.from_repr(f) for f in rep.fields], - templates=[Template.from_html_files(t) for t in rep.templates], - css=HTMLFile.create_or_get(rep.css_file).get_data(deep_copy=False), - - name=rep.name, is_cloze=bool(rep.is_cloze), - latex_pre=rep.latex_pre, latex_post=rep.latex_post, latex_svg=rep.latex_svg, - required_fields_per_template=rep.required_fields_per_template, - tags=rep.tags, sort_field_num=rep.sort_field_num, version=rep.version, - id=rep.id, crowdanki_type=rep.crowdanki_type - ) - - @dataclass - class CrowdAnki(RepresentationBase): - name: str - crowdanki_uuid: str - css: str - flds: List[dict] - tmpls: List[dict] - req: List[list] = field(default_factory=lambda: REQUIRED_FIELDS_PER_TEMPLATE.default_value) - latexPre: str = field(default=LATEX_PRE.default_value) - latexPost: str = field(default=LATEX_POST.default_value) - latexsvg: bool = field(default=LATEX_SVG.default_value) # TODO: Fix lowercase here in CrowdAnki - __type__: str = field(default=CROWDANKI_TYPE.default_value) - tags: List[str] = field(default_factory=lambda: TAGS.default_value) - sortf: int = field(default=SORT_FIELD_NUM.default_value) - type: int = field(default=0) # Is_Cloze Manually set to 0 - vers: list = field(default_factory=lambda: VERSION.default_value) - - rep: Union[Representation, CrowdAnki] - - name: str - id: str - css: str - fields: List[Field] - templates: List[Template] - - required_fields_per_template: List[list] = field(default_factory=lambda: REQUIRED_FIELDS_PER_TEMPLATE.default_value) - latex_post: str = field(default=LATEX_POST.default_value) - latex_pre: str = field(default=LATEX_PRE.default_value) - latex_svg: bool = field(default=LATEX_SVG.default_value) - sort_field_num: int = field(default=SORT_FIELD_NUM.default_value) - is_cloze: bool = field(default=IS_CLOZE.default_value) - crowdanki_type: str = field(default=CROWDANKI_TYPE.default_value) # Should always be "NoteModel" - tags: List[str] = field(default_factory=lambda: TAGS.default_value) # Tags of the last added note - version: list = field(default_factory=lambda: VERSION.default_value) # Legacy version number. Deprecated in Anki - - @classmethod - def from_yaml_file(cls, filename: str): - data = cls.read_to_dict(filename) - return cls.from_repr(data) - - @classmethod - def from_crowdanki(cls, data: Union[CrowdAnki, dict]): # TODO: field_whitelist, note_model_whitelist - ca: cls.CrowdAnki = data if isinstance(data, cls.CrowdAnki) else cls.CrowdAnki.from_dict(data) - return cls( - rep=ca, - fields=[Field.from_crowd_anki(f) for f in ca.flds], - templates=[Template.from_crowdanki(t) for t in ca.tmpls], - is_cloze=bool(ca.type), - name=ca.name, css=ca.css, latex_pre=ca.latexPre, latex_post=ca.latexPost, latex_svg=ca.latexsvg, - required_fields_per_template=ca.req, tags=ca.tags, sort_field_num=ca.sortf, version=ca.vers, - id=ca.crowdanki_uuid, crowdanki_type=ca.__type__ - ) - - def encode_as_crowdanki(self) -> dict: - data_dict = { - NAME.anki_name: self.name, - CROWDANKI_ID.anki_name: self.id, - CSS.anki_name: self.css, - REQUIRED_FIELDS_PER_TEMPLATE.anki_name: self.required_fields_per_template, - LATEX_PRE.anki_name: self.latex_pre, - LATEX_POST.anki_name: self.latex_post, - LATEX_SVG.anki_name: self.latex_svg, - SORT_FIELD_NUM.anki_name: self.sort_field_num, - CROWDANKI_TYPE.anki_name: self.crowdanki_type, - TAGS.anki_name: self.tags, - VERSION.anki_name: self.version, - IS_CLOZE.anki_name: 1 if self.is_cloze else 0 - } - - data_dict.setdefault(FIELDS.anki_name, [f.encode_as_crowdanki(num) for num, f in enumerate(self.fields)]) - data_dict.setdefault(TEMPLATES.anki_name, [t.encode_as_crowdanki(num) for num, t in enumerate(self.templates)]) - - return OrderedDict(sorted(data_dict.items())) - - def encode_as_part_with_empty_file_references(self) -> dict: - data_dict: Dict[str, Union[str, list]] = { - NAME.name: self.name, - CROWDANKI_ID.name: self.id, - CSS_FILE.name: "" - } - - SORT_FIELD_NUM.append_name_if_differs(data_dict, self.sort_field_num) - IS_CLOZE.append_name_if_differs(data_dict, self.is_cloze) - LATEX_PRE.append_name_if_differs(data_dict, self.latex_pre) - LATEX_POST.append_name_if_differs(data_dict, self.latex_post) - LATEX_SVG.append_name_if_differs(data_dict, self.latex_svg) - - data_dict.setdefault(FIELDS.name, [f.encode_as_part() for f in self.fields]) - data_dict.setdefault(TEMPLATES.name, [t.encode_as_part() for t in self.templates]) - - # Useless - TAGS.append_name_if_differs(data_dict, self.tags) - VERSION.append_name_if_differs(data_dict, self.version) - CROWDANKI_TYPE.append_name_if_differs(data_dict, self.crowdanki_type) - REQUIRED_FIELDS_PER_TEMPLATE.append_name_if_differs(data_dict, self.required_fields_per_template) - - return data_dict - - def encode(self) -> dict: - data_dict: Dict[str, Union[str, list]] = { - NAME.name: self.name, - CROWDANKI_ID.name: self.id, - CSS.name: self.css - } - - SORT_FIELD_NUM.append_name_if_differs(data_dict, self.sort_field_num) - IS_CLOZE.append_name_if_differs(data_dict, self.is_cloze) - LATEX_PRE.append_name_if_differs(data_dict, self.latex_pre) - LATEX_POST.append_name_if_differs(data_dict, self.latex_post) - LATEX_SVG.append_name_if_differs(data_dict, self.latex_svg) - - data_dict.setdefault(FIELDS.name, [f.encode_as_part() for f in self.fields]) - data_dict.setdefault(TEMPLATES.name, [t.encode() for t in self.templates]) - - # Useless - TAGS.append_name_if_differs(data_dict, self.tags) - VERSION.append_name_if_differs(data_dict, self.version) - CROWDANKI_TYPE.append_name_if_differs(data_dict, self.crowdanki_type) - data_dict.setdefault(REQUIRED_FIELDS_PER_TEMPLATE.name, self.required_fields_per_template) - - return data_dict - - def get_all_media_references(self) -> Set[str]: - all_media = set() - for template in self.templates: - all_media = all_media.union(template.get_all_media_references()) - - return all_media - - @property - def field_names_lowercase(self): - return list_of_str_to_lowercase([f.name for f in self.fields]) - - def check_field_overlap(self, fields_to_check: List[str]): - fields_to_check = list_of_str_to_lowercase(fields_to_check) - - missing = [f for f in self.field_names_lowercase if f not in fields_to_check] - - return missing - - def check_field_extra(self, fields_to_check: List[str]): - fields_to_check = list_of_str_to_lowercase(fields_to_check) - - return [f for f in fields_to_check if f not in self.field_names_lowercase] - - def zip_field_to_data(self, data: List[str]) -> dict: - if len(self.fields) != len(data): - raise Exception( - f"Data of length {len(data)} cannot map to fields of length {len(self.field_names_lowercase)}", data, self.field_names_lowercase) - return dict(zip(self.field_names_lowercase, data)) - - diff --git a/brain_brew/representation/yaml/note_model_field.py b/brain_brew/representation/yaml/note_model_field.py deleted file mode 100644 index eebf277..0000000 --- a/brain_brew/representation/yaml/note_model_field.py +++ /dev/null @@ -1,86 +0,0 @@ -from dataclasses import dataclass, field -from typing import List, Union - -from brain_brew.configuration.anki_field import AnkiField -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr - -NAME = AnkiField("name") -ORDINAL = AnkiField("ord", "ordinal") -FONT = AnkiField("font", default_value="Liberation Sans") -MEDIA = AnkiField("media", default_value=[]) -IS_RIGHT_TO_LEFT = AnkiField("rtl", "is_right_to_left", default_value=False) -FONT_SIZE = AnkiField("size", "font_size", default_value=20) -IS_STICKY = AnkiField("sticky", "is_sticky", default_value=False) - - -@dataclass -class Field(RepresentationBase, YamlRepr): - @classmethod - def task_name(cls) -> str: - return r"note_model_field" - - @classmethod - def yamale_schema(cls) -> str: - return f"""\ - name: str() - font: str(required=False) - font_size: int(required=False) - is_sticky: bool(required=False) - is_right_to_left: bool(required=False) - """ - - @classmethod - def from_repr(cls, data: dict): - return cls.from_dict(data) - - @dataclass - class CrowdAnki(RepresentationBase): - name: str - ord: int = field(default=None) - font: str = field(default=FONT.default_value) - media: List[str] = field(default_factory=lambda: MEDIA.default_value) - rtl: bool = field(default=IS_RIGHT_TO_LEFT.default_value) - size: int = field(default=FONT_SIZE.default_value) - sticky: bool = field(default=IS_STICKY.default_value) - - name: str - font: str = field(default=FONT.default_value) - is_right_to_left: bool = field(default=IS_RIGHT_TO_LEFT.default_value) - font_size: int = field(default=FONT_SIZE.default_value) - is_sticky: bool = field(default=IS_STICKY.default_value) - media: List[str] = field(default_factory=lambda: MEDIA.default_value) # Unused in Anki - - @classmethod - def from_crowd_anki(cls, data: Union[CrowdAnki, dict]): - ca: cls.CrowdAnki = data if isinstance(data, cls.CrowdAnki) else cls.CrowdAnki.from_dict(data) - return cls( - name=ca.name, font=ca.font, media=ca.media, - is_right_to_left=ca.rtl, font_size=ca.size, is_sticky=ca.sticky - ) - - def encode_as_crowdanki(self, ordinal: int) -> dict: - data_dict = { - FONT.anki_name: self.font, - MEDIA.anki_name: self.media, - NAME.anki_name: self.name, - ORDINAL.anki_name: ordinal, - IS_RIGHT_TO_LEFT.anki_name: self.is_right_to_left, - FONT_SIZE.anki_name: self.font_size, - IS_STICKY.anki_name: self.is_sticky - } - - return data_dict - - def encode_as_part(self) -> dict: - data_dict = { - NAME.name: self.name - } - - FONT.append_name_if_differs(data_dict, self.font) - MEDIA.append_name_if_differs(data_dict, self.media) - IS_RIGHT_TO_LEFT.append_name_if_differs(data_dict, self.is_right_to_left) - FONT_SIZE.append_name_if_differs(data_dict, self.font_size) - IS_STICKY.append_name_if_differs(data_dict, self.is_sticky) - - return data_dict diff --git a/brain_brew/representation/yaml/note_model_template.py b/brain_brew/representation/yaml/note_model_template.py deleted file mode 100644 index 2fa1fb8..0000000 --- a/brain_brew/representation/yaml/note_model_template.py +++ /dev/null @@ -1,196 +0,0 @@ -import os -from dataclasses import dataclass, field -from typing import Optional, Union, Set - -from brain_brew.configuration.anki_field import AnkiField -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr -from brain_brew.representation.generic.html_file import HTMLFile -from brain_brew.representation.yaml.yaml_object import YamlObject -from brain_brew.utils import find_media_in_field, split_by_regex - -NAME = AnkiField("name") -ORDINAL = AnkiField("ord", "ordinal") -QUESTION_FORMAT = AnkiField("qfmt", "question_format") -ANSWER_FORMAT = AnkiField("afmt", "answer_format") -BROWSER_ANSWER_FORMAT = AnkiField("bafmt", "browser_answer_format", default_value="") -BROWSER_QUESTION_FORMAT = AnkiField("bqfmt", "browser_question_format", default_value="") -DECK_OVERRIDE_ID = AnkiField("did", "deck_override_id", default_value=None) -BROWSER_FONT = AnkiField("bfont", "browser_font", default_value="") -BROWSER_FONT_SIZE = AnkiField("bsize", "browser_font_size", default_value=0) -SCRATCH_PAD = AnkiField("scratchPad", "scratch_pad", default_value=0) - -HTML_FILE = AnkiField("html_file") -BROWSER_HTML_FILE = AnkiField("browser_html_file", default_value=None) - -html_separator_regex = r'(?:\r\n|\r|\n){1,}[-]{1,}(?:\r\n|\r|\n){1,}' - - -@dataclass -class Template(RepresentationBase, YamlObject, YamlRepr): - @classmethod - def task_name(cls) -> str: - return r'note_model_template_from_html' - - @classmethod - def yamale_schema(cls) -> str: - return f"""\ - name: str() - html_file: str() - browser_html_file: str(required=False) - deck_override_id: int(required=False) - """ - - @dataclass - class HTML(RepresentationBase): - name: str - html_file: str - browser_html_file: Optional[str] = field(default=None) - browser_font: str = field(default=BROWSER_FONT.default_value) - browser_font_size: int = field(default=BROWSER_FONT_SIZE.default_value) - deck_override_id: Optional[int] = field(default=DECK_OVERRIDE_ID.default_value) - scratch_pad: int = field(default=SCRATCH_PAD.default_value) - - @classmethod - def from_repr(cls, data: Union[HTML, dict]): - rep: cls.HTML = data if isinstance(data, cls.HTML) else cls.HTML.from_dict(data) - return cls.from_html_files(rep) - - @classmethod - def from_yaml_file(cls, filename: str) -> 'Template': - return cls.from_dict(cls.read_to_dict(filename)) - - @dataclass - class CrowdAnki(RepresentationBase): - name: str - qfmt: str - afmt: str - bqfmt: str = field(default=BROWSER_QUESTION_FORMAT.default_value) - bafmt: str = field(default=BROWSER_ANSWER_FORMAT.default_value) - bfont: str = field(default=BROWSER_FONT.default_value) - bsize: int = field(default=BROWSER_FONT_SIZE.default_value) - ord: int = field(default=None) - did: Optional[int] = field(default=None) - scratchPad: int = field(default=SCRATCH_PAD.default_value) - - name: str - question_format: str - answer_format: str - question_format_in_browser: str = field(default=BROWSER_QUESTION_FORMAT.default_value) - answer_format_in_browser: str = field(default=BROWSER_ANSWER_FORMAT.default_value) - browser_font: str = field(default=BROWSER_FONT.default_value) - browser_font_size: int = field(default=BROWSER_FONT_SIZE.default_value) - deck_override_id: Optional[int] = field(default=DECK_OVERRIDE_ID.default_value) - scratch_pad: int = field(default=SCRATCH_PAD.default_value) - - html_file: Optional[str] = field(default="") - browser_html_file: Optional[str] = field(default="") - - @classmethod - def from_html_files(cls, data: Union[HTML, dict]): - html_rep: cls.HTML = data if isinstance(data, cls.HTML) else cls.HTML.from_dict(data) - - html_file = HTMLFile.create_or_get(html_rep.html_file) - browser_html_file = HTMLFile.create_or_get(html_rep.browser_html_file) if html_rep.browser_html_file else None - - main_data = html_file.get_data(deep_copy=True) - browser_data = browser_html_file.get_data(deep_copy=True) if browser_html_file else None - - def split_template(the_data, file): - split = split_by_regex(the_data, html_separator_regex) - if len(split) != 2: - raise ValueError(f"Cannot find" if len(split) < 2 else "More than one" - f" separator '---' in html file '{file.file_location}'") - return split[0], split[1] - - front, back = split_template(main_data, html_file) - browser_front, browser_back = split_template(browser_data, browser_html_file) if browser_data else ("", "") - - return cls( - name=html_rep.name, - question_format=front, - answer_format=back, - question_format_in_browser=browser_front, - answer_format_in_browser=browser_back, - deck_override_id=html_rep.deck_override_id, - html_file=html_rep.html_file, - browser_html_file=html_rep.browser_html_file, - browser_font=html_rep.browser_font, - browser_font_size=html_rep.browser_font_size, - scratch_pad=html_rep.scratch_pad, - ) - - @classmethod - def from_crowdanki(cls, data: Union[CrowdAnki, dict]): - ca: cls.CrowdAnki = data if isinstance(data, cls.CrowdAnki) else cls.CrowdAnki.from_dict(data) - return cls( - name=ca.name, question_format=ca.qfmt, answer_format=ca.afmt, - question_format_in_browser=ca.bqfmt, answer_format_in_browser=ca.bafmt, - deck_override_id=ca.did, browser_font=ca.bfont, browser_font_size=ca.bsize, scratch_pad=ca.scratchPad, - ) - - def encode_as_part(self): - data_dict = { - NAME.name: self.name, - HTML_FILE.name: "" - } - - if self.has_browser_template(): - data_dict.setdefault(BROWSER_HTML_FILE.name, "") - - DECK_OVERRIDE_ID.append_name_if_differs(data_dict, self.deck_override_id) - BROWSER_FONT.append_name_if_differs(data_dict, self.browser_font) - BROWSER_FONT_SIZE.append_name_if_differs(data_dict, self.browser_font_size) - SCRATCH_PAD.append_name_if_differs(data_dict, self.scratch_pad) - - return data_dict - - def encode_as_crowdanki(self, ordinal: int) -> dict: - data_dict = { - ANSWER_FORMAT.anki_name: self.answer_format, - BROWSER_ANSWER_FORMAT.anki_name: self.answer_format_in_browser, - BROWSER_FONT.anki_name: self.browser_font, - BROWSER_QUESTION_FORMAT.anki_name: self.question_format_in_browser, - BROWSER_FONT_SIZE.anki_name: self.browser_font_size, - DECK_OVERRIDE_ID.anki_name: self.deck_override_id, - NAME.anki_name: self.name, - ORDINAL.anki_name: ordinal, - QUESTION_FORMAT.anki_name: self.question_format, - SCRATCH_PAD.anki_name: self.scratch_pad, - } - - return data_dict - - def encode(self) -> dict: - data_dict = { - NAME.name: self.name, - QUESTION_FORMAT.name: self.question_format, - ANSWER_FORMAT.name: self.answer_format - } - - BROWSER_QUESTION_FORMAT.append_name_if_differs(data_dict, self.question_format_in_browser) - BROWSER_ANSWER_FORMAT.append_name_if_differs(data_dict, self.answer_format_in_browser) - DECK_OVERRIDE_ID.append_name_if_differs(data_dict, self.deck_override_id) - BROWSER_FONT.append_name_if_differs(data_dict, self.browser_font) - BROWSER_FONT_SIZE.append_name_if_differs(data_dict, self.browser_font_size) - SCRATCH_PAD.append_name_if_differs(data_dict, self.scratch_pad) - - return data_dict - - def get_all_media_references(self) -> Set[str]: - all_media = set() \ - .union(find_media_in_field(self.question_format)) \ - .union(find_media_in_field(self.answer_format)) \ - .union(find_media_in_field(self.question_format_in_browser)) \ - .union(find_media_in_field(self.answer_format_in_browser)) - return all_media - - def has_browser_template(self): - return BROWSER_QUESTION_FORMAT.does_differ(self.question_format_in_browser) \ - or BROWSER_ANSWER_FORMAT.does_differ(self.answer_format_in_browser) - - def get_template_files_data(self): - template = f"{self.question_format}\n\n--\n\n{self.answer_format}" - browser_template = f"{self.question_format}\n\n--\n\n{self.answer_format}" if self.has_browser_template() else None - - return template, browser_template diff --git a/brain_brew/representation/yaml/notes.py b/brain_brew/representation/yaml/notes.py deleted file mode 100644 index ae90dcd..0000000 --- a/brain_brew/representation/yaml/notes.py +++ /dev/null @@ -1,184 +0,0 @@ -import logging -from abc import ABCMeta -from dataclasses import dataclass -from typing import List, Optional, Dict, Set - -from brain_brew.interfaces.media_container import MediaContainer -from brain_brew.representation.yaml.yaml_object import YamlObject -from brain_brew.utils import find_media_in_field - -FIELDS = 'fields' -GUID = 'guid' -TAGS = 'tags' -NOTE_MODEL = 'note_model' -FLAGS = "flags" -NOTES = "notes" -NOTE_GROUPINGS = "note_groupings" -MEDIA_REFERENCES = "media_references" - - -@dataclass -class GroupableNoteData(YamlObject, MediaContainer, metaclass=ABCMeta): - note_model: Optional[str] - tags: Optional[List[str]] - - def encode_groupable(self, data_dict): - if self.note_model is not None: - data_dict.setdefault(NOTE_MODEL, self.note_model) - if self.tags is not None and self.tags != []: - data_dict.setdefault(TAGS, self.tags) - return data_dict - - -@dataclass -class Note(GroupableNoteData): - @classmethod - def from_yaml_file(cls, filename: str) -> 'Note': - return cls.from_dict(cls.read_to_dict(filename)) - - fields: List[str] - guid: str - flags: int - # media_references: Optional[Set[str]] - - @classmethod - def from_dict(cls, data: dict): - return cls( - fields=data.get(FIELDS), - guid=data.get(GUID), - note_model=data.get(NOTE_MODEL, None), - tags=data.get(TAGS, None), - flags=data.get(FLAGS, 0) - ) - - def encode(self) -> dict: - data_dict: Dict[str, any] = {FIELDS: self.fields, GUID: self.guid} - if self.flags is not None and self.flags != 0: - data_dict.setdefault(FLAGS, self.flags) - super().encode_groupable(data_dict) - return data_dict - - def get_all_media_references(self) -> Set[str]: - return {entry for field in self.fields for entry in find_media_in_field(field)} - - -@dataclass -class NoteGrouping(GroupableNoteData): - notes: List[Note] - - @classmethod - def from_yaml_file(cls, filename: str) -> 'NoteGrouping': - return cls.from_dict(cls.read_to_dict(filename)) - - @classmethod - def from_dict(cls, data: dict): - return cls( - notes=list(map(Note.from_dict, data.get(NOTES))), - note_model=data.get(NOTE_MODEL, None), - tags=data.get(TAGS, None) - ) - - def encode(self) -> dict: - data_dict = {} - super().encode_groupable(data_dict) - data_dict.setdefault(NOTES, [note.encode() for note in self.notes]) - return data_dict - - # TODO: Extract Shared Tags and Note Models - - def verify_groupings(self): - errors = [] - if self.note_model is not None: - if any([note.note_model for note in self.notes]): - errors.append(ValueError(f"NoteGrouping for 'note_model' {self.note_model} has notes with 'note_model'." - f" Please remove one of these.")) - return errors - - def get_all_known_note_model_names(self) -> set: - return {self.note_model} if self.note_model else {note.note_model for note in self.notes} - - def get_all_media_references(self) -> Set[str]: - all_media = set() - for note in self.notes: - media = note.get_all_media_references() - all_media = all_media.union(media) - return all_media - - def get_sorted_notes(self, sort_by_keys, reverse_sort, case_insensitive_sort): - if sort_by_keys: - def sort_method(i: Note): - def get_sort_tuple(attr_or_field): - if attr_or_field in [GUID, FLAGS, NOTE_MODEL, TAGS]: - value = getattr(i, attr_or_field) - elif isinstance(attr_or_field, int) and attr_or_field < len(i.fields): - value = i.fields[attr_or_field] - else: - value = "" - logging.warning(f"No known sort value for {attr_or_field}") - - if not isinstance(value, str): - return True, False - return (value == "", value.lower()) if case_insensitive_sort else (value == "", value) - - return tuple(get_sort_tuple(column) for column in sort_by_keys) - - return sorted(self.notes, key=sort_method, reverse=reverse_sort) - elif reverse_sort: - return list(reversed(self.notes)) - - return self.notes - - def get_all_notes_copy(self, sort_by_keys, reverse_sort, case_insensitive_sort) -> List[Note]: - def join_tags(n_tags): - if self.tags is None and n_tags is None: - return [] - elif self.tags is None: - return n_tags - elif n_tags is None: - return self.tags - else: - return [*n_tags, *self.tags] - - return [Note( - note_model=self.note_model or n.note_model, - tags=join_tags(n.tags), - fields=n.fields, - guid=n.guid, - flags=n.flags - # media_references=n.media_references or n.get_media_references() - ) for n in self.get_sorted_notes(sort_by_keys, reverse_sort, case_insensitive_sort)] - - -@dataclass -class Notes(YamlObject, MediaContainer): - note_groupings: List[NoteGrouping] - - @classmethod - def from_yaml_file(cls, filename: str) -> 'Notes': - return cls.from_dict(cls.read_to_dict(filename)) - - @classmethod - def from_dict(cls, data: dict): - return cls(note_groupings=list(map(NoteGrouping.from_dict, data.get(NOTE_GROUPINGS)))) - - @classmethod - def from_list_of_notes(cls, notes: List[Note]): - return cls(note_groupings=[NoteGrouping(note_model=None, tags=None, notes=notes)]) # TODO: Check grouping here - - def encode(self) -> dict: - data_dict = {NOTE_GROUPINGS: [note_grouping.encode() for note_grouping in self.note_groupings]} - return data_dict - - def get_all_known_note_model_names(self): - return {nms for group in self.note_groupings for nms in group.get_all_known_note_model_names()} - - def get_all_media_references(self) -> Set[str]: - all_media = set() - for note_group in self.note_groupings: - media = note_group.get_all_media_references() - all_media = all_media.union(media) - return all_media - - def get_sorted_notes_copy(self, sort_by_keys, reverse_sort, case_insensitive_sort): - return [note for group in self.note_groupings - for note in group.get_all_notes_copy(sort_by_keys, reverse_sort, case_insensitive_sort)] diff --git a/brain_brew/representation/yaml/yaml_object.py b/brain_brew/representation/yaml/yaml_object.py deleted file mode 100644 index 3a05198..0000000 --- a/brain_brew/representation/yaml/yaml_object.py +++ /dev/null @@ -1,55 +0,0 @@ -from abc import ABC, abstractmethod -from pathlib import Path - -from ruamel.yaml import YAML - -from brain_brew.utils import create_path_if_not_exists - -yaml_load = YAML(typ='safe') - - -yaml_dump = YAML() -yaml_dump.preserve_quotes = False -yaml_dump.indent(mapping=2, sequence=2, offset=0) -yaml_dump.representer.ignore_aliases = lambda *data: True -# yaml.sort_base_mapping_type_on_output = False - - -class YamlObject(ABC): - @staticmethod - def read_to_dict(filename: str): - filename = YamlObject.to_filename_yaml(filename) - - if not Path(filename).is_file(): - raise FileNotFoundError(filename) - - with open(filename) as file: - return yaml_load.load(file) - - @staticmethod - def to_filename_yaml(filename: str): - if filename[-5:] != ".yaml" and filename[-4:] != ".yml": - return filename + ".yaml" - return filename - - @abstractmethod - def encode(self) -> dict: - pass - - @classmethod - @abstractmethod - def from_yaml_file(cls, filename: str) -> 'YamlObject': - pass - - def dump_to_yaml(self, filepath): - self.dump_to_yaml_file(filepath, self.encode()) - - @classmethod - def dump_to_yaml_file(cls, filepath, data): - filepath = YamlObject.to_filename_yaml(filepath) - - create_path_if_not_exists(filepath) - - with open(filepath, 'w') as fp: - yaml_dump.dump(data, fp) - diff --git a/brain_brew/schemas/__init__.py b/brain_brew/schemas/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/schemas/recipe.yaml b/brain_brew/schemas/recipe.yaml deleted file mode 100644 index 6cf5ca7..0000000 --- a/brain_brew/schemas/recipe.yaml +++ /dev/null @@ -1,173 +0,0 @@ -list( - map(include('build_parts'), key=regex('build_parts?', ignore_case=True)), - map(any(include('generate_crowd_anki'), list(include('generate_crowd_anki'))), key=regex('generate_crowd_anki', ignore_case=True)), - map(any(include('generate_csvs'), list(include('generate_csvs'))), key=regex('generate_csvs?', ignore_case=True)), - map(any(include('generate_guids_in_csvs'), list(include('generate_guids_in_csvs'))), key=regex('generate_guids_in_csvs?', ignore_case=True)), - map(any(include('save_media_groups_to_folder'), list(include('save_media_groups_to_folder'))), key=regex('save_media_groups?_to_folder', ignore_case=True)), - map(any(include('save_note_models_to_folder'), list(include('save_note_models_to_folder'))), key=regex('save_note_models?_to_folder', ignore_case=True)) -) - - ---- - -build_parts: - list( - map(any(include('headers_from_crowd_anki'), list(include('headers_from_crowd_anki'))), key=regex('headers?_from_crowd_anki', ignore_case=True)), - map(any(include('headers_from_yaml_part'), list(include('headers_from_yaml_part'))), key=regex('headers?_from_yaml_part', ignore_case=True)), - map(any(include('media_group_from_crowd_anki'), list(include('media_group_from_crowd_anki'))), key=regex('media_group_from_crowd_anki', ignore_case=True)), - map(any(include('media_group_from_folder'), list(include('media_group_from_folder'))), key=regex('media_group_from_folder', ignore_case=True)), - map(any(include('media_group_from_yaml_part'), list(include('media_group_from_yaml_part'))), key=regex('media_group_from_yaml_part', ignore_case=True)), - map(any(include('note_model_from_crowd_anki'), list(include('note_model_from_crowd_anki'))), key=regex('note_model_from_crowd_anki', ignore_case=True)), - map(any(include('note_model_from_html_parts'), list(include('note_model_from_html_parts'))), key=regex('note_model_from_html_parts', ignore_case=True)), - map(any(include('note_models_all_from_crowd_anki'), list(include('note_models_all_from_crowd_anki'))), key=regex('note_models_all_from_crowd_anki', ignore_case=True)), - map(any(include('note_models_from_yaml_part'), list(include('note_models_from_yaml_part'))), key=regex('note_models?_from_yaml_part', ignore_case=True)), - map(any(include('notes_from_crowd_anki'), list(include('notes_from_crowd_anki'))), key=regex('notes_from_crowd_anki', ignore_case=True)), - map(any(include('notes_from_csvs'), list(include('notes_from_csvs'))), key=regex('notes_from_csvs?', ignore_case=True)), - map(any(include('notes_from_yaml_part'), list(include('notes_from_yaml_part'))), key=regex('notes_from_yaml_part', ignore_case=True)) - ) - -generate_crowd_anki: - folder: str() - headers: str() - notes: include('notes_to_crowd_anki') - note_models: include('note_models_to_crowd_anki') - media: include('media_group_to_crowd_anki', required=False) - -generate_csvs: - notes: str() - note_model_mappings: list(include('note_model_mapping')) - file_mappings: list(include('file_mapping')) - -generate_guids_in_csvs: - source: any(str(), list(str())) - columns: any(str(), list(str())) - delimiter: str(required=False) - -save_media_groups_to_folder: - parts: list(str()) - folder: str() - clear_folder: bool(required=False) - recursive: bool(required=False) - -save_note_models_to_folder: - parts: list(str()) - folder: str() - clear_existing: bool(required=False) - - ---- - -file_mapping: - file: str() - note_model: str(required=False) - sort_by_columns: list(str(), required=False) - reverse_sort: bool(required=False) - case_insensitive_sort: bool(required=False) - derivatives: list(include('file_mapping'), required=False) - delimiter: str(required=False) - -headers_from_crowd_anki: - part_id: str() - source: str() - save_to_file: str(required=False) - -headers_from_yaml_part: - part_id: str() - file: str() - override: include('headers_override', required=False) - -headers_override: - crowdanki_uuid: str(required=False) - deck_description_html_file: str(required=False) - name: str(required=False) - -media_group_from_crowd_anki: - part_id: str() - source: str() - save_to_file: str(required=False) - recursive: bool(required=False) - filter_whitelist_from_parts: list(str(), required=False) - filter_blacklist_from_parts: list(str(), required=False) - -media_group_from_folder: - part_id: str() - source: str() - save_to_file: str(required=False) - recursive: bool(required=False) - filter_whitelist_from_parts: list(str(), required=False) - filter_blacklist_from_parts: list(str(), required=False) - -media_group_from_yaml_part: - part_id: str() - file: str() - -media_group_to_crowd_anki: - parts: list(str()) - -note_model_field: - name: str() - font: str(required=False) - font_size: int(required=False) - is_sticky: bool(required=False) - is_right_to_left: bool(required=False) - -note_model_from_crowd_anki: - part_id: str() - source: str() - model_name: str(required=False) - save_to_file: str(required=False) - -note_model_from_html_parts: - part_id: str() - model_id: str() - css_file: str() - fields: list(include('note_model_field')) - templates: list(str()) - model_name: str(required=False) - save_to_file: str(required=False) - -note_model_mapping: - note_models: any(list(str()), str()) - columns_to_fields: map(str(), key=str(), required=False) - personal_fields: list(str(), required=False) - -note_models_all_from_crowd_anki: - source: str() - -note_models_from_yaml_part: - part_id: str() - file: str() - -note_models_to_crowd_anki: - parts: list(include('note_models_to_crowd_anki_item')) - -note_models_to_crowd_anki_item: - part_id: str() - -notes_from_crowd_anki: - part_id: str() - source: str() - sort_order: list(str(), required=False) - save_to_file: str(required=False) - reverse_sort: str(required=False) - -notes_from_csvs: - part_id: str() - save_to_file: str(required=False) - note_model_mappings: list(include('note_model_mapping')) - file_mappings: list(include('file_mapping')) - -notes_from_yaml_part: - part_id: str() - file: str() - -notes_override: - note_model: str(required=False) - -notes_to_crowd_anki: - part_id: str() - sort_order: list(str(), required=False) - reverse_sort: bool(required=False) - additional_items_to_add: map(str(), key=str(), required=False) - override: include('notes_override', required=False) - case_insensitive_sort: bool(required=False) diff --git a/brain_brew/transformers/__init__.py b/brain_brew/transformers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/brain_brew/transformers/create_media_group_from_location.py b/brain_brew/transformers/create_media_group_from_location.py deleted file mode 100644 index ee09946..0000000 --- a/brain_brew/transformers/create_media_group_from_location.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import List - -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.interfaces.media_container import MediaContainer -from brain_brew.representation.yaml.media_group import MediaGroup - - -def create_media_group_from_location( - part_id: str, - save_to_file: str, - media_group: MediaGroup, - groups_to_blacklist: List[MediaContainer], - groups_to_whitelist: List[MediaContainer] -) -> MediaGroup: - if groups_to_whitelist: - white = list(set.union(*[container.get_all_media_references() for container in groups_to_whitelist])) - media_group.filter_by_filenames(white, should_match=True) - - if groups_to_blacklist: - black = list(set.union(*[container.get_all_media_references() for container in groups_to_blacklist])) - media_group.filter_by_filenames(black, should_match=False) - - holder = PartHolder.override_or_create(part_id, save_to_file, media_group) - return holder.part diff --git a/brain_brew/transformers/file_mapping.py b/brain_brew/transformers/file_mapping.py deleted file mode 100644 index 644dd9d..0000000 --- a/brain_brew/transformers/file_mapping.py +++ /dev/null @@ -1,194 +0,0 @@ -import re - -import logging -from dataclasses import dataclass, field -from typing import Dict, List, Optional, Union - -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr -from brain_brew.representation.generic.csv_file import CsvFile, CsvKeys -from brain_brew.utils import single_item_to_list - -FILE = "csv_file" -NOTE_MODEL = "note_model" -SORT_BY_COLUMNS = "sort_by_columns" -REVERSE_SORT = "reverse_sort" -DERIVATIVES = "derivatives" - - -@dataclass -class FileMappingDerivative(YamlRepr): - @classmethod - def task_name(cls) -> str: - return r'file_mapping' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - file: str() - note_model: str(required=False) - sort_by_columns: list(str(), required=False) - reverse_sort: bool(required=False) - case_insensitive_sort: bool(required=False) - derivatives: list(include('{cls.task_name()}'), required=False) - delimiter: str(required=False) - ''' - - @dataclass(init=False) - class Representation(RepresentationBase): - file: str - note_model: Optional[str] - sort_by_columns: Optional[Union[list, str]] - reverse_sort: Optional[bool] - derivatives: Optional[List['FileMappingDerivative.Representation']] - delimiter: Optional[str] - - def __init__(self, file, note_model=None, sort_by_columns=None, reverse_sort=None, case_insensitive_sort=None, derivatives=None, delimiter=None): - self.file = file - self.note_model = note_model - self.sort_by_columns = sort_by_columns - self.reverse_sort = reverse_sort - self.case_insensitive_sort = case_insensitive_sort - self.derivatives = list(map(FileMappingDerivative.Representation.from_dict, derivatives)) \ - if derivatives is not None else [] - self.delimiter = delimiter - - def encode_filter(self, key, value): - if not super().encode_filter(key, value): - return False - if key == 'delimiter' and CsvFile.delimiter_matches_file_type(value, self.file): - return False - return True - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - csv = CsvFile.create_or_get(rep.file) - csv.set_delimiter(rep.delimiter) - csv.read_file() - return cls( - rep=rep, - csv_file=csv, - note_model=rep.note_model.strip() if rep.note_model else None, - sort_by_columns=single_item_to_list(rep.sort_by_columns), - reverse_sort=rep.reverse_sort or False, - case_insensitive_sort=rep.case_insensitive_sort or True, - derivatives=list(map(cls.from_repr, rep.derivatives)) if rep.derivatives is not None else [], - ) - - rep: Representation - - compiled_data: Dict[str, dict] = field(init=False) - - csv_file: CsvFile - - note_model: Optional[str] - sort_by_columns: list - reverse_sort: bool - case_insensitive_sort: bool - derivatives: Optional[List['FileMappingDerivative']] - - def get_available_columns(self): - return self.csv_file.column_headers + [col for der in self.derivatives for col in der.get_available_columns()] - - def get_used_note_model_names(self) -> List[str]: - nm = [self.note_model] if self.note_model is not None else [] - return nm + [name for der in self.derivatives for name in der.get_used_note_model_names()] - - def _build_data_recursive(self) -> List[dict]: - data_in_progress = self.csv_file.get_data(deep_copy=True) - - new_columns_seen_so_far = self.csv_file.column_headers.copy() - for der in self.derivatives: - der_cols = der.get_available_columns() - overlapping_cols = [col for col in der_cols if col in self.csv_file.column_headers] - der_cols = [col for col in der_cols if col not in overlapping_cols] - - if not overlapping_cols: - raise KeyError("No column overlap for derivative") - - column_repeat_errors = [KeyError(f"Derivative column {c} in multiple derivative lines") - for c in der_cols if c in new_columns_seen_so_far] - if column_repeat_errors: - raise Exception(column_repeat_errors) - new_columns_seen_so_far += der_cols - - der_match_errors = [] - for der_row in der._build_data_recursive(): - # Find matching row to pair data with - found_match = False - for row in data_in_progress: - if all([der_row[c] == row[c] for c in overlapping_cols]): - for der_col in der_cols: - row[der_col] = der_row[der_col] - found_match = True - # Set Note Model to matching Derivative Note Model - if der.note_model is not None: - row.setdefault(NOTE_MODEL, der.note_model) - break - if not found_match: - der_match_errors.append(ValueError(f"Cannot match derivative row {der_row} to parent")) - - if der_match_errors: - raise Exception(der_match_errors) - - return data_in_progress - - def write_to_csv(self, data_to_set): - self.csv_file.set_data_from_superset(data_to_set) - self.csv_file.sort_data(self.sort_by_columns, self.reverse_sort, self.case_insensitive_sort) - self.csv_file.write_file() - - for der in self.derivatives: - der.write_to_csv(data_to_set) - - -@dataclass -class FileMapping(FileMappingDerivative): - note_model: str # Override Optional on Children - - data_set_has_changed: bool = field(init=False, default=False) - - def compile_data(self): - self.compiled_data = {} - self.data_set_has_changed = False - - data_in_progress = self._build_data_recursive() - - # Set Note Model if not already set - if self.note_model is not None: - for row in data_in_progress: - row.setdefault(NOTE_MODEL, self.note_model) - - for row in data_in_progress: - guid = row[CsvKeys.GUID.value] - if not guid: - raise KeyError("Some rows are missing guids") - self.compiled_data.setdefault(guid, {key.lower(): row[key] for key in row}) - - def set_relevant_data(self, data_set: Dict[str, dict]): - unchanged, changed, added = 0, 0, 0 - for guid in data_set: - if guid in self.compiled_data.keys(): - changed_row = False - for key in data_set[guid]: - if key in self.compiled_data[guid] and self.compiled_data[guid][key] != data_set[guid][key]: - self.compiled_data[guid][key] = data_set[guid][key] - changed_row = True - if changed_row: - changed += 1 - else: - unchanged += 1 - else: - added += 1 - self.compiled_data.setdefault(guid, data_set[guid]) - - if changed > 0 or added > 0: - self.data_set_has_changed = True - - logging.info(f"Set {self.csv_file.file_location} data; changed {changed}, " - f"added {added}, while {unchanged} were identical") - - def write_file_on_close(self): - if self.data_set_has_changed: - self.write_to_csv(list(self.compiled_data.values())) diff --git a/brain_brew/transformers/note_model_mapping.py b/brain_brew/transformers/note_model_mapping.py deleted file mode 100644 index f27dce8..0000000 --- a/brain_brew/transformers/note_model_mapping.py +++ /dev/null @@ -1,178 +0,0 @@ -from dataclasses import dataclass, field -from enum import Enum -from typing import List, Union, Dict, Optional - -from brain_brew.configuration.part_holder import PartHolder -from brain_brew.configuration.representation_base import RepresentationBase -from brain_brew.interfaces.yamale_verifyable import YamlRepr -from brain_brew.representation.yaml.note_model import NoteModel -from brain_brew.representation.yaml.notes import GUID, TAGS -from brain_brew.utils import single_item_to_list - - -class FieldMapping: - class FieldMappingType(Enum): - COLUMN = "column" - PERSONAL_FIELD = "personal_field" - DEFAULT = "default" - - @classmethod - def values(cls): - return set(it.value for it in cls) - - type: FieldMappingType - value: str - field_name: str - - def __init__(self, field_type: FieldMappingType, field_name: str, value: str): - self.type = field_type - self.field_name = field_name.lower() - - if self.type == FieldMapping.FieldMappingType.COLUMN: - self.value = value.lower() - else: - self.value = value - - -@dataclass -class NoteModelMapping(YamlRepr): - @classmethod - def task_name(cls) -> str: - return r'note_model_mapping' - - @classmethod - def yamale_schema(cls) -> str: - return f'''\ - note_models: any(list(str()), str()) - columns_to_fields: map(str(), key=str(), required=False) - personal_fields: list(str(), required=False) - ''' - - @dataclass - class Representation(RepresentationBase): - note_models: Union[str, list] - columns_to_fields: Optional[Dict[str, str]] = field(default=None) - personal_fields: List[str] = field(default_factory=lambda: []) - - note_models: Dict[str, PartHolder[NoteModel]] - columns_manually_mapped: List[FieldMapping] - personal_fields: List[FieldMapping] - - @classmethod - def from_repr(cls, data: Union[Representation, dict]): - rep: cls.Representation = data if isinstance(data, cls.Representation) else cls.Representation.from_dict(data) - note_models = [PartHolder.from_file_manager(model) for model in single_item_to_list(rep.note_models)] - - return cls( - columns_manually_mapped=[FieldMapping( - field_type=FieldMapping.FieldMappingType.COLUMN, - field_name=f, - value=key) for key, f in rep.columns_to_fields.items()] - if rep.columns_to_fields else [], - personal_fields=[FieldMapping( - field_type=FieldMapping.FieldMappingType.PERSONAL_FIELD, - field_name=f, - value="") for f in rep.personal_fields], - note_models=dict(map(lambda nm: (nm.part_id, nm), note_models)) - ) - - def get_note_model_mapping_dict(self): - return {model: self for model in self.note_models} - - def verify_contents(self): - if not self.columns_manually_mapped: # No check needed if no manual mapping is performed - return - - errors = [] - required_field_definitions = [GUID, TAGS] - - extra_fields = [field.field_name for field in self.columns_manually_mapped - if field.field_name not in required_field_definitions] - - for holder in self.note_models.values(): - model: NoteModel = holder.part - - # Check for Required Fields - missing = [] - for req in required_field_definitions: - if req not in [field.field_name for field in self.columns_manually_mapped]: - missing.append(req) - - if missing: - errors.append(KeyError(f"""Error in note_model_mappings part with note model "{holder.part_id}". \ - When mapping columns_to_fields you must map all fields. \ - Mapping is missing for for fields: {missing}""")) - - # Check Fields Align with Note Type - missing = model.check_field_overlap( - [field.field_name for field in self.columns_manually_mapped - if field.field_name not in required_field_definitions] - ) - missing = [m for m in missing if m not in [field.field_name for field in self.personal_fields]] - - if missing: - errors.append(KeyError(f"""Error in note_model_mappings part with note model "{holder.part_id}". \ - When mapping columns_to_fields you must map all fields. \ - Mapping is missing for for fields: {missing}""")) - - # Find mappings which do not exist on any note models - if extra_fields: - extra_fields = model.check_field_extra(extra_fields) - - if extra_fields: - errors.append( - KeyError(f"""Error in note_model_mappings part. \ - Field(s) '{extra_fields}' are defined as mappings, but match no Note Model fields""")) - - if errors: - raise Exception(errors) - - def csv_row_map_to_note_fields(self, row: dict) -> dict: - relevant_row_data = self.filter_data_row_by_relevant_columns(row) - - for pf in self.personal_fields: # Add in Personal Fields - relevant_row_data.setdefault(pf.field_name, False) - for column in self.columns_manually_mapped: # Rename from Csv Column to Note Type Field - if column.value in relevant_row_data: - relevant_row_data[column.field_name] = relevant_row_data.pop(column.value) - - return relevant_row_data - - def csv_headers_map_to_note_fields(self, row: list) -> list: - return list(self.csv_row_map_to_note_fields({row_name: "" for row_name in row}).keys()) - - def note_fields_map_to_csv_row(self, row): - for column in self.columns_manually_mapped: # Rename from Note Type Field to Csv Column - if column.field_name in row: - row[column.value] = row.pop(column.field_name) - for pf in self.personal_fields: # Remove Personal Fields - if pf.field_name in row: - del row[pf.field_name] - - relevant_row_data = self.filter_data_row_by_relevant_columns(row) - - return relevant_row_data - - def filter_data_row_by_relevant_columns(self, row): - cols = list(row.keys()) - - relevant_columns = [f.value for f in self.columns_manually_mapped] - if not relevant_columns: - return row - - # errors = [KeyError(f"Missing column {rel_col}") for rel_col in relevant_columns if rel_col not in cols] - # if errors: - # raise Exception(errors) - - irrelevant_columns = [column for column in cols if column not in relevant_columns] - if not irrelevant_columns: - return row - - relevant_data = {key: row[key] for key in row if key not in irrelevant_columns} - - return relevant_data - - def field_values_in_note_model_order(self, note_model_name, fields_from_csv): - return [fields_from_csv[f] if f in fields_from_csv else "" - for f in self.note_models[note_model_name].part.field_names_lowercase - ] diff --git a/brain_brew/transformers/save_media_group_to_location.py b/brain_brew/transformers/save_media_group_to_location.py deleted file mode 100644 index fb5e8fb..0000000 --- a/brain_brew/transformers/save_media_group_to_location.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging -from typing import List, Set - -from brain_brew.representation.generic.media_file import MediaFile -from brain_brew.representation.yaml.media_group import MediaGroup -from brain_brew.utils import create_path_if_not_exists - - -def save_media_groups_to_location( - parts: List[MediaGroup], - folder: str, - clear_folder: bool, - recursive: bool -) -> Set[MediaFile]: - - create_path_if_not_exists(folder, is_path_override=True) - - existing_media_group = MediaGroup.from_directory(folder, recursive) - all_media_group = MediaGroup.from_many(parts) - - in_both, to_move, to_delete = all_media_group.compare(existing_media_group) - - for filename, media_file in all_media_group.media_files.items(): - if filename in in_both: - media_file.copy_self_to_target(existing_media_group.media_files[filename].file_path) - # TODO: Check if copying is needed? - elif filename in to_move: - media_file.copy_self_to_target(folder) - - if clear_folder and to_delete: - deleted = '\n'.join(to_delete) - logging.warning(f"Deleting extra files in media folder '{folder}':\n{'-'*20}\n{deleted}\n{'-'*20}") - for delete_name in to_delete: - existing_media_group.media_files[delete_name].delete_self() - - return set(all_media_group.media_files.values()) diff --git a/brain_brew/transformers/save_note_model_to_location.py b/brain_brew/transformers/save_note_model_to_location.py deleted file mode 100644 index 9a7b367..0000000 --- a/brain_brew/transformers/save_note_model_to_location.py +++ /dev/null @@ -1,46 +0,0 @@ -import logging -import os -from typing import List - -from brain_brew.representation.generic.html_file import HTMLFile -from brain_brew.representation.yaml.note_model import NoteModel, CSS_FILE, TEMPLATES -from brain_brew.representation.yaml.note_model_template import HTML_FILE as TEMPLATE_HTML_FILE, NAME as TEMPLATE_NAME, BROWSER_HTML_FILE as TEMPLATE_BROWSER_HTML_FILE -from brain_brew.representation.yaml.yaml_object import YamlObject -from brain_brew.utils import create_path_if_not_exists, clear_contents_of_folder - - -def save_note_model_to_location( - model: NoteModel, - folder: str, - clear_folder: bool -) -> str: - - nm_folder = os.path.join(folder, model.name + '/') - create_path_if_not_exists(nm_folder) - - if clear_folder: - clear_contents_of_folder(nm_folder) - - model_encoded = model.encode_as_part_with_empty_file_references() - - model_encoded[CSS_FILE.name] = os.path.join(nm_folder, "style.css") - HTMLFile.write_file(model_encoded[CSS_FILE.name], model.css) - - templates_dict = {t.name: t for t in model.templates} - - for template_data in model_encoded[TEMPLATES.name]: - name = template_data[TEMPLATE_NAME.name] - template = templates_dict[name] - t_data, b_t_data = template.get_template_files_data() - - template_data[TEMPLATE_HTML_FILE.name] = os.path.join(nm_folder, HTMLFile.to_filename_html(name)) - HTMLFile.write_file(template_data[TEMPLATE_HTML_FILE.name], t_data) - - if TEMPLATE_BROWSER_HTML_FILE.name in template_data and b_t_data is not None: - template_data[TEMPLATE_BROWSER_HTML_FILE.name] = os.path.join(nm_folder, HTMLFile.to_filename_html(name + "_browser")) - HTMLFile.write_file(template_data[TEMPLATE_BROWSER_HTML_FILE.name], b_t_data) - - model_yaml_file_name = YamlObject.to_filename_yaml(os.path.join(nm_folder, model.name)) - YamlObject.dump_to_yaml_file(model_yaml_file_name, model_encoded) - - return model_yaml_file_name diff --git a/brain_brew/utils.py b/brain_brew/utils.py deleted file mode 100644 index 2b85f39..0000000 --- a/brain_brew/utils.py +++ /dev/null @@ -1,129 +0,0 @@ -import logging -import os -import random -import re -import shutil -import string -from pathlib import Path -from typing import List - - -def blank_str_if_none(s): - return '' if s is None else s - - -def list_of_str_to_lowercase(list_of_strings): - if not list_of_strings: - return [] - return [entry.lower() for entry in list_of_strings] - - -def single_item_to_list(item): - if isinstance(item, list): - return item - if item is None: - return [] - return [item] - - -def str_to_lowercase_no_separators(str_to_tidy: str): - return re.sub(r'[\s+_-]+', '', str_to_tidy.lower()) - - -def filename_from_full_path(full_path): - return re.findall(r'[^\\/:*?"<>|\r\n]+$', full_path)[0] - - -def folder_name_from_full_path(full_path): - return re.findall(r'[^\\/:*?"<>|\r\n]+[/]?$', full_path)[0] - - -def split_by_regex(str_to_split: str, pattern: str) -> List[str]: - return re.split(pattern, str_to_split) - - -def find_media_in_field(field_value: str) -> List[str]: - if not field_value: - return [] - - images = re.findall(r'<\s*?img.*?src="(.*?)"[^>]*?>', field_value) - audio = re.findall(r'\[sound:(.*?)]', field_value) - - return images + audio - - -def find_all_files_in_directory(directory, recursive=False): - found_files = [] - for path, dirs, files in os.walk(directory): - for file in files: - found_files.append(os.path.join(path, file)) - if not recursive: - return found_files - return found_files - - -def create_path_if_not_exists(path, is_path_override=False): - dir_name = os.path.dirname(path) if not is_path_override else path - if not Path(dir_name).is_dir(): - logging.warning(f"Creating missing filepath '{dir_name}'") - os.makedirs(dir_name, exist_ok=True) - - -def clear_contents_of_folder(path): - for filename in os.listdir(path): - file_path = os.path.join(path, filename) - try: - if os.path.isfile(file_path) or os.path.islink(file_path): - os.unlink(file_path) - elif os.path.isdir(file_path): - shutil.rmtree(file_path) - except Exception as e: - print('Failed to delete %s. Reason: %s' % (file_path, e)) - - -def split_tags(tags_value: str) -> list: - split = [entry.strip() for entry in re.split(r';\s*|,\s*|\s+', tags_value)] - while "" in split: - split.remove("") - return split - - -def join_tags(tags_list: list) -> str: - return ", ".join(tags_list) # TODO: Make configurable - - -def generate_anki_guid() -> str: - """Return a base91-encoded 64bit random number.""" - - def base62(num: int, extra: str = "") -> str: - s = string - table = s.ascii_letters + s.digits + extra - buf = "" - while num: - num, i = divmod(num, len(table)) - buf = table[i] + buf - return buf - - _base91_extra_chars = "!#$%&()*+,-./:;<=>?@[]^_`{|}~" - - def base91(num: int) -> str: - # all printable characters minus quotes, backslash and separators - return base62(num, _base91_extra_chars) - - return base91(random.randint(0, 2 ** 64 - 1)) - - -def sort_dict(data, sort_by_keys, reverse_sort, case_insensitive_sort): - if sort_by_keys: - if case_insensitive_sort: - def sort_method(i): - return tuple((i[column] == "", i[column].lower()) for column in sort_by_keys) - else: - def sort_method(i): - return tuple((i[column] == "", i[column]) for column in sort_by_keys) - - return sorted(data, key=sort_method, reverse=reverse_sort) - elif reverse_sort: - return list(reversed(data)) - - return data diff --git a/scripts/__init__.py b/scripts/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/scripts/build.bash b/scripts/build.bash deleted file mode 100755 index 1213b46..0000000 --- a/scripts/build.bash +++ /dev/null @@ -1,4 +0,0 @@ -# Build -rm -r dist -rm -r build -python3 setup.py sdist bdist_wheel \ No newline at end of file diff --git a/scripts/dist.bash b/scripts/dist.bash deleted file mode 100755 index 9e1a94b..0000000 --- a/scripts/dist.bash +++ /dev/null @@ -1,9 +0,0 @@ -# See credentials in ~/.pypirc - -# To use an API token: -# -# Set your username to __token__ -# Set your password to the token value, including the pypi- prefix - -# Upload -twine upload dist/* --verbose diff --git a/scripts/yamale_build.py b/scripts/yamale_build.py deleted file mode 100755 index c5eba2c..0000000 --- a/scripts/yamale_build.py +++ /dev/null @@ -1,13 +0,0 @@ -import os -import sys - -sys.path.append(os.path.abspath('')) - -from brain_brew.commands.run_recipe.top_level_builder import TopLevelBuilder - -build: str = TopLevelBuilder.build_yamale() -filepath = "brain_brew/schemas/recipe.yaml" - -with open(filepath, 'w') as fp: - fp.write(build) - fp.close() diff --git a/setup.py b/setup.py deleted file mode 100644 index 71eed1c..0000000 --- a/setup.py +++ /dev/null @@ -1,36 +0,0 @@ -import setuptools -from brain_brew.front_matter import latest_version_number - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="Brain-Brew", - version=latest_version_number(), - author="Jordan Munch O'Hare", - author_email="brainbrew@jordan.munchohare.com", - description="Automated Anki flashcard creation and extraction to/from Csv ", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/jeprecated/brain-brew", - packages=setuptools.find_packages(), - include_package_data=True, - entry_points={ - 'console_scripts': [ - 'brain_brew = brain_brew.main:main', - 'brain-brew = brain_brew.main:main', - 'brainbrew = brain_brew.main:main', - ] - }, - classifiers=[ - "Programming Language :: Python :: 3", - "License :: Public Domain", - "Operating System :: OS Independent", - ], - python_requires='>=3.7', - install_requires=[ - 'ruamel.yaml.clib>=0.2.2', - 'ruamel.yaml>=0.16.10', - 'yamale>=3.0.4' - ] -) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/build_tasks/__init__.py b/tests/build_tasks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/build_tasks/test_source_crowd_anki_json.py b/tests/build_tasks/test_source_crowd_anki_json.py deleted file mode 100644 index 5c2a98f..0000000 --- a/tests/build_tasks/test_source_crowd_anki_json.py +++ /dev/null @@ -1,113 +0,0 @@ -# from unittest.mock import patch -# -# import pytest -# -# from brain_brew.constants.build_config_keys import BuildConfigKeys -# from brain_brew.build_tasks.source_crowd_anki import SourceCrowdAnki, CrowdAnkiKeys -# from brain_brew.representation.json.crowd_anki_export import CrowdAnkiExport -# from brain_brew.representation.json.part_header import DeckPartHeader -# from brain_brew.representation.yaml.note_model_repr import DeckPartNoteModel -# from brain_brew.representation.json.part_notes import DeckPartNotes -# -# -# def setup_ca_config(file, media, useless_note_keys, notes, headers): -# return { -# CrowdAnkiKeys.FILE.value: file, -# CrowdAnkiKeys.MEDIA.value: media, -# CrowdAnkiKeys.USELESS_NOTE_KEYS.value: useless_note_keys, -# BuildConfigKeys.NOTES.value: notes, -# BuildConfigKeys.HEADERS.value: headers -# } -# -# -# class TestConstructor: -# @pytest.mark.parametrize("file, media, useless_note_keys, notes, headers, read_file_now", [ -# ("test", False, {}, "test.json", "header.json", False), -# ("export1", True, {}, "test.json", "header.json", False), -# ("json.json", False, {}, "test.json", "", True), -# ("", False, {"__type__": "Note", "data": None, "flags": 0}, "test.json", "header.json", False) -# ]) -# def test_runs(self, file, media, useless_note_keys, notes, headers, read_file_now, global_config): -# config = setup_ca_config(file, media, useless_note_keys, notes, headers) -# -# def assert_dp_header(passed_file, read_now): -# assert passed_file == headers -# assert read_now == read_file_now -# -# def assert_dp_notes(passed_file, read_now): -# assert passed_file == notes -# assert read_now == read_file_now -# -# def assert_ca_export(passed_file, read_now): -# assert passed_file == file -# assert read_now == read_file_now -# -# with patch.object(DeckPartHeader, "create", side_effect=assert_dp_header) as mock_header, \ -# patch.object(DeckPartNotes, "create", side_effect=assert_dp_notes) as mock_notes, \ -# patch.object(CrowdAnkiExport, "create", side_effect=assert_ca_export) as ca_export: -# -# source = SourceCrowdAnki(config, read_now=read_file_now) -# -# assert isinstance(source, SourceCrowdAnki) -# assert source.should_handle_media == media -# assert source.useless_note_keys == useless_note_keys -# -# assert mock_header.call_count == 1 -# assert mock_notes.call_count == 1 -# assert ca_export.call_count == 1 -# -# -# @pytest.fixture() -# def source_crowd_anki_test1(global_config) -> SourceCrowdAnki: -# with patch.object(DeckPartHeader, "create", return_value=None) as mock_header, \ -# patch.object(DeckPartNotes, "create", return_value=None) as mock_notes, \ -# patch.object(CrowdAnkiExport, "create", return_value=None) as mock_ca_export: -# -# source = SourceCrowdAnki( -# setup_ca_config("", False, {"__type__": "Note", "data": None, "flags": 0}, "", "") -# ) -# -# # source.notes = dp_notes_test1 -# # source.headers = dp_headers_test1 -# # source.crowd_anki_export = ca_export_test1 -# -# return source -# -# -# class TestSourceToDeckParts: -# def test_runs(self, source_crowd_anki_test1: SourceCrowdAnki, ca_export_test1, -# temp_dp_note_model_file, temp_dp_headers_file, temp_dp_notes_file, -# dp_note_model_test1, dp_headers_test1, dp_notes_test1): -# -# # CrowdAnki Export it will use to write to the DeckParts -# source_crowd_anki_test1.crowd_anki_export = ca_export_test1 -# -# # DeckParts to be written to (+ the NoteModel below) -# source_crowd_anki_test1.headers = temp_dp_headers_file -# source_crowd_anki_test1.notes = temp_dp_notes_file -# -# def assert_note_model(name, data_override): -# assert data_override == dp_note_model_test1.get_data() -# return dp_note_model_test1 -# -# with patch.object(DeckPartNoteModel, "create", side_effect=assert_note_model) as mock_nm: -# source_crowd_anki_test1.source_to_parts() -# -# assert source_crowd_anki_test1.headers.get_data() == dp_headers_test1.get_data() -# assert source_crowd_anki_test1.notes.get_data() == dp_notes_test1.get_data() -# -# assert mock_nm.call_count == 1 -# -# -# class TestDeckPartsToSource: -# def test_runs(self, source_crowd_anki_test1: SourceCrowdAnki, temp_ca_export_file, -# ca_export_test1, dp_notes_test1, dp_headers_test1): -# source_crowd_anki_test1.crowd_anki_export = temp_ca_export_file # File to write result to -# -# # DeckParts it will use (+ dp_note_model_test1, but it reads that in as a file) -# source_crowd_anki_test1.headers = dp_headers_test1 -# source_crowd_anki_test1.notes = dp_notes_test1 -# -# source_crowd_anki_test1.parts_to_source() # Where the magic happens -# -# assert temp_ca_export_file.get_data() == ca_export_test1.get_data() diff --git a/tests/build_tasks/test_source_csv.py b/tests/build_tasks/test_source_csv.py deleted file mode 100644 index 2b5e923..0000000 --- a/tests/build_tasks/test_source_csv.py +++ /dev/null @@ -1,140 +0,0 @@ -# from typing import List -# from unittest.mock import patch -# -# import pytest -# -# from brain_brew.build_tasks.source_csv import SourceCsv, SourceCsvKeys -# from brain_brew.constants.deckpart_keys import DeckPartNoteKeys -# from brain_brew.representation.configuration.csv_file_mapping import FileMapping -# from brain_brew.representation.configuration.note_model_mapping import NoteModelMapping -# from brain_brew.representation.generic.csv_file import CsvFile -# from brain_brew.representation.generic.source_file import SourceFile -# from brain_brew.representation.json.part_notes import DeckPartNotes -# from tests.representation.json.test_part_notes import dp_notes_test1 -# from tests.representation.configuration.test_note_model_mapping import setup_nmm_config -# from tests.representation.configuration.test_csv_file_mapping import setup_csv_fm_config -# -# -# def setup_source_csv_config(notes: str, nmm: list, csv_mappings: list): -# return { -# SourceCsvKeys.NOTES.value: notes, -# SourceCsvKeys.NOTE_MODEL_MAPPINGS.value: nmm, -# SourceCsvKeys.CSV_MAPPINGS.value: csv_mappings -# } -# -# -# def get_csv_default(notes: DeckPartNotes, nmm: List[NoteModelMapping], csv_maps: List[FileMapping]) -> SourceCsv: -# csv_source = SourceCsv(setup_source_csv_config("", [], []), read_now=False) -# -# csv_source.notes = notes -# csv_source.note_model_mappings_dict = {nm_map.note_model.name: nm_map for nm_map in nmm} -# csv_source.csv_file_mappings = csv_maps -# -# return csv_source -# -# -# @pytest.fixture() -# def csv_source_test1(dp_notes_test1, nmm_test1, csv_file_mapping1) -> SourceCsv: -# return get_csv_default(dp_notes_test1, [nmm_test1], [csv_file_mapping1]) -# -# -# @pytest.fixture() -# def csv_source_test1_split1(csv_source_default, csv_test1_split1, dp_notes_test1) -> SourceCsv: -# csv_source_default.csv_file = csv_test1_split1 -# csv_source_default.notes = dp_notes_test1 -# return csv_source_default -# -# -# @pytest.fixture() -# def csv_source_test1_split2(csv_source_default2, csv_test1_split2, dp_notes_test2) -> SourceCsv: -# csv_source_default2.csv_file = csv_test1_split2 -# csv_source_default2.notes = dp_notes_test1 -# return csv_source_default2 -# -# -# @pytest.fixture() -# def csv_source_test2(dp_notes_test2, nmm_test1, csv_file_mapping2) -> SourceCsv: -# return get_csv_default(dp_notes_test2, [nmm_test1], [csv_file_mapping2]) -# -# -# # @pytest.fixture() -# # def temp_csv_source(global_config, tmpdir) -> SourceCsv: -# # file = tmpdir.mkdir("notes").join("file.csv") -# # file.write("test,1,2,3") -# -# -# class TestConstructor: -# def test_runs(self): -# source_csv = get_csv_default(None, [], []) -# assert isinstance(source_csv, SourceCsv) -# -# @pytest.mark.parametrize("notes, model, columns, personal_fields, csv_file", [ -# ("notes.json", "Test Model", {"a": "b"}, ["extra"], "file.csv") -# ]) -# def test_calls_correctly(self, notes, model, columns, personal_fields, csv_file, nmm_test1): -# nmm_config = [setup_nmm_config(model, columns, personal_fields)] -# csv_config = [setup_csv_fm_config(csv_file, note_model_name=model)] -# -# def assert_csv(config, read_now): -# assert config in csv_config -# assert read_now is False -# -# def assert_nmm(config, read_now): -# assert config in nmm_config -# assert read_now is False -# -# def assert_dpn(config, read_now): -# assert config == notes -# assert read_now is False -# -# with patch.object(FileMapping, "__init__", side_effect=assert_csv), \ -# patch.object(NoteModelMapping, "__init__", side_effect=assert_nmm), \ -# patch.object(NoteModelMapping, "note_model"), \ -# patch.object(DeckPartNotes, "create", side_effect=assert_dpn): -# -# #nmm_mock.return_value = False -# -# source_csv = SourceCsv(setup_source_csv_config( -# notes, -# nmm_config, -# csv_config -# ), read_now=False) -# -# -# # def test_missing_non_required_columns -# -# -# class TestSourceToDeckParts: -# def test_runs_first(self, csv_source_test1, dp_notes_test1, csv_source_test2, dp_notes_test2): -# self.run_s2dp(csv_source_test1, dp_notes_test1) -# self.run_s2dp(csv_source_test2, dp_notes_test2) -# -# @staticmethod -# def run_s2dp(csv_source: SourceCsv, dp_notes: DeckPartNotes): -# def assert_format(notes_data): -# assert notes_data == dp_notes.get_data()[DeckPartNoteKeys.NOTES.value] -# -# with patch.object(DeckPartNotes, "set_data", side_effect=assert_format) as mock_set_data: -# csv_source.source_to_parts() -# assert mock_set_data.call_count == 1 -# -# -# class TestDeckPartsToSource: -# def test_runs_with_no_change(self, csv_source_test1, csv_test1, csv_source_test2, csv_test2): -# -# self.run_dpts(csv_source_test1, csv_test1) -# self.run_dpts(csv_source_test2, csv_test2) -# -# @staticmethod -# def run_dpts(csv_source: SourceCsv, csv_file: CsvFile): -# def assert_format(source_data): -# assert source_data == csv_file.get_data() -# -# with patch.object(SourceFile, "set_data", side_effect=assert_format) as mock_set_data: -# csv_source.parts_to_source() -# assert csv_source.csv_file_mappings[0].data_set_has_changed is False -# -# csv_source.csv_file_mappings[0].data_set_has_changed = True -# csv_source.csv_file_mappings[0].write_file_on_close() -# assert mock_set_data.call_count == 1 -# diff --git a/tests/representation/__init__.py b/tests/representation/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/representation/configuration/__init__.py b/tests/representation/configuration/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/representation/configuration/test_csv_file_mapping.py b/tests/representation/configuration/test_csv_file_mapping.py deleted file mode 100644 index 0f2b75b..0000000 --- a/tests/representation/configuration/test_csv_file_mapping.py +++ /dev/null @@ -1,160 +0,0 @@ -# def setup_csv_fm_config(csv: str, sort_by_columns: List[str] = None, reverse_sort: bool = None, -# note_model_name: str = None, derivatives: List[dict] = None): -# cfm: dict = { -# FILE: csv -# } -# if sort_by_columns is not None: -# cfm.setdefault(SORT_BY_COLUMNS, sort_by_columns) -# if reverse_sort is not None: -# cfm.setdefault(REVERSE_SORT, reverse_sort) -# if note_model_name is not None: -# cfm.setdefault(NOTE_MODEL, note_model_name) -# if derivatives is not None: -# cfm.setdefault(DERIVATIVES, derivatives) -# -# return cfm -# - -# class TestConstructor: -# @pytest.mark.parametrize("read_file_now, note_model_name, csv, sort_by_columns, reverse_sort", [ -# (False, "note_model.json", "first.csv", ["guid"], False), -# (True, "model_model.json", "second.csv", ["guid", "note_model_name"], True), -# (False, "note_model-json", "first.csv", ["guid"], False,) -# ]) -# def test_runs_without_derivatives(self, read_file_now, note_model_name, csv, -# sort_by_columns, reverse_sort): -# get_new_file_manager() -# config = setup_csv_fm_config(csv, sort_by_columns, reverse_sort, note_model_name=note_model_name) -# -# def assert_csv(passed_file, read_now): -# assert passed_file == csv -# assert read_now == read_file_now -# -# with patch.object(FileMappingDerivative, "create_derivative", return_value=None) as mock_derivatives, \ -# patch.object(CsvFile, "create", side_effect=assert_csv) as mock_csv: -# -# csv_fm = FileMapping(config, read_now=read_file_now) -# -# assert isinstance(csv_fm, FileMapping) -# assert csv_fm.reverse_sort == reverse_sort -# assert csv_fm.sort_by_columns == sort_by_columns -# assert csv_fm.note_model_name == note_model_name -# -# assert mock_csv.call_count == 1 -# assert mock_derivatives.call_count == 0 -# -# @pytest.mark.parametrize("derivatives", [ -# [setup_csv_fm_config("test_csv.csv")], -# [setup_csv_fm_config("test_csv.csv"), setup_csv_fm_config("second.csv")], -# [setup_csv_fm_config("a.csv"), setup_csv_fm_config("b.csv"), setup_csv_fm_config("c.csv")], -# [setup_csv_fm_config("a.csv", sort_by_columns=["word", "guid"], reverse_sort=True, note_model_name="d")], -# [setup_csv_fm_config("test_csv.csv", derivatives=[setup_csv_fm_config("der_der.csv")])], -# ]) -# def test_runs_with_derivatives(self, derivatives: list): -# get_new_file_manager() -# config = setup_csv_fm_config("test", [], False, note_model_name="nm", derivatives=derivatives.copy()) -# expected_call_count = len(derivatives) -# -# def assert_der(passed_file, read_now): -# der = derivatives.pop(0) -# assert passed_file == der -# assert read_now is False -# -# with patch.object(FileMappingDerivative, "create_derivative", side_effect=assert_der) as mock_derivatives, \ -# patch.object(CsvFile, "create", return_value=None): -# -# csv_fm = FileMapping(config, read_now=False) -# -# assert mock_derivatives.call_count == len(csv_fm.derivatives) == expected_call_count - - -# def csv_fixture_gen(csv_fix): -# with patch.object(CsvFile, "create_or_get", return_value=csv_fix): -# csv = FileMapping(**setup_csv_fm_config("", note_model_name="Test Model")) -# csv.compile_data() -# return csv -# -# -# @pytest.fixture() -# def csv_file_mapping1(csv_test1): -# return csv_fixture_gen(csv_test1) -# -# -# @pytest.fixture() -# def csv_file_mapping2(csv_test2): -# return csv_fixture_gen(csv_test2) -# -# -# @pytest.fixture() -# def csv_file_mapping3(csv_test3): -# return csv_fixture_gen(csv_test3) -# -# -# @pytest.fixture() -# def csv_file_mapping_split1(csv_test1_split1): -# return csv_fixture_gen(csv_test1_split1) -# -# -# @pytest.fixture() -# def csv_file_mapping_split1(csv_test1_split2): -# return csv_fixture_gen(csv_test1_split2) -# -# -# @pytest.fixture() -# def csv_file_mapping2_missing_guids(csv_test2_missing_guids): -# return csv_fixture_gen(csv_test2_missing_guids) -# -# -# class TestSetRelevantData: -# def test_no_change(self, csv_file_mapping1: FileMapping, csv_file_mapping_split1: FileMapping): -# assert csv_file_mapping1.data_set_has_changed is False -# -# previous_data = csv_file_mapping1.compiled_data.copy() -# csv_file_mapping1.set_relevant_data(csv_file_mapping_split1.compiled_data) -# -# assert previous_data == csv_file_mapping1.compiled_data -# assert csv_file_mapping1.data_set_has_changed is False -# -# def test_change_but_no_extra(self, csv_file_mapping1: FileMapping, csv_file_mapping2: FileMapping): -# assert csv_file_mapping1.data_set_has_changed is False -# assert len(csv_file_mapping1.compiled_data) == 15 -# -# previous_data = copy.deepcopy(csv_file_mapping1.compiled_data) -# csv_file_mapping1.set_relevant_data(csv_file_mapping2.compiled_data) -# -# assert previous_data != csv_file_mapping1.compiled_data -# assert csv_file_mapping1.data_set_has_changed is True -# assert len(csv_file_mapping1.compiled_data) == 15 -# -# def test_change_extra_row(self, csv_file_mapping1: FileMapping, csv_file_mapping3: FileMapping): -# assert csv_file_mapping1.data_set_has_changed is False -# assert len(csv_file_mapping1.compiled_data) == 15 -# -# previous_data = copy.deepcopy(csv_file_mapping1.compiled_data.copy()) -# csv_file_mapping1.set_relevant_data(csv_file_mapping3.compiled_data) -# -# assert previous_data != csv_file_mapping1.compiled_data -# assert csv_file_mapping1.data_set_has_changed is True -# assert len(csv_file_mapping1.compiled_data) == 16 -# -# -# class TestCompileData: -# num = 0 -# -# def get_num(self): -# self.num += 1 -# return self.num -# -# def test_when_missing_guids(self, csv_file_mapping2_missing_guids: FileMapping): -# with patch("brain_brew.representation.configuration.csv_file_mapping.generate_anki_guid", wraps=self.get_num) as mock_guid: -# -# csv_file_mapping2_missing_guids.compile_data() -# -# assert csv_file_mapping2_missing_guids.data_set_has_changed is True -# assert mock_guid.call_count == 9 -# assert list(csv_file_mapping2_missing_guids.compiled_data.keys()) == list(range(1, 10)) - -# Tests still to do: -# -# Top level needs a NoteModel, others do not - diff --git a/tests/representation/configuration/test_note_model_mapping.py b/tests/representation/configuration/test_note_model_mapping.py deleted file mode 100644 index da4a2d2..0000000 --- a/tests/representation/configuration/test_note_model_mapping.py +++ /dev/null @@ -1,146 +0,0 @@ -# from unittest.mock import patch -# -# import pytest -# -# from brain_brew.representation.configuration.note_model_mapping import NoteModelMapping, FieldMapping -# from brain_brew.representation.generic.csv_file import CsvFile -# from tests.test_file_manager import get_new_file_manager -# -# -# @pytest.fixture(autouse=True) -# def run_around_tests(): -# get_new_file_manager() -# yield -# -# -# @pytest.fixture() -# def nmm_test1_repr() -> NoteModelMapping.Representation: -# return NoteModelMapping.Representation( -# "Test Model", -# { -# "guid": "guid", -# "tags": "tags", -# -# "english": "word", -# "danish": "otherword" -# }, -# [] -# ) -# -# -# @pytest.fixture() -# def nmm_test2_repr() -> NoteModelMapping.Representation: -# return NoteModelMapping.Representation( -# "Test Model", -# { -# "guid": "guid", -# "tags": "tags", -# -# "english": "word", -# "danish": "otherword" -# }, -# ["extra", "morph_focus"] -# ) -# -# -# @pytest.fixture() -# def nmm_test1(nmm_test1_repr) -> NoteModelMapping: -# return NoteModelMapping.from_repr(nmm_test1_repr) -# -# -# @pytest.fixture() -# def nmm_test2(nmm_test2_repr) -> NoteModelMapping: -# return NoteModelMapping.from_repr(nmm_test2_repr) -# -# -# class TestInit: -# def test_runs(self): -# nmm = NoteModelMapping.from_repr(NoteModelMapping.Representation("test", {}, [])) -# assert isinstance(nmm, NoteModelMapping) -# -# @pytest.mark.parametrize("read_file_now, note_model, personal_fields, columns", [ -# (False, "note_model.json", ["x"], {"guid": "guid", "tags": "tags", "english": "word", "danish": "otherword"}), -# (True, "model_model", [], {"guid": "guid", "tags": "tags"}), -# (False, "note_model-json", ["x", "y", "z"], {"guid": "guid", "tags": "tags", "english": "word", "danish": "otherword"}) -# ]) -# def test_values(self, read_file_now, note_model, personal_fields, columns): -# config = setup_nmm_config(note_model, columns, personal_fields) -# -# def assert_dp_note_model(passed_file, read_now): -# assert passed_file == note_model -# assert read_now == read_file_now -# -# with patch.object(DeckPartNoteModel, "create", side_effect=assert_dp_note_model) as mock_nm: -# -# nmm = NoteModelMapping(config, read_now=read_file_now) -# -# assert isinstance(nmm, NoteModelMapping) -# assert len(nmm.columns) == len(columns) -# assert len(nmm.personal_fields) == len(personal_fields) -# -# assert mock_nm.call_count == 1 -# -# -# class TestVerifyContents: -# pass # TODO -# -# -# class TestCsvRowNoteFieldConversion: -# @staticmethod -# def get_csv_row(): return { -# "guid": "AAAA", -# "tags": "nice card", -# -# "english": "what", -# "danish": "hvad" -# } -# -# @staticmethod -# def get_note_field(): return{ -# "guid": "AAAA", -# "tags": "nice card", -# -# "word": "what", -# "otherword": "hvad", -# "extra": False, -# "morph_focus": False -# } -# -# def test_csv_row_map_to_note_fields(self, nmm_test_with_personal_fields1): -# assert nmm_test_with_personal_fields1.csv_row_map_to_note_fields(self.get_csv_row()) == self.get_note_field() -# -# def test_note_fields_map_to_csv_row(self, nmm_test_with_personal_fields1): -# assert nmm_test_with_personal_fields1.note_fields_map_to_csv_row(self.get_note_field()) == self.get_csv_row() -# -# -# class TestGetRelevantData: -# def test_data_correct(self, nmm_test_with_personal_fields1: NoteModelMapping, csv_test1: CsvFile): -# expected_relevant_columns = ["guid", "english", "danish", "tags"] -# data = csv_test1.get_data() -# -# for row in data: -# relevant_data = nmm_test_with_personal_fields1.get_relevant_data(row) -# assert len(relevant_data) == 4 -# assert list(relevant_data.keys()) == expected_relevant_columns -# -# def test_data_missing_columns(self, nmm_test_with_personal_fields1: NoteModelMapping, csv_test1: CsvFile): -# row_missing = { -# "guid": "test", -# "english": "test" -# } -# with pytest.raises(Exception) as e: -# relevant_data = nmm_test_with_personal_fields1.get_relevant_data(row_missing) -# -# errors = e.value.args[0] -# assert len(errors) == 2 -# assert isinstance(errors[0], KeyError) -# assert isinstance(errors[1], KeyError) -# assert errors[0].args[0] == "Missing column tags" -# assert errors[1].args[0] == "Missing column danish" -# -# -# class TestFieldMapping: -# def test_init(self): -# fm = FieldMapping(FieldMapping.FieldMappingType.COLUMN, "Csv_Row", "note_model_field") -# assert isinstance(fm, FieldMapping) -# assert (fm.field_name, fm.value) == ("csv_row", "note_model_field") diff --git a/tests/representation/generic/__init__.py b/tests/representation/generic/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/representation/generic/test_csv_file.py b/tests/representation/generic/test_csv_file.py deleted file mode 100644 index a6bd134..0000000 --- a/tests/representation/generic/test_csv_file.py +++ /dev/null @@ -1,117 +0,0 @@ -import pytest - -from brain_brew.representation.generic.csv_file import CsvFile -from tests.test_file_manager import get_new_file_manager -from tests.test_files import TestFiles - -get_new_file_manager() - - -@pytest.fixture() -def csv_test1(): - csv = CsvFile(TestFiles.CsvFiles.TEST1) - csv.read_file() - return csv - - -@pytest.fixture() -def tsv_test1(): - tsv = CsvFile(TestFiles.TsvFiles.TEST1, delimiter='\t') - tsv.read_file() - return tsv - - -@pytest.fixture() -def csv_test1_split1(): - csv = CsvFile(TestFiles.CsvFiles.TEST1_SPLIT1) - csv.read_file() - return csv - - -@pytest.fixture() -def csv_test1_split2(): - csv = CsvFile(TestFiles.CsvFiles.TEST1_SPLIT2) - csv.read_file() - return csv - - -@pytest.fixture() -def csv_test2(): - csv = CsvFile(TestFiles.CsvFiles.TEST2) - csv.read_file() - return csv - - -@pytest.fixture() -def csv_test3(): - csv = CsvFile(TestFiles.CsvFiles.TEST3) - csv.read_file() - return csv - - -@pytest.fixture() -def csv_test2_missing_guids(): - csv = CsvFile(TestFiles.CsvFiles.TEST2_MISSING_GUIDS) - csv.read_file() - return csv - - -@pytest.fixture() -def temp_csv_test1(tmpdir, csv_test1) -> CsvFile: - file = tmpdir.mkdir("json").join("file.csv") - file.write("blank") - - csv = CsvFile.create_or_get(file.strpath) - csv.read_file() - return csv - - -class TestConstructor: - def test_runs(self, csv_test1): - assert isinstance(csv_test1, CsvFile) - assert csv_test1.file_location == TestFiles.CsvFiles.TEST1 - assert "guid" in csv_test1.column_headers - - -def test_to_filename_csv(): - assert "read-this-file.csv" == CsvFile.to_filename_csv("read-this-file") - assert "read-this-file.csv" == CsvFile.to_filename_csv("read-this-file.csv") - assert "read-this-file.tsv" == CsvFile.to_filename_csv("read-this-file.tsv") - - -class TestWriteFile: - def test_runs(self, temp_csv_test1: CsvFile, csv_test1: CsvFile): - temp_csv_test1.set_data(csv_test1.get_data()) - temp_csv_test1.write_file() - temp_csv_test1.read_file() - - assert temp_csv_test1.get_data() == csv_test1.get_data() - - def test_tsv_same_data(self, temp_csv_test1: CsvFile, tsv_test1: CsvFile): - temp_csv_test1.set_data(tsv_test1.get_data()) - temp_csv_test1.write_file() - temp_csv_test1.read_file() - - assert temp_csv_test1.get_data() == tsv_test1.get_data() - - -class TestSortData: - @pytest.mark.parametrize("columns, reverse, result_column, expected_results", [ - (["guid"], False, "guid", [(0, "AAAA"), (1, "BBBB"), (2, "CCCC"), (14, "OOOO")]), - (["guid"], True, "guid", [(14, "AAAA"), (13, "BBBB"), (12, "CCCC"), (0, "OOOO")]), - (["english"], False, "english", [(0, "banana"), (1, "bird"), (2, "cat"), (14, "you")]), - (["english"], True, "english", [(14, "banana"), (13, "bird"), (12, "cat"), (0, "you")]), - (["tags"], False, "tags", [(0, "besttag"), (1, "funny"), (2, "tag2 tag3"), (13, ""), (14, "")]), - (["esperanto", "english"], False, "esperanto", [(0, "banano"), (1, "birdo"), (6, "vi"), (14, "")]), - (["esperanto", "guid"], False, "guid", [(7, "BBBB"), (14, "LLLL")]), - ]) - def test_sort(self, csv_test1: CsvFile, columns, reverse, result_column, expected_results): - csv_test1.sort_data(columns, reverse, case_insensitive_sort=True) - - sorted_data = csv_test1.get_data() - - for result in expected_results: - assert sorted_data[result[0]][result_column] == result[1] - - def test_insensitive(self): - pass diff --git a/tests/representation/generic/test_media_file.py b/tests/representation/generic/test_media_file.py deleted file mode 100644 index 9711021..0000000 --- a/tests/representation/generic/test_media_file.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest - -from brain_brew.representation.generic.media_file import MediaFile - - -@pytest.fixture() -def media_file_test1() -> MediaFile: - return MediaFile("place/loc/file.txt") - - -class TestConstructor: - def test_without_override(self): - loc = "place/loc/file.txt" - - media_file = MediaFile(loc) - - assert isinstance(media_file, MediaFile) - assert media_file.file_path == loc - assert media_file.filename == "file.txt" - - -class TestCopy: - def test_copies_file(self, tmpdir): - source_dir = tmpdir.mkdir("source") - source = source_dir.join("test.txt") - source.write("test content") - assert len(source_dir.listdir()) == 1 - - target_dir = tmpdir.mkdir("target") - target = target_dir.join("test.txt") - assert len(target_dir.listdir()) == 0 - - media_file = MediaFile(str(source)) - media_file.copy_self_to_target(str(target)) - - assert len(target_dir.listdir()) == len(source_dir.listdir()) == 1 - assert target.read() == "test content" diff --git a/tests/representation/json/__init__.py b/tests/representation/json/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/representation/json/test_crowd_anki_export.py b/tests/representation/json/test_crowd_anki_export.py deleted file mode 100644 index 01c0c22..0000000 --- a/tests/representation/json/test_crowd_anki_export.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest - -from brain_brew.representation.json.crowd_anki_export import CrowdAnkiExport -from tests.test_files import TestFiles - - -class TestConstructor: - @pytest.mark.parametrize("export_name", [ - TestFiles.CrowdAnkiExport.TEST1_FOLDER, - TestFiles.CrowdAnkiExport.TEST1_FOLDER_WITHOUT_SLASH - ]) - def test_runs(self, export_name): - file = CrowdAnkiExport(export_name) - - assert isinstance(file, CrowdAnkiExport) - assert file.folder_location == TestFiles.CrowdAnkiExport.TEST1_FOLDER - assert file.json_file_location == TestFiles.CrowdAnkiExport.TEST1_JSON - assert len(file.json_data.data.keys()) == 13 - - -class TestFindJsonFileInFolder: - # def test_no_json_file(self, tmpdir): - # directory = tmpdir.mkdir("test") - # - # with pytest.raises(FileNotFoundError): - # file = CrowdAnkiExport(directory.strpath) - - def test_too_many_json_files(self, tmpdir): - directory = tmpdir.mkdir("test") - file1, file2 = directory.join("file1.json"), directory.join("file2.json") - file1.write("{}") - file2.write("{}") - - with pytest.raises(FileExistsError): - file = CrowdAnkiExport(directory.strpath) - - -@pytest.fixture() -def ca_export_test1() -> CrowdAnkiExport: - return CrowdAnkiExport.create_or_get(TestFiles.CrowdAnkiExport.TEST1_FOLDER) - - -@pytest.fixture() -def temp_ca_export_file(tmpdir) -> CrowdAnkiExport: - folder = tmpdir.mkdir("ca_export") - file = folder.join("file.json") - file.write("{}") - - return CrowdAnkiExport(folder.strpath) diff --git a/tests/representation/yaml/__init__.py b/tests/representation/yaml/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/representation/yaml/test_note_model_repr.py b/tests/representation/yaml/test_note_model_repr.py deleted file mode 100644 index a5ae878..0000000 --- a/tests/representation/yaml/test_note_model_repr.py +++ /dev/null @@ -1,128 +0,0 @@ -import pytest - -from brain_brew.representation.json.json_file import JsonFile -from brain_brew.representation.yaml.note_model import NoteModel -from brain_brew.representation.yaml.note_model_field import Field -from brain_brew.representation.yaml.note_model_template import Template -from brain_brew.representation.yaml.yaml_object import YamlObject -from tests.test_files import TestFiles - - -# CrowdAnki Files -------------------------------------------------------------------------- -from tests.test_helpers import debug_write_part_to_file - - -@pytest.fixture -def ca_nm_data_word(): - return JsonFile.read_file(TestFiles.CrowdAnkiNoteModels.LL_WORD) - - -@pytest.fixture -def ca_nm_word(ca_nm_data_word) -> NoteModel: - return NoteModel.from_crowdanki(ca_nm_data_word) - - -@pytest.fixture -def ca_nm_data_word_required_only(): - return JsonFile.read_file(TestFiles.CrowdAnkiNoteModels.LL_WORD_ONLY_REQUIRED) - - -@pytest.fixture -def ca_nm_word_required_only(ca_nm_data_word_required_only) -> NoteModel: - return NoteModel.from_crowdanki(ca_nm_data_word_required_only) - - -@pytest.fixture -def ca_nm_data_word_no_defaults(): - return JsonFile.read_file(TestFiles.CrowdAnkiNoteModels.LL_WORD_NO_DEFAULTS) - - -@pytest.fixture -def ca_nm_word_no_defaults(ca_nm_data_word_no_defaults) -> NoteModel: - return NoteModel.from_crowdanki(ca_nm_data_word_no_defaults) - - -# Yaml Files -------------------------------------------------------------------------- -@pytest.fixture -def nm_data_word_required_only(): - return YamlObject.read_to_dict(TestFiles.NoteModels.LL_WORD_ONLY_REQUIRED) - - -@pytest.fixture -def nm_data_word_no_defaults(): - return YamlObject.read_to_dict(TestFiles.NoteModels.LL_WORD_NO_DEFAULTS) - - -class TestCrowdAnkiNoteModel: - class TestConstructor: - def test_normal(self, ca_nm_word): - model = ca_nm_word - assert isinstance(model, NoteModel) - - assert model.name == "LL Word" - assert isinstance(model.fields, list) - assert len(model.fields) == 7 - assert all([isinstance(field, Field) for field in model.fields]) - - assert isinstance(model.templates, list) - assert len(model.templates) == 7 - assert all([isinstance(template, Template) for template in model.templates]) - - def test_only_required(self, ca_nm_word_required_only): - model = ca_nm_word_required_only - assert isinstance(model, NoteModel) - - def test_manual_construction(self): - model = NoteModel( - "name", - "23094149+8124+91284+12984", - "css is garbage", - [], - [Field( - "field1" - )], - [Template( - "template1", - "{{Question}}", - "{{Answer}}" - )] - ) - - assert isinstance(model, NoteModel) - - class TestEncodeAsCrowdAnki: - def test_normal(self, ca_nm_word, ca_nm_data_word): - model = ca_nm_word - - encoded = model.encode_as_crowdanki() - # JsonFile.write_file(TestFiles.CrowdAnkiNoteModels.LL_WORD, encoded) - - assert encoded == ca_nm_data_word - - def test_only_required_uses_defaults(self, ca_nm_word_required_only, - ca_nm_data_word, ca_nm_data_word_required_only): - model = ca_nm_word_required_only - - encoded = model.encode_as_crowdanki() - - assert encoded != ca_nm_data_word_required_only - assert encoded == ca_nm_data_word - - class TestEncodeAsDeckPart: - def test_normal(self, ca_nm_word, ca_nm_data_word, ca_nm_data_word_required_only, nm_data_word_required_only): - model = ca_nm_word - - encoded = model.encode() - - assert encoded != ca_nm_data_word - assert encoded != ca_nm_data_word_required_only - assert encoded == nm_data_word_required_only - - def test_only_required_uses_defaults(self, ca_nm_word_no_defaults, ca_nm_data_word_no_defaults, nm_data_word_no_defaults): - model = ca_nm_word_no_defaults - - encoded = model.encode() - - - assert encoded != ca_nm_data_word_no_defaults - assert encoded == nm_data_word_no_defaults diff --git a/tests/representation/yaml/test_note_repr.py b/tests/representation/yaml/test_note_repr.py deleted file mode 100644 index 1a704f5..0000000 --- a/tests/representation/yaml/test_note_repr.py +++ /dev/null @@ -1,392 +0,0 @@ -from textwrap import dedent -from typing import List, Set - -import pytest - -from brain_brew.representation.yaml.notes import Note, NoteGrouping, Notes, \ - NOTES, NOTE_GROUPINGS, FIELDS, GUID, NOTE_MODEL, TAGS, FLAGS - -working_notes = { - "test1": {FIELDS: ['first'], GUID: "12345", NOTE_MODEL: "model_name", TAGS: ['noun', 'other']}, - "test2": {FIELDS: ['english', 'german'], GUID: "sdfhfghsvsdv", NOTE_MODEL: "LL Test", TAGS: ['marked']}, - "no_note_model": {FIELDS: ['first'], GUID: "12345", TAGS: ['noun', 'other']}, - "no_note_model2": {FIELDS: ['second'], GUID: "67890", TAGS: ['noun', 'other']}, - "no_tags1": {FIELDS: ['first'], GUID: "12345", NOTE_MODEL: "model_name"}, - "no_tags2": {FIELDS: ['first'], GUID: "12345", NOTE_MODEL: "model_name", TAGS: []}, - "no_model_or_tags": {FIELDS: ['first'], GUID: "12345"}, - "test1_with_default_flags": {FIELDS: ['first'], GUID: "12345", NOTE_MODEL: "model_name", TAGS: ['noun', 'other'], FLAGS: 0}, - "test1_with_flags": {FIELDS: ['first'], GUID: "12345", NOTE_MODEL: "model_name", TAGS: ['noun', 'other'], FLAGS: 1}, -} - -working_note_groupings = { - "nothing_grouped": {NOTES: [working_notes["test1"], working_notes["test2"]]}, - "note_model_grouped": {NOTES: [working_notes["no_note_model"], working_notes["no_note_model2"]], NOTE_MODEL: "model_name"}, - "note_model_grouped2": {NOTES: [working_notes["no_note_model"], working_notes["no_note_model2"]], NOTE_MODEL: "different_model"}, - "tags_grouped": {NOTES: [working_notes["no_tags1"], working_notes["no_tags2"]], TAGS: ["noun", "other"]}, - "tags_grouped_as_addition": {NOTES: [working_notes["test1"], working_notes["test2"]], TAGS: ["test", "recent"]}, - "model_and_tags_grouped": {NOTES: [working_notes["no_model_or_tags"], working_notes["no_model_or_tags"]], NOTE_MODEL: "model_name", TAGS: ["noun", "other"]} -} - -working_dpns = { - "one_group": {NOTE_GROUPINGS: [working_note_groupings["nothing_grouped"]]}, - "two_groups_two_models": {NOTE_GROUPINGS: [working_note_groupings["nothing_grouped"], working_note_groupings["note_model_grouped"]]}, - "two_groups_three_models": {NOTE_GROUPINGS: [working_note_groupings["nothing_grouped"], working_note_groupings["note_model_grouped2"]]}, -} - - -@pytest.fixture(params=working_notes.values()) -def note_fixtures(request): - return Note.from_dict(request.param) - - -@pytest.fixture(params=working_note_groupings.values()) -def note_grouping_fixtures(request): - return NoteGrouping.from_dict(request.param) - - -class TestConstructor: - class TestNote: - @pytest.mark.parametrize("fields, guid, note_model, tags, flags, media", [ - ([], "", "", [], 0, {}), - (None, None, None, None, None, None), - (["test", "blah", "whatever"], "1234567890x", "model_name", ["noun"], 1, {}), - (["test", "blah", ""], "1234567890x", "model_name", ["noun"], 2, {"animal.jpg"}), - ]) - def test_constructor(self, fields: List[str], guid: str, note_model: str, tags: List[str], flags: int, media: Set[str]): - note = Note(fields=fields, guid=guid, note_model=note_model, tags=tags, flags=flags) - - assert isinstance(note, Note) - assert note.fields == fields - assert note.guid == guid - assert note.note_model == note_model - assert note.tags == tags - assert note.flags == flags - # assert note.media_references == media - - def test_from_dict(self, note_fixtures): - assert isinstance(note_fixtures, Note) - - class TestNoteGrouping: - def test_constructor(self): - note_grouping = NoteGrouping(notes=[Note.from_dict(working_notes["test1"])], note_model=None, tags=None) - - assert isinstance(note_grouping, NoteGrouping) - assert isinstance(note_grouping.notes, List) - - def test_from_dict(self, note_grouping_fixtures): - assert isinstance(note_grouping_fixtures, NoteGrouping) - - class TestDeckPartNote: - def test_constructor(self): - dpn = Notes(note_groupings=[NoteGrouping.from_dict(working_note_groupings["nothing_grouped"])]) - assert isinstance(dpn, Notes) - - def test_from_dict(self): - dpn = Notes.from_dict({NOTE_GROUPINGS: [working_note_groupings["nothing_grouped"]]}) - assert isinstance(dpn, Notes) - - -class TestDumpToYaml: - @staticmethod - def _make_temp_file(tmpdir): - folder = tmpdir.mkdir("yaml_files") - file = folder.join("test.yaml") - file.write("test") - return file - - class TestNote: - @staticmethod - def _assert_dump_to_yaml(tmpdir, ystring, note_name): - file = TestDumpToYaml._make_temp_file(tmpdir) - - note = Note.from_dict(working_notes[note_name]) - note.dump_to_yaml(str(file)) - - assert file.read() == ystring - - def test_all1(self, tmpdir): - ystring = dedent(f'''\ - {FIELDS}: - - first - {GUID}: '12345' - {NOTE_MODEL}: model_name - {TAGS}: - - noun - - other - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, "test1") - - def test_all2(self, tmpdir): - ystring = dedent(f'''\ - {FIELDS}: - - english - - german - {GUID}: sdfhfghsvsdv - {NOTE_MODEL}: LL Test - {TAGS}: - - marked - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, "test2") - - def test_no_note_model(self, tmpdir): - ystring = dedent(f'''\ - {FIELDS}: - - first - {GUID}: '12345' - {TAGS}: - - noun - - other - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, "no_note_model") - - def test_no_tags(self, tmpdir): - for num, note in enumerate(["no_tags1", "no_tags2"]): - ystring = dedent(f'''\ - {FIELDS}: - - first - {GUID}: '12345' - {NOTE_MODEL}: model_name - ''') - - self._assert_dump_to_yaml(tmpdir.mkdir(str(num)), ystring, note) - - def test_with_flags(self, tmpdir): - ystring = dedent(f'''\ - {FIELDS}: - - first - {GUID}: '12345' - {FLAGS}: 1 - {NOTE_MODEL}: model_name - {TAGS}: - - noun - - other - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, "test1_with_flags") - - def test_with_default_flags(self, tmpdir): - ystring = dedent(f'''\ - {FIELDS}: - - first - {GUID}: '12345' - {NOTE_MODEL}: model_name - {TAGS}: - - noun - - other - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, "test1_with_default_flags") - - class TestNoteGrouping: - @staticmethod - def _assert_dump_to_yaml(tmpdir, ystring, note_grouping_name): - file = TestDumpToYaml._make_temp_file(tmpdir) - - note = NoteGrouping.from_dict(working_note_groupings[note_grouping_name]) - note.dump_to_yaml(str(file)) - - assert file.read() == ystring - - def test_nothing_grouped(self, tmpdir): - ystring = dedent(f'''\ - {NOTES}: - - {FIELDS}: - - first - {GUID}: '12345' - {NOTE_MODEL}: model_name - {TAGS}: - - noun - - other - - {FIELDS}: - - english - - german - {GUID}: sdfhfghsvsdv - {NOTE_MODEL}: LL Test - {TAGS}: - - marked - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, "nothing_grouped") - - def test_note_model_grouped(self, tmpdir): - ystring = dedent(f'''\ - {NOTE_MODEL}: model_name - {NOTES}: - - {FIELDS}: - - first - {GUID}: '12345' - {TAGS}: - - noun - - other - - {FIELDS}: - - second - {GUID}: '67890' - {TAGS}: - - noun - - other - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, "note_model_grouped") - - def test_note_tags_grouped(self, tmpdir): - ystring = dedent(f'''\ - {TAGS}: - - noun - - other - {NOTES}: - - {FIELDS}: - - first - {GUID}: '12345' - {NOTE_MODEL}: model_name - - {FIELDS}: - - first - {GUID}: '12345' - {NOTE_MODEL}: model_name - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, "tags_grouped") - - def test_note_model_and_tags_grouped(self, tmpdir): - ystring = dedent(f'''\ - {NOTE_MODEL}: model_name - {TAGS}: - - noun - - other - {NOTES}: - - {FIELDS}: - - first - {GUID}: '12345' - - {FIELDS}: - - first - {GUID}: '12345' - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, "model_and_tags_grouped") - - class TestDeckPartNotes: - @staticmethod - def _assert_dump_to_yaml(tmpdir, ystring, groups: list): - file = TestDumpToYaml._make_temp_file(tmpdir) - - note = Notes.from_dict({NOTE_GROUPINGS: [working_note_groupings[name] for name in groups]}) - note.dump_to_yaml(str(file)) - - assert file.read() == ystring - - def test_two_groupings(self, tmpdir): - ystring = dedent(f'''\ - {NOTE_GROUPINGS}: - - {NOTE_MODEL}: model_name - {TAGS}: - - noun - - other - {NOTES}: - - {FIELDS}: - - first - {GUID}: '12345' - - {FIELDS}: - - first - {GUID}: '12345' - - {NOTES}: - - {FIELDS}: - - first - {GUID}: '12345' - {NOTE_MODEL}: model_name - {TAGS}: - - noun - - other - - {FIELDS}: - - english - - german - {GUID}: sdfhfghsvsdv - {NOTE_MODEL}: LL Test - {TAGS}: - - marked - ''') - - self._assert_dump_to_yaml(tmpdir, ystring, ["model_and_tags_grouped", "nothing_grouped"]) - - -class TestFunctionality: - class TestGetMediaReferences: - class TestNote: - @pytest.mark.parametrize("fields, expected_count", [ - ([], 0), - (["nothing", "empty", "can't find nothing here"], 0), - (["", "empty", "can't find nothing here"], 1), - (["", "", ""], 1), - (["", "", ""], 3), - (["", "[sound:test.mp3]", "[sound:test.mp3]"], 2), - ]) - def test_all(self, fields, expected_count): - note = Note(fields=fields, note_model=None, guid="", tags=None, flags=0) - media_found = note.get_all_media_references() - assert isinstance(media_found, Set) - assert len(media_found) == expected_count - - class TestGetAllNoteModels: - class TestNoteGrouping: - def test_nothing_grouped(self): - group = NoteGrouping.from_dict(working_note_groupings["nothing_grouped"]) - models = group.get_all_known_note_model_names() - assert models == {'LL Test', 'model_name'} - - def test_grouped(self): - group = NoteGrouping.from_dict(working_note_groupings["note_model_grouped"]) - models = group.get_all_known_note_model_names() - assert models == {'model_name'} - - class TestDeckPartNotes: - def test_two_groups_two_models(self): - dpn = Notes.from_dict(working_dpns["two_groups_two_models"]) - models = dpn.get_all_known_note_model_names() - assert models == {'LL Test', 'model_name'} - - def test_two_groups_three_models(self): - dpn = Notes.from_dict(working_dpns["two_groups_three_models"]) - models = dpn.get_all_known_note_model_names() - assert models == {'LL Test', 'model_name', 'different_model'} - - # class TestGetAllNotes: - # class TestNoteGrouping: - # def test_nothing_grouped(self): - # group = NoteGrouping.from_dict(working_note_groupings["nothing_grouped"]) - # notes = group.get_all_notes_copy([], False) - # assert len(notes) == 2 - # - # def test_model_grouped(self): - # group = NoteGrouping.from_dict(working_note_groupings["note_model_grouped"]) - # assert group.note_model == "model_name" - # assert all([note.note_model is None for note in group.notes]) - # - # notes = group.get_all_notes_copy() - # assert {note.note_model for note in notes} == {"model_name"} - # - # def test_tags_grouped(self): - # group = NoteGrouping.from_dict(working_note_groupings["tags_grouped"]) - # assert group.tags == ["noun", "other"] - # assert all([note.tags is None or note.tags == [] for note in group.notes]) - # - # notes = group.get_all_notes_copy() - # assert all([note.tags == ["noun", "other"] for note in notes]) - # - # def test_tags_grouped_as_addition(self): - # group = NoteGrouping.from_dict(working_note_groupings["tags_grouped_as_addition"]) - # assert group.tags == ["test", "recent"] - # assert all([note.tags is not None for note in group.notes]) - # - # notes = group.get_all_notes_copy() - # assert notes[0].tags == ['noun', 'other', "test", "recent"] - # assert notes[1].tags == ['marked', "test", "recent"] - # - # def test_no_tags(self): - # group = NoteGrouping.from_dict(working_note_groupings["tags_grouped"]) - # group.tags = None - # assert all([note.tags is None or note.tags == [] for note in group.notes]) - # - # notes = group.get_all_notes_copy() - # assert all([note.tags == [] for note in notes]) - # diff --git a/tests/test_argument_reader.py b/tests/test_argument_reader.py deleted file mode 100644 index 089e77e..0000000 --- a/tests/test_argument_reader.py +++ /dev/null @@ -1,51 +0,0 @@ -from argparse import ArgumentParser, ArgumentError -from unittest.mock import patch - -import pytest - -from brain_brew.commands.argument_reader import BBArgumentReader, Commands - - -@pytest.fixture() -def arg_reader_test1(): - return BBArgumentReader(test_mode=True) - - -def test_constructor(arg_reader_test1): - assert isinstance(arg_reader_test1, BBArgumentReader) - assert isinstance(arg_reader_test1, ArgumentParser) - - -class TestArguments: - class CommandRun: - @pytest.mark.parametrize("arguments", [ - ([Commands.RUN_RECIPE.value]), - ([Commands.RUN_RECIPE.value, ""]), - ]) - def test_broken_arguments(self, arg_reader_test1, arguments): - def raise_exit(message): - raise SystemExit - - with pytest.raises(SystemExit): - with patch.object(BBArgumentReader, "error", side_effect=raise_exit): - arg_reader_test1.get_parsed(arguments) - - @pytest.mark.parametrize("arguments, recipe, verify_only", [ - ([Commands.RUN_RECIPE.value, "test_recipe.yaml"], "test_recipe.yaml", False), - ([Commands.RUN_RECIPE.value, "test_recipe.yaml", "--verify"], "test_recipe.yaml", True), - ([Commands.RUN_RECIPE.value, "test_recipe.yaml", "-v"], "test_recipe.yaml", True), - ]) - def test_correct_arguments(self, arg_reader_test1, arguments, recipe, verify_only): - parsed_args = arg_reader_test1.parse_args(arguments) - - assert parsed_args.recipe == recipe - assert parsed_args.verify_only == verify_only - - class CommandInit: - @pytest.mark.parametrize("arguments, location", [ - (["init", "crowdankifolder72"], "crowdankifolder72"), - ]) - def test_correct_arguments(self, arg_reader_test1, arguments, location): - parsed_args = arg_reader_test1.parse_args(arguments) - - assert parsed_args.crowdanki_folder == location diff --git a/tests/test_builder.py b/tests/test_builder.py deleted file mode 100644 index 6eb4445..0000000 --- a/tests/test_builder.py +++ /dev/null @@ -1,12 +0,0 @@ -# class TestConstructor: -# def test_runs(self): -# with patch.object(CsvsGenerate, "__init__", return_value=None) as mock_csv_tr, \ -# patch.object(DeckPartHolder, "from_part_pool", return_value=Mock()), \ -# patch.object(CsvFile, "create_or_get", return_value=Mock()): -# -# data = YamlObject.read_to_dict(TestFiles.BuildConfig.ONE_OF_EACH_TYPE) -# builder = TopLevelRecipeBuilder.from_list(data) -# builder.execute() -# -# assert len(builder.tasks) == 1 -# assert mock_csv_tr.call_count == 1 diff --git a/tests/test_file_manager.py b/tests/test_file_manager.py deleted file mode 100644 index e69d0ad..0000000 --- a/tests/test_file_manager.py +++ /dev/null @@ -1,32 +0,0 @@ -from brain_brew.configuration.file_manager import FileManager - - -def get_new_file_manager(): - FileManager.clear_instance() - return FileManager() - - -# class TestSingletonConstructor: -# def test_runs(self, global_config): -# fm = get_new_file_manager() -# assert isinstance(fm, FileManager) -# -# def test_returns_existing_singleton(self): -# fm = get_new_file_manager() -# fm.known_files_dict = {'test': None} -# fm2 = FileManager.get_instance() -# -# assert fm2.known_files_dict == {'test': None} -# assert fm2 == fm -# -# def test_raises_error(self): -# with pytest.raises(Exception): -# FileManager() -# FileManager() -# -# -# class TestFindMediaFiles: -# def test_finds(self): -# fm = get_new_file_manager() -# -# assert len(fm.known_media_files_dict) == 2 diff --git a/tests/test_files.py b/tests/test_files.py deleted file mode 100644 index a01ef63..0000000 --- a/tests/test_files.py +++ /dev/null @@ -1,72 +0,0 @@ - - -class TestFiles: - class Headers: - LOC = "tests/test_files/deck_parts/headers/" - - FIRST = "default header" - FIRST_COMPLETE = "default-header.json" - - class NoteFiles: - LOC = "tests/test_files/deck_parts/" - - TEST1_NO_GROUPING_OR_SHARED_TAGS = "csvtonotes1_withnogroupingorsharedtags.json" - TEST1_WITH_GROUPING = "csvtonotes1_withgrouping.json" - TEST1_WITH_SHARED_TAGS = "csvtonotes1_withsharedtags.json" - TEST1_WITH_SHARED_TAGS_EMPTY_AND_GROUPING = "csvtonotes1_withsharedtagsandgrouping_butnothingtogroup.json" - TEST2_WITH_SHARED_TAGS_AND_GROUPING = "csvtonotes2_withsharedtagsandgrouping.json" - - class CrowdAnkiNoteModels: - LOC = "tests/test_files/deck_parts/note_models/" - - TEST = "Test Model" - - LL_WORD = LOC + "LL Word" - - LL_WORD_ONLY_REQUIRED = LOC + "LL Word Only Required" - - LL_WORD_NO_DEFAULTS = LOC + "LL Word No Defaults" - - class NoteModels: - LOC = "tests/test_files/deck_parts/yaml/note_models/" - - LL_WORD = LOC + "LL-Word.yaml" - - LL_WORD_ONLY_REQUIRED = LOC + "LL-Word-Only-Required.yaml" - - LL_WORD_NO_DEFAULTS = LOC + "LL-Word-No-Defaults.yaml" - - class CsvFiles: - LOC = "tests/test_files/csv/" - - TEST1 = LOC + "test1.csv" - TEST1_SPLIT1 = LOC + "test1_split1.csv" - TEST1_SPLIT2 = LOC + "test1_split2.csv" - TEST2 = LOC + "test2.csv" - TEST2_MISSING_GUIDS = LOC + "test2_missing_guids.csv" - TEST3 = LOC + "test3.csv" - - class TsvFiles: - LOC = "tests/test_files/tsv/" - - TEST1 = LOC + "test1.tsv" - - class CrowdAnkiExport: - LOC = "tests/test_files/crowd_anki/" - - TEST1_FOLDER = LOC + "crowdanki_example_1/" - TEST1_FOLDER_WITHOUT_SLASH = LOC + "crowdanki_example_1" - TEST1_JSON = TEST1_FOLDER + "deck.json" - - class BuildConfig: - LOC = "tests/test_files/build_files/" - - ONE_OF_EACH_TYPE = LOC + "builder1.yaml" - - class MediaFiles: - LOC = "tests/test_files/media_files/" - - class YamlNotes: - LOC = "tests/test_files/yaml/notes/" - - TEST1 = LOC + "note1.yaml" diff --git a/tests/test_files/build_files/builder1.yaml b/tests/test_files/build_files/builder1.yaml deleted file mode 100644 index c45cda5..0000000 --- a/tests/test_files/build_files/builder1.yaml +++ /dev/null @@ -1,42 +0,0 @@ - -- generate_csv_collection: - notes: test_from_CA - - note_model_mappings: - - note_models: - - LL Word - - LL Verb - - LL Noun - columns_to_fields: - guid: guid - tags: tags - - english: Word - danish: X Word - danish audio: X Pronunciation (Recording and/or IPA) - esperanto: Y Word - esperanto audio: Y Pronunciation (Recording and/or IPA) - - present: Form Present - past: Form Past - present perfect: Form Perfect Present - - plural: Plural - indefinite plural: Indefinite Plural - definite plural: Definite Plural - personal_fields: - - picture - - extra - - morphman_focusmorph - - file_mappings: - - file: source/vocab/main.csv - note_model: LL Word - sort_by_columns: [english] - reverse_sort: false - - derivatives: - - file: source/vocab/derivatives/danish/danish_verbs.csv - note_model: LL Verb - - file: source/vocab/derivatives/danish/danish_nouns.csv - note_model: LL Noun \ No newline at end of file diff --git a/tests/test_files/crowd_anki/crowdanki_example_1/deck.json b/tests/test_files/crowd_anki/crowdanki_example_1/deck.json deleted file mode 100644 index ac970af..0000000 --- a/tests/test_files/crowd_anki/crowdanki_example_1/deck.json +++ /dev/null @@ -1,398 +0,0 @@ -{ - "__type__": "Deck", - "children": [], - "crowdanki_uuid": "16bef726-1426-11ea-a85d-d8cb8ac9abf0", - "deck_config_uuid": "3cc64d85-e410-11e9-960e-d8cb8ac9abf0", - "deck_configurations": [ - { - "__type__": "DeckConfig", - "autoplay": true, - "crowdanki_uuid": "3cc64d85-e410-11e9-960e-d8cb8ac9abf0", - "currentValue": 120, - "dyn": false, - "lapse": { - "delays": [ - 10, - 1440 - ], - "leechAction": 1, - "leechFails": 8, - "minInt": 1, - "mult": 0.25 - }, - "maxLife": 120, - "maxTaken": 60, - "name": "LL Default", - "new": { - "bury": false, - "delays": [ - 1, - 15 - ], - "initialFactor": 2500, - "ints": [ - 5, - 10, - 7 - ], - "order": 0, - "perDay": 3, - "separate": true - }, - "recover": 5, - "replayq": true, - "rev": { - "bury": true, - "ease4": 1.5, - "fuzz": 0.05, - "hardFactor": 1.2, - "ivlFct": 1.5, - "maxIvl": 36500, - "minSpace": 1, - "perDay": 70 - }, - "timer": 0 - } - ], - "desc": "", - "dyn": 0, - "extendNew": 10, - "extendRev": 50, - "name": "LL::1. Vocab", - "note_models": [ - { - "__type__": "NoteModel", - "crowdanki_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", - "css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n\n.card1,.card3, .card5 { background-color: #B60F2D; }\n.card2,.card4, .card6 { background-color: #2E9017; }\n.card7 { background: linear-gradient(90deg, #B60F2D 49.9%, #2E9017 50.1%); }\n\n.word {\n font-size:1.5em;\n}\n\n.pronunciation{\n color:blue;\n}\n\n.extrainfo{\n color:lightgrey;\n}", - "flds": [ - { - "name": "Word" - }, - { - "name": "OtherWord" - } - ], - "latexPost": "\\end{document}", - "latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n", - "name": "Test Model", - "req": [ - [ - 0, - "all", - [ - 1 - ] - ], - [ - 1, - "all", - [ - 2 - ] - ], - [ - 2, - "all", - [ - 1, - 3 - ] - ], - [ - 3, - "all", - [ - 2, - 3 - ] - ], - [ - 4, - "all", - [ - 1, - 3 - ] - ], - [ - 5, - "all", - [ - 2, - 3 - ] - ], - [ - 6, - "all", - [ - 1, - 2, - 3 - ] - ] - ], - "sortf": 0, - "tags": [ - "Meta::InProgress" - ], - "tmpls": [ - { - "afmt": "{{#X Word}}\n\t{{X Word}}\n{{/X Word}}\n\n
\n\n{{Picture}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "X Comprehension", - "ord": 0, - "qfmt": "{{#X Word}}\n\t{{text:X Word}}\n{{/X Word}}" - }, - { - "afmt": "{{#Y Word}}\n\t {{Y Word}}\n{{/Y Word}}\n\n
\n\n{{Picture}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "Y Comprehension", - "ord": 1, - "qfmt": "{{#Y Word}}\n\t{{text:Y Word}}\n{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n{{X Word}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "X Production", - "ord": 2, - "qfmt": "{{#X Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/X Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n {{Y Word}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "Y Production", - "ord": 3, - "qfmt": "{{#Y Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "X Spelling", - "ord": 4, - "qfmt": "{{#X Word}}\n\t
Spell this word:
\n\n\t{{type:X Word}}\n\n\t
{{Picture}}\n{{/X Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "Y Spelling", - "ord": 5, - "qfmt": "{{#Y Word}}\n\t
Spell this word:
\n\n\t{{type:Y Word}}\n\n\t
{{Picture}}\n{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n
{{text:X Word}}
\n
{{text:Y Word}}
\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "X and Y Production", - "ord": 6, - "qfmt": "{{#X Word}}\n{{#Y Word}}\n\t{{Picture}}\n{{/Y Word}}\n{{/X Word}}\n" - } - ], - "type": 0, - "vers": [] - } - ], - "notes": [ - { - "fields": [ - "you", - "du" - ], - "guid": "AAAA", - "tags": [ - "funny" - ], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "healthy", - "rask" - ], - "guid": "BBBB", - "tags": [ - "tag2", - "tag3" - ], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "tired", - "træt" - ], - "guid": "CCCC", - "tags": [ - "besttag" - ], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "banana", - "en banan" - ], - "guid": "DDDD", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "cat", - "en kat" - ], - "guid": "EEEE", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "dog", - "en hund" - ], - "guid": "FFFF", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "fish", - "en fisk" - ], - "guid": "GGGG", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "bird", - "en fugl" - ], - "guid": "HHHH", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "cow", - "en ko" - ], - "guid": "IIII", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "pig", - "et svin" - ], - "guid": "JJJJ", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "mouse", - "en mus" - ], - "guid": "KKKK", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "horse", - "en hest" - ], - "guid": "LLLL", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "to learn", - "at lære" - ], - "guid": "MMMM", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "to eat", - "at spise" - ], - "guid": "NNNN", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - }, - { - "fields": [ - "to drink", - "at drikke" - ], - "guid": "OOOO", - "tags": [], - "__type__": "Note", - "data": "", - "flags": 0, - "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - } - ], - "media_files": [] -} \ No newline at end of file diff --git a/tests/test_files/csv/test1.csv b/tests/test_files/csv/test1.csv deleted file mode 100644 index 7308d3b..0000000 --- a/tests/test_files/csv/test1.csv +++ /dev/null @@ -1,16 +0,0 @@ -guid,English,Danish,Esperanto,Danish Audio,Esperanto Audio,Japanese,Tags -AAAA,you,du,vi,[sound:pronunciation_da_du.mp3],,,funny -BBBB,healthy,rask,,,,test,tag2 tag3 -CCCC,tired,træt,,,,,besttag -DDDD,banana,en banan,banano,[sound:pronunciation_da_banan.mp3],,, -EEEE,cat,en kat,,,,, -FFFF,dog,en hund,hundo,,,, -GGGG,fish,en fisk,,,,, -HHHH,bird,en fugl,birdo,,,, -IIII,cow,en ko,,,,, -JJJJ,pig,et svin,,,,, -KKKK,mouse,en mus,,,,, -LLLL,horse,en hest,,,,, -MMMM,to learn,at lære,lerni,[sound:pronunciation_da_lære.mp3],,, -NNNN,to eat,at spise,manĝi,,,, -OOOO,to drink,at drikke,drinki,,,, \ No newline at end of file diff --git a/tests/test_files/csv/test1_split1.csv b/tests/test_files/csv/test1_split1.csv deleted file mode 100644 index 8b4b711..0000000 --- a/tests/test_files/csv/test1_split1.csv +++ /dev/null @@ -1,7 +0,0 @@ -guid,English,Danish,Esperanto,Danish Audio,Esperanto Audio,Japanese,Tags -AAAA,you,du,vi,[sound:pronunciation_da_du.mp3],,,funny -BBBB,healthy,rask,,,,test,tag2 tag3 -CCCC,tired,træt,,,,,besttag -DDDD,banana,en banan,banano,[sound:pronunciation_da_banan.mp3],,, -EEEE,cat,en kat,,,,, -FFFF,dog,en hund,hundo,,,, \ No newline at end of file diff --git a/tests/test_files/csv/test1_split2.csv b/tests/test_files/csv/test1_split2.csv deleted file mode 100644 index 176857d..0000000 --- a/tests/test_files/csv/test1_split2.csv +++ /dev/null @@ -1,10 +0,0 @@ -guid,English,Danish,Esperanto,Danish Audio,Esperanto Audio,Japanese,Tags -GGGG,fish,en fisk,,,,, -HHHH,bird,en fugl,birdo,,,, -IIII,cow,en ko,,,,, -JJJJ,pig,et svin,,,,, -KKKK,mouse,en mus,,,,, -LLLL,horse,en hest,,,,, -MMMM,to learn,at lære,lerni,[sound:pronunciation_da_lære.mp3],,, -NNNN,to eat,at spise,manĝi,,,, -OOOO,to drink,at drikke,drinki,,,, \ No newline at end of file diff --git a/tests/test_files/csv/test2.csv b/tests/test_files/csv/test2.csv deleted file mode 100644 index ffbd0ed..0000000 --- a/tests/test_files/csv/test2.csv +++ /dev/null @@ -1,10 +0,0 @@ -guid,English,Danish,Esperanto,Danish Audio,Esperanto Audio,Japanese,Tags -DDDD,banana,en banan,banano,[sound:pronunciation_da_banan.mp3],,,LL::Noun -EEEE,cat,en kat,,,,,Animal LL::Noun -FFFF,dog,en hund,hundo,,,,Animal LL::Noun -GGGG,fish,en fisk,,,,,Animal LL::Noun -HHHH,bird,en fugl,birdo,,,,Animal LL::Noun -IIII,cow,en ko,,,,,Animal LL::Noun -JJJJ,pig,et svin,,,,,Animal LL::Noun -KKKK,mouse,en mus,,,,,Animal LL::Noun -LLLL,horse,en hest,,,,,Animal LL::Noun \ No newline at end of file diff --git a/tests/test_files/csv/test2_missing_guids.csv b/tests/test_files/csv/test2_missing_guids.csv deleted file mode 100644 index 894bb66..0000000 --- a/tests/test_files/csv/test2_missing_guids.csv +++ /dev/null @@ -1,10 +0,0 @@ -guid,English,Danish,Esperanto,Danish Audio,Esperanto Audio,Japanese,Tags -,banana,en banan,banano,[sound:pronunciation_da_banan.mp3],,,LL::Noun -,cat,en kat,,,,,Animal LL::Noun -,dog,en hund,hundo,,,,Animal LL::Noun -,fish,en fisk,,,,,Animal LL::Noun -,bird,en fugl,birdo,,,,Animal LL::Noun -,cow,en ko,,,,,Animal LL::Noun -,pig,et svin,,,,,Animal LL::Noun -,mouse,en mus,,,,,Animal LL::Noun -,horse,en hest,,,,,Animal LL::Noun \ No newline at end of file diff --git a/tests/test_files/csv/test3.csv b/tests/test_files/csv/test3.csv deleted file mode 100644 index ace7b59..0000000 --- a/tests/test_files/csv/test3.csv +++ /dev/null @@ -1,2 +0,0 @@ -guid,English,Danish,Esperanto,Danish Audio,Esperanto Audio,Japanese,Tags -1111,New,Ny,nova,,,atarashi, \ No newline at end of file diff --git a/tests/test_files/deck_parts/note_models/LL Word No Defaults.json b/tests/test_files/deck_parts/note_models/LL Word No Defaults.json deleted file mode 100644 index bc20584..0000000 --- a/tests/test_files/deck_parts/note_models/LL Word No Defaults.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "__type__": "NoteModelTEST", - "crowdanki_uuid": "057a8d66-bc4e-11e9-9822-d8cb8ac9abf0", - "css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n\n.card1,.card3, .card5 { background-color: #B60F2D; }\n.card2,.card4, .card6 { background-color: #2E9017; }\n.card7 { background: linear-gradient(90deg, #B60F2D 49.9%, #2E9017 50.1%); }\n\n.word {\n font-size:1.5em;\n}\n\n.pronunciation{\n color:blue;\n}\n\n.extrainfo{\n color:lightgrey;\n}", - "flds": [ - { - "font": "Liberation SansTEST", - "media": ["TEST"], - "name": "Word", - "ord": 0, - "rtl": true, - "size": 10, - "sticky": true - }, - { - "font": "Arial", - "media": ["TEST"], - "name": "X Word", - "ord": 1, - "rtl": true, - "size": 10, - "sticky": true - }, - { - "font": "Arial", - "media": ["TEST"], - "name": "Y Word", - "ord": 2, - "rtl": true, - "size": 10, - "sticky": true - }, - { - "font": "Arial", - "media": ["TEST"], - "name": "Picture", - "ord": 3, - "rtl": true, - "size": 10, - "sticky": true - }, - { - "font": "Arial", - "media": ["TEST"], - "name": "Extra", - "ord": 4, - "rtl": true, - "size": 10, - "sticky": true - }, - { - "font": "Arial", - "media": ["TEST"], - "name": "X Pronunciation (Recording and/or IPA)", - "ord": 5, - "rtl": true, - "size": 10, - "sticky": true - }, - { - "font": "Arial", - "media": ["TEST"], - "name": "Y Pronunciation (Recording and/or IPA)", - "ord": 6, - "rtl": true, - "size": 10, - "sticky": true - } - ], - "latexPost": "\\end{document}TEST", - "latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\nTEST", - "name": "LL Word", - "req": [], - "sortf": 1, - "tags": ["TEST"], - "tmpls": [ - { - "afmt": "{{#X Word}}\n\t{{X Word}}\n{{/X Word}}\n\n
\n\n{{Picture}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "TEST", - "bqfmt": "TEST", - "did": 1, - "name": "X Comprehension", - "ord": 0, - "qfmt": "{{#X Word}}\n\t{{text:X Word}}\n{{/X Word}}" - }, - { - "afmt": "{{#Y Word}}\n\t{{Y Word}}\n{{/Y Word}}\n\n
\n\n{{Picture}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "TEST", - "bqfmt": "TEST", - "did": 1, - "name": "Y Comprehension", - "ord": 1, - "qfmt": "{{#Y Word}}\n\t{{text:Y Word}}\n{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n{{X Word}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "TEST", - "bqfmt": "TEST", - "did": 1, - "name": "X Production", - "ord": 2, - "qfmt": "{{#X Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/X Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n{{Y Word}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "TEST", - "bqfmt": "TEST", - "did": 1, - "name": "Y Production", - "ord": 3, - "qfmt": "{{#Y Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "TEST", - "bqfmt": "TEST", - "did": 1, - "name": "X Spelling", - "ord": 4, - "qfmt": "{{#X Word}}\n\t
Spell this word:
\n\n\t
{{type:X Word}}
\n\n\t
{{Picture}}\n{{/X Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "TEST", - "bqfmt": "TEST", - "did": 2, - "name": "Y Spelling", - "ord": 5, - "qfmt": "{{#Y Word}}\n\t
Spell this word:
\n\n\t
{{type:Y Word}}
\n\n\t
{{Picture}}\n{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n
{{text:X Word}}
\n
{{text:Y Word}}
\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "TEST", - "bqfmt": "TEST", - "did": 1, - "name": "X and Y Production", - "ord": 6, - "qfmt": "{{#X Word}}\n{{#Y Word}}\n\t{{Picture}}\n{{/Y Word}}\n{{/X Word}}" - } - ], - "type": 1, - "vers": ["TEST"] -} \ No newline at end of file diff --git a/tests/test_files/deck_parts/note_models/LL Word Only Required.json b/tests/test_files/deck_parts/note_models/LL Word Only Required.json deleted file mode 100644 index e9becab..0000000 --- a/tests/test_files/deck_parts/note_models/LL Word Only Required.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "crowdanki_uuid": "057a8d66-bc4e-11e9-9822-d8cb8ac9abf0", - "css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n\n.card1,.card3, .card5 { background-color: #B60F2D; }\n.card2,.card4, .card6 { background-color: #2E9017; }\n.card7 { background: linear-gradient(90deg, #B60F2D 49.9%, #2E9017 50.1%); }\n\n.word {\n font-size:1.5em;\n}\n\n.pronunciation{\n color:blue;\n}\n\n.extrainfo{\n color:lightgrey;\n}", - "flds": [ - { - "name": "Word", - "ord": 0, - "size": 12 - }, - { - "font": "Arial", - "name": "X Word", - "ord": 1 - }, - { - "font": "Arial", - "name": "Y Word", - "ord": 2 - }, - { - "font": "Arial", - "name": "Picture", - "ord": 3, - "size": 6 - }, - { - "font": "Arial", - "name": "Extra", - "ord": 4 - }, - { - "font": "Arial", - "name": "X Pronunciation (Recording and/or IPA)", - "ord": 5 - }, - { - "font": "Arial", - "name": "Y Pronunciation (Recording and/or IPA)", - "ord": 6 - } - ], - "name": "LL Word", - "tmpls": [ - { - "afmt": "{{#X Word}}\n\t{{X Word}}\n{{/X Word}}\n\n
\n\n{{Picture}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "name": "X Comprehension", - "ord": 0, - "qfmt": "{{#X Word}}\n\t{{text:X Word}}\n{{/X Word}}" - }, - { - "afmt": "{{#Y Word}}\n\t{{Y Word}}\n{{/Y Word}}\n\n
\n\n{{Picture}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "name": "Y Comprehension", - "ord": 1, - "qfmt": "{{#Y Word}}\n\t{{text:Y Word}}\n{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n{{X Word}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "name": "X Production", - "ord": 2, - "qfmt": "{{#X Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/X Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n{{Y Word}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "name": "Y Production", - "ord": 3, - "qfmt": "{{#Y Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "name": "X Spelling", - "ord": 4, - "qfmt": "{{#X Word}}\n\t
Spell this word:
\n\n\t
{{type:X Word}}
\n\n\t
{{Picture}}\n{{/X Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "name": "Y Spelling", - "ord": 5, - "qfmt": "{{#Y Word}}\n\t
Spell this word:
\n\n\t
{{type:Y Word}}
\n\n\t
{{Picture}}\n{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n
{{text:X Word}}
\n
{{text:Y Word}}
\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "name": "X and Y Production", - "ord": 6, - "qfmt": "{{#X Word}}\n{{#Y Word}}\n\t{{Picture}}\n{{/Y Word}}\n{{/X Word}}" - } - ] -} \ No newline at end of file diff --git a/tests/test_files/deck_parts/note_models/LL Word.json b/tests/test_files/deck_parts/note_models/LL Word.json deleted file mode 100644 index cc48cfa..0000000 --- a/tests/test_files/deck_parts/note_models/LL Word.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "__type__": "NoteModel", - "crowdanki_uuid": "057a8d66-bc4e-11e9-9822-d8cb8ac9abf0", - "css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n\n.card1,.card3, .card5 { background-color: #B60F2D; }\n.card2,.card4, .card6 { background-color: #2E9017; }\n.card7 { background: linear-gradient(90deg, #B60F2D 49.9%, #2E9017 50.1%); }\n\n.word {\n font-size:1.5em;\n}\n\n.pronunciation{\n color:blue;\n}\n\n.extrainfo{\n color:lightgrey;\n}", - "flds": [ - { - "font": "Liberation Sans", - "media": [], - "name": "Word", - "ord": 0, - "rtl": false, - "size": 12, - "sticky": false - }, - { - "font": "Arial", - "media": [], - "name": "X Word", - "ord": 1, - "rtl": false, - "size": 20, - "sticky": false - }, - { - "font": "Arial", - "media": [], - "name": "Y Word", - "ord": 2, - "rtl": false, - "size": 20, - "sticky": false - }, - { - "font": "Arial", - "media": [], - "name": "Picture", - "ord": 3, - "rtl": false, - "size": 6, - "sticky": false - }, - { - "font": "Arial", - "media": [], - "name": "Extra", - "ord": 4, - "rtl": false, - "size": 20, - "sticky": false - }, - { - "font": "Arial", - "media": [], - "name": "X Pronunciation (Recording and/or IPA)", - "ord": 5, - "rtl": false, - "size": 20, - "sticky": false - }, - { - "font": "Arial", - "media": [], - "name": "Y Pronunciation (Recording and/or IPA)", - "ord": 6, - "rtl": false, - "size": 20, - "sticky": false - } - ], - "latexPost": "\\end{document}", - "latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n", - "latexsvg": false, - "name": "LL Word", - "req": [], - "sortf": 0, - "tags": [], - "tmpls": [ - { - "afmt": "{{#X Word}}\n\t{{X Word}}\n{{/X Word}}\n\n
\n\n{{Picture}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bfont": "", - "bqfmt": "", - "bsize": 0, - "did": null, - "name": "X Comprehension", - "ord": 0, - "qfmt": "{{#X Word}}\n\t{{text:X Word}}\n{{/X Word}}", - "scratchPad": 0 - }, - { - "afmt": "{{#Y Word}}\n\t{{Y Word}}\n{{/Y Word}}\n\n
\n\n{{Picture}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bfont": "", - "bqfmt": "", - "bsize": 0, - "did": null, - "name": "Y Comprehension", - "ord": 1, - "qfmt": "{{#Y Word}}\n\t{{text:Y Word}}\n{{/Y Word}}", - "scratchPad": 0 - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n{{X Word}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bfont": "", - "bqfmt": "", - "bsize": 0, - "did": null, - "name": "X Production", - "ord": 2, - "qfmt": "{{#X Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/X Word}}", - "scratchPad": 0 - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n{{Y Word}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bfont": "", - "bqfmt": "", - "bsize": 0, - "did": null, - "name": "Y Production", - "ord": 3, - "qfmt": "{{#Y Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/Y Word}}", - "scratchPad": 0 - }, - { - "afmt": "{{FrontSide}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bfont": "", - "bqfmt": "", - "bsize": 0, - "did": null, - "name": "X Spelling", - "ord": 4, - "qfmt": "{{#X Word}}\n\t
Spell this word:
\n\n\t
{{type:X Word}}
\n\n\t
{{Picture}}\n{{/X Word}}", - "scratchPad": 0 - }, - { - "afmt": "{{FrontSide}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bfont": "", - "bqfmt": "", - "bsize": 0, - "did": null, - "name": "Y Spelling", - "ord": 5, - "qfmt": "{{#Y Word}}\n\t
Spell this word:
\n\n\t
{{type:Y Word}}
\n\n\t
{{Picture}}\n{{/Y Word}}", - "scratchPad": 0 - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n
{{text:X Word}}
\n
{{text:Y Word}}
\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bfont": "", - "bqfmt": "", - "bsize": 0, - "did": null, - "name": "X and Y Production", - "ord": 6, - "qfmt": "{{#X Word}}\n{{#Y Word}}\n\t{{Picture}}\n{{/Y Word}}\n{{/X Word}}", - "scratchPad": 0 - } - ], - "type": 0, - "vers": [] -} \ No newline at end of file diff --git a/tests/test_files/deck_parts/note_models/Test-Model.json b/tests/test_files/deck_parts/note_models/Test-Model.json deleted file mode 100644 index 1341727..0000000 --- a/tests/test_files/deck_parts/note_models/Test-Model.json +++ /dev/null @@ -1,144 +0,0 @@ -{ - "__type__": "NoteModel", - "crowdanki_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", - "css": ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\n background-color: white;\n}\n\n.card1,.card3, .card5 { background-color: #B60F2D; }\n.card2,.card4, .card6 { background-color: #2E9017; }\n.card7 { background: linear-gradient(90deg, #B60F2D 49.9%, #2E9017 50.1%); }\n\n.word {\n font-size:1.5em;\n}\n\n.pronunciation{\n color:blue;\n}\n\n.extrainfo{\n color:lightgrey;\n}", - "flds": [ - { - "name": "Word" - }, - { - "name": "OtherWord" - } - ], - "latexPost": "\\end{document}", - "latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n", - "name": "Test Model", - "req": [ - [ - 0, - "all", - [ - 1 - ] - ], - [ - 1, - "all", - [ - 2 - ] - ], - [ - 2, - "all", - [ - 1, - 3 - ] - ], - [ - 3, - "all", - [ - 2, - 3 - ] - ], - [ - 4, - "all", - [ - 1, - 3 - ] - ], - [ - 5, - "all", - [ - 2, - 3 - ] - ], - [ - 6, - "all", - [ - 1, - 2, - 3 - ] - ] - ], - "sortf": 0, - "tags": [ - "Meta::InProgress" - ], - "tmpls": [ - { - "afmt": "{{#X Word}}\n\t{{X Word}}\n{{/X Word}}\n\n
\n\n{{Picture}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "X Comprehension", - "ord": 0, - "qfmt": "{{#X Word}}\n\t{{text:X Word}}\n{{/X Word}}" - }, - { - "afmt": "{{#Y Word}}\n\t {{Y Word}}\n{{/Y Word}}\n\n
\n\n{{Picture}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "Y Comprehension", - "ord": 1, - "qfmt": "{{#Y Word}}\n\t{{text:Y Word}}\n{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n{{X Word}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "X Production", - "ord": 2, - "qfmt": "{{#X Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/X Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n {{Y Word}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "Y Production", - "ord": 3, - "qfmt": "{{#Y Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "X Spelling", - "ord": 4, - "qfmt": "{{#X Word}}\n\t
Spell this word:
\n\n\t{{type:X Word}}\n\n\t
{{Picture}}\n{{/X Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "Y Spelling", - "ord": 5, - "qfmt": "{{#Y Word}}\n\t
Spell this word:
\n\n\t{{type:Y Word}}\n\n\t
{{Picture}}\n{{/Y Word}}" - }, - { - "afmt": "{{FrontSide}}\n\n
\n\n
{{text:X Word}}
\n
{{text:Y Word}}
\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}", - "bafmt": "", - "bqfmt": "", - "did": null, - "name": "X and Y Production", - "ord": 6, - "qfmt": "{{#X Word}}\n{{#Y Word}}\n\t{{Picture}}\n{{/Y Word}}\n{{/X Word}}\n" - } - ], - "type": 0, - "vers": [] -} \ No newline at end of file diff --git a/tests/test_files/deck_parts/yaml/note_models/LL-Word-No-Defaults.yaml b/tests/test_files/deck_parts/yaml/note_models/LL-Word-No-Defaults.yaml deleted file mode 100644 index f4dcb16..0000000 --- a/tests/test_files/deck_parts/yaml/note_models/LL-Word-No-Defaults.yaml +++ /dev/null @@ -1,144 +0,0 @@ -name: LL Word -id: 057a8d66-bc4e-11e9-9822-d8cb8ac9abf0 -css: ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color:\ - \ black;\n background-color: white;\n}\n\n.card1,.card3, .card5 { background-color:\ - \ #B60F2D; }\n.card2,.card4, .card6 { background-color: #2E9017; }\n.card7 { background:\ - \ linear-gradient(90deg, #B60F2D 49.9%, #2E9017 50.1%); }\n\n.word {\n font-size:1.5em;\n\ - }\n\n.pronunciation{\n color:blue;\n}\n\n.extrainfo{\n color:lightgrey;\n}" -sort_field_num: 1 -is_cloze: true -latex_pre: "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage{amssymb,amsmath}\n\ - \\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\nTEST" -latex_post: \end{document}TEST -fields: -- name: Word - font: Liberation SansTEST - media: - - TEST - is_right_to_left: true - font_size: 10 - is_sticky: true -- name: X Word - font: Arial - media: - - TEST - is_right_to_left: true - font_size: 10 - is_sticky: true -- name: Y Word - font: Arial - media: - - TEST - is_right_to_left: true - font_size: 10 - is_sticky: true -- name: Picture - font: Arial - media: - - TEST - is_right_to_left: true - font_size: 10 - is_sticky: true -- name: Extra - font: Arial - media: - - TEST - is_right_to_left: true - font_size: 10 - is_sticky: true -- name: X Pronunciation (Recording and/or IPA) - font: Arial - media: - - TEST - is_right_to_left: true - font_size: 10 - is_sticky: true -- name: Y Pronunciation (Recording and/or IPA) - font: Arial - media: - - TEST - is_right_to_left: true - font_size: 10 - is_sticky: true -templates: -- name: X Comprehension - question_format: "{{#X Word}}\n\t{{text:X Word}}\n{{/X\ - \ Word}}" - answer_format: "{{#X Word}}\n\t{{X Word}}\n{{/X Word}}\n\ - \n
\n\n{{Picture}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\ - \t
{{X Pronunciation (Recording and/or IPA)}}\n\ - {{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}" - browser_question_format: TEST - browser_answer_format: TEST - deck_override_id: 1 -- name: Y Comprehension - question_format: "{{#Y Word}}\n\t{{text:Y Word}}\n{{/Y\ - \ Word}}" - answer_format: "{{#Y Word}}\n\t{{Y Word}}\n{{/Y Word}}\n\ - \n
\n\n{{Picture}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\ - \t
{{Y Pronunciation (Recording and/or IPA)}}\n\ - {{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}" - browser_question_format: TEST - browser_answer_format: TEST - deck_override_id: 1 -- name: X Production - question_format: "{{#X Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/X Word}}" - answer_format: "{{FrontSide}}\n\n
\n\n{{X Word}}\n\ - \n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording\ - \ and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n\ - {{/Extra}}" - browser_question_format: TEST - browser_answer_format: TEST - deck_override_id: 1 -- name: Y Production - question_format: "{{#Y Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/Y Word}}" - answer_format: "{{FrontSide}}\n\n
\n\n{{Y Word}}\n\ - \n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording\ - \ and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n\ - {{/Extra}}" - browser_question_format: TEST - browser_answer_format: TEST - deck_override_id: 1 -- name: X Spelling - question_format: "{{#X Word}}\n\t
Spell this word:
\n\n\t
{{type:X Word}}
\n\n\t
{{Picture}}\n{{/X Word}}" - answer_format: "{{FrontSide}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t\ -
{{X Pronunciation (Recording and/or IPA)}}\n\ - {{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}" - browser_question_format: TEST - browser_answer_format: TEST - deck_override_id: 1 -- name: Y Spelling - question_format: "{{#Y Word}}\n\t
Spell this word:
\n\n\t
{{type:Y Word}}
\n\n\t
{{Picture}}\n{{/Y Word}}" - answer_format: "{{FrontSide}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t\ -
{{Y Pronunciation (Recording and/or IPA)}}\n\ - {{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}" - browser_question_format: TEST - browser_answer_format: TEST - deck_override_id: 2 -- name: X and Y Production - question_format: "{{#X Word}}\n{{#Y Word}}\n\t{{Picture}}\n{{/Y Word}}\n{{/X Word}}" - answer_format: "{{FrontSide}}\n\n
\n\n
{{text:X\ - \ Word}}
\n
{{text:Y Word}}
\n\n{{#X Pronunciation\ - \ (Recording and/or IPA)}}\n\t
{{X Pronunciation\ - \ (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\ - \n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording\ - \ and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n\ - {{/Extra}}" - browser_question_format: TEST - browser_answer_format: TEST - deck_override_id: 1 -tags: -- TEST -version: -- TEST -__type__: NoteModelTEST -required_fields_per_template: [] \ No newline at end of file diff --git a/tests/test_files/deck_parts/yaml/note_models/LL-Word-Only-Required.yaml b/tests/test_files/deck_parts/yaml/note_models/LL-Word-Only-Required.yaml deleted file mode 100644 index 4128c83..0000000 --- a/tests/test_files/deck_parts/yaml/note_models/LL-Word-Only-Required.yaml +++ /dev/null @@ -1,79 +0,0 @@ -name: LL Word -id: 057a8d66-bc4e-11e9-9822-d8cb8ac9abf0 -css: ".card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color:\ - \ black;\n background-color: white;\n}\n\n.card1,.card3, .card5 { background-color:\ - \ #B60F2D; }\n.card2,.card4, .card6 { background-color: #2E9017; }\n.card7 { background:\ - \ linear-gradient(90deg, #B60F2D 49.9%, #2E9017 50.1%); }\n\n.word {\n font-size:1.5em;\n\ - }\n\n.pronunciation{\n color:blue;\n}\n\n.extrainfo{\n color:lightgrey;\n}" -fields: -- name: Word - font_size: 12 -- name: X Word - font: Arial -- name: Y Word - font: Arial -- name: Picture - font: Arial - font_size: 6 -- name: Extra - font: Arial -- name: X Pronunciation (Recording and/or IPA) - font: Arial -- name: Y Pronunciation (Recording and/or IPA) - font: Arial -templates: -- name: X Comprehension - question_format: "{{#X Word}}\n\t{{text:X Word}}\n{{/X\ - \ Word}}" - answer_format: "{{#X Word}}\n\t{{X Word}}\n{{/X Word}}\n\ - \n
\n\n{{Picture}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\ - \t
{{X Pronunciation (Recording and/or IPA)}}\n\ - {{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}" -- name: Y Comprehension - question_format: "{{#Y Word}}\n\t{{text:Y Word}}\n{{/Y\ - \ Word}}" - answer_format: "{{#Y Word}}\n\t{{Y Word}}\n{{/Y Word}}\n\ - \n
\n\n{{Picture}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\ - \t
{{Y Pronunciation (Recording and/or IPA)}}\n\ - {{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}" -- name: X Production - question_format: "{{#X Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/X Word}}" - answer_format: "{{FrontSide}}\n\n
\n\n{{X Word}}\n\ - \n{{#X Pronunciation (Recording and/or IPA)}}\n\t
{{X Pronunciation (Recording and/or IPA)}}\n{{/X Pronunciation (Recording\ - \ and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n\ - {{/Extra}}" -- name: Y Production - question_format: "{{#Y Word}}{{#Picture}}\n\t{{Picture}}\n{{/Picture}}{{/Y Word}}" - answer_format: "{{FrontSide}}\n\n
\n\n{{Y Word}}\n\ - \n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording\ - \ and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n\ - {{/Extra}}" -- name: X Spelling - question_format: "{{#X Word}}\n\t
Spell this word:
\n\n\t
{{type:X Word}}
\n\n\t
{{Picture}}\n{{/X Word}}" - answer_format: "{{FrontSide}}\n\n{{#X Pronunciation (Recording and/or IPA)}}\n\t\ -
{{X Pronunciation (Recording and/or IPA)}}\n\ - {{/X Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}" -- name: Y Spelling - question_format: "{{#Y Word}}\n\t
Spell this word:
\n\n\t
{{type:Y Word}}
\n\n\t
{{Picture}}\n{{/Y Word}}" - answer_format: "{{FrontSide}}\n\n{{#Y Pronunciation (Recording and/or IPA)}}\n\t\ -
{{Y Pronunciation (Recording and/or IPA)}}\n\ - {{/Y Pronunciation (Recording and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n{{/Extra}}" -- name: X and Y Production - question_format: "{{#X Word}}\n{{#Y Word}}\n\t{{Picture}}\n{{/Y Word}}\n{{/X Word}}" - answer_format: "{{FrontSide}}\n\n
\n\n
{{text:X\ - \ Word}}
\n
{{text:Y Word}}
\n\n{{#X Pronunciation\ - \ (Recording and/or IPA)}}\n\t
{{X Pronunciation\ - \ (Recording and/or IPA)}}\n{{/X Pronunciation (Recording and/or IPA)}}\n\ - \n{{#Y Pronunciation (Recording and/or IPA)}}\n\t
{{Y Pronunciation (Recording and/or IPA)}}\n{{/Y Pronunciation (Recording\ - \ and/or IPA)}}\n\n
\n{{#Extra}}\n\t
{{Extra}}\n\ - {{/Extra}}" -required_fields_per_template: [] \ No newline at end of file diff --git a/tests/test_files/deck_parts/yaml/notes/note1.yaml b/tests/test_files/deck_parts/yaml/notes/note1.yaml deleted file mode 100644 index a79da3d..0000000 --- a/tests/test_files/deck_parts/yaml/notes/note1.yaml +++ /dev/null @@ -1,31 +0,0 @@ -note_groupings: - - note_model: LL Noun - tags: - - noun - - english - notes: - - guid: 7ysf7ysd8f8 - fields: - - test - - blah - - another one - - guid: sfkdsfhsd - fields: - - first - - second - - third - - note_model: LL Verb - tags: - - verb - - english - notes: - - guid: dhdfhsdf - fields: - - verby - - boo - - another one - - guid: dfgdfhgjs - fields: - - first - - second - - third diff --git a/tests/test_files/media_files/buried/even_more/signals2.png b/tests/test_files/media_files/buried/even_more/signals2.png deleted file mode 100644 index 4c55c9dbfd78f517efeb63c402b511a9bcaf0e11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15118 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 Xd4fuKjbkpr77%r1oeM?htwa7BkS8Q( diff --git a/tests/test_files/media_files/signals.png b/tests/test_files/media_files/signals.png deleted file mode 100644 index 4c55c9dbfd78f517efeb63c402b511a9bcaf0e11..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15118 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 Xd4fuKjbkpr77%r1oeM?htwa7BkS8Q( diff --git a/tests/test_files/tsv/test1.tsv b/tests/test_files/tsv/test1.tsv deleted file mode 100644 index b4ca91b..0000000 --- a/tests/test_files/tsv/test1.tsv +++ /dev/null @@ -1,16 +0,0 @@ -guid English Danish Esperanto Danish Audio Esperanto Audio Japanese Tags -AAAA you du vi [sound:pronunciation_da_du.mp3] funny -BBBB healthy rask test tag2 tag3 -CCCC tired træt besttag -DDDD banana en banan banano [sound:pronunciation_da_banan.mp3] -EEEE cat en kat -FFFF dog en hund hundo -GGGG fish en fisk -HHHH bird en fugl birdo -IIII cow en ko -JJJJ pig et svin -KKKK mouse en mus -LLLL horse en hest -MMMM to learn at lære lerni [sound:pronunciation_da_lære.mp3] -NNNN to eat at spise manĝi -OOOO to drink at drikke drinki diff --git a/tests/test_helpers.py b/tests/test_helpers.py deleted file mode 100644 index 863e869..0000000 --- a/tests/test_helpers.py +++ /dev/null @@ -1,7 +0,0 @@ -from brain_brew.configuration.part_holder import PartHolder - - -def debug_write_part_to_file(part, filepath: str): - dp = PartHolder("Blah", filepath, part) - dp.save_to_file = filepath - dp.write_to_file() diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index a64ce90..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,84 +0,0 @@ -import pytest - -from brain_brew.representation.yaml.note_model_template import html_separator_regex -from brain_brew.utils import find_media_in_field, str_to_lowercase_no_separators, split_tags, split_by_regex - - -class TestFindMedia: - @pytest.mark.parametrize("field_value, expected_results", [ - (r'', ["image.png"]), - (r'', ["image.png"]), - (r'< img src="image.png">', ["image.png"]), - (r'< img src="image.png">', ["image.png"]), - (r'', ["image.png"]), - (r'', ["image.png"]), - (r'', ["image.png"]), - (r'words in the field end other stuff', ["image.png"]), - (r'', ["ug-map-saint_barthelemy.png"]), - (r'', - ["ug-map-saint_barthelemy.png", "image.png"]), - (r'[sound:test.mp3]', ["test.mp3"]), - (r'[sound:test.mp3][sound:othersound.mp3]', ["test.mp3", "othersound.mp3"]), - (r'[sound:test.mp3] [sound:othersound.mp3]', ["test.mp3", "othersound.mp3"]), - (r'words in the field [sound:test.mp3] other stuff too [sound:othersound.mp3] end', ["test.mp3", "othersound.mp3"]), - (r'[sound:unfinished-bracket.mp3', []) - ]) - def test_find_media_in_field(self, field_value, expected_results): - assert find_media_in_field(field_value) == expected_results - - -class TestHelperFunctions: - @pytest.mark.parametrize("str_to_tidy", [ - 'Generate Csv Blah Blah', - 'Generate__Csv_Blah-Blah', - 'Generate Csv Blah Blah', - 'generateCsvBlahBlah' - ]) - def test_remove_spacers_from_str(self, str_to_tidy): - assert str_to_lowercase_no_separators(str_to_tidy) == "generatecsvblahblah" - - -class TestSplitTags: - @pytest.mark.parametrize("str_to_split, expected_result", [ - ("tags1, tags2", ["tags1", "tags2"]), - ("tags1 tags2", ["tags1", "tags2"]), - ("tags1; tags2", ["tags1", "tags2"]), - ("tags1 tags2", ["tags1", "tags2"]), - ("tags1, tags2, tags3, tags4, tags5, tags6, tags7, tags8, tags9", - ["tags1", "tags2", "tags3", "tags4", "tags5", "tags6", "tags7", "tags8", "tags9"]), - ("tags1, tags2; tags3 tags4 tags5, tags6; tags7 tags8, tags9", - ["tags1", "tags2", "tags3", "tags4", "tags5", "tags6", "tags7", "tags8", "tags9"]), - ("tags1,tags2", ["tags1", "tags2"]), - ("tags1;tags2", ["tags1", "tags2"]), - ("tags1, tags2", ["tags1", "tags2"]), - ("tags1; tags2", ["tags1", "tags2"]), - ]) - def test_runs(self, str_to_split, expected_result): - assert split_tags(str_to_split) == expected_result - - -class TestSplitByRegex: - @pytest.mark.parametrize("str_to_split, split_by, expected_result", [ - ("testbabyhighfive", "baby", ["test", "highfive"]), - ("testbabyhighfive", "(baby)", ["test", "baby", "highfive"]), - ("testbabyhighfive", html_separator_regex, ["testbabyhighfive"]), - ("test\n---\nhighfive", html_separator_regex, ["test", "highfive"]), - ("test\n---\n\nhighfive", html_separator_regex, ["test", "highfive"]), - ("test\n-\nhighfive", html_separator_regex, ["test", "highfive"]), - ("test\n\n\n\n-\nhighfive", html_separator_regex, ["test", "highfive"]), - ("test\n\n\n\n---\n\n\n\nhighfive", html_separator_regex, ["test", "highfive"]), - ("test\n\n\n\n---\n\n\n\nhighfive\n\n--\n\nbackflip", html_separator_regex, ["test", "highfive", "backflip"]), - ]) - def test_runs(self, str_to_split, split_by, expected_result): - assert split_by_regex(str_to_split, split_by) == expected_result - - -# class TestJoinTags: -# @pytest.mark.parametrize("join_with, expected_result", [ -# (", ", "test, test1, test2") -# ]) -# def test_joins(self, global_config, join_with, expected_result): -# list_to_join = ["test", "test1", "test2"] -# global_config.flags.join_values_with = join_with -# -# assert join_tags(list_to_join) == expected_result From 19048d4c876de05b8a0e362faaf36fbea79abc02 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 25 May 2026 09:50:37 +0200 Subject: [PATCH 002/142] feat: add Rust Brain Brew core and CLI --- .editorconfig | 15 + .gitignore | 149 +- Cargo.lock | 1265 ++++++++ Cargo.toml | 25 + crates/brain-brew-cli/Cargo.toml | 28 + crates/brain-brew-cli/src/args.rs | 335 ++ crates/brain-brew-cli/src/commands/compose.rs | 73 + crates/brain-brew-cli/src/commands/diff.rs | 33 + crates/brain-brew-cli/src/commands/explain.rs | 122 + crates/brain-brew-cli/src/commands/export.rs | 109 + crates/brain-brew-cli/src/commands/fmt.rs | 23 + crates/brain-brew-cli/src/commands/import.rs | 38 + crates/brain-brew-cli/src/commands/lock.rs | 885 ++++++ crates/brain-brew-cli/src/commands/mod.rs | 10 + crates/brain-brew-cli/src/commands/targets.rs | 78 + .../brain-brew-cli/src/commands/validate.rs | 128 + crates/brain-brew-cli/src/commands/verify.rs | 120 + crates/brain-brew-cli/src/help.rs | 73 + crates/brain-brew-cli/src/io.rs | 482 +++ crates/brain-brew-cli/src/main.rs | 63 + crates/brain-brew-cli/src/media_assets.rs | 58 + crates/brain-brew-cli/src/output.rs | 174 ++ crates/brain-brew-cli/src/overlay_draft.rs | 299 ++ crates/brain-brew-cli/src/package_resolver.rs | 93 + crates/brain-brew-core/Cargo.toml | 12 + crates/brain-brew-core/src/lib.rs | 2720 +++++++++++++++++ crates/brain-brew-formats/Cargo.toml | 19 + .../brain-brew-formats/src/canonical_yaml.rs | 1827 +++++++++++ crates/brain-brew-formats/src/crowdanki.rs | 1110 +++++++ crates/brain-brew-formats/src/lib.rs | 31 + crates/brain-brew-formats/src/lockfile.rs | 238 ++ crates/brain-brew-formats/src/manifest.rs | 405 +++ crates/brain-brew-formats/src/media.rs | 235 ++ devbox.json | 46 + devbox.lock | 295 ++ flake.lock | 27 + flake.nix | 88 + rustfmt.toml | 3 + 38 files changed, 11607 insertions(+), 127 deletions(-) create mode 100644 .editorconfig create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/brain-brew-cli/Cargo.toml create mode 100644 crates/brain-brew-cli/src/args.rs create mode 100644 crates/brain-brew-cli/src/commands/compose.rs create mode 100644 crates/brain-brew-cli/src/commands/diff.rs create mode 100644 crates/brain-brew-cli/src/commands/explain.rs create mode 100644 crates/brain-brew-cli/src/commands/export.rs create mode 100644 crates/brain-brew-cli/src/commands/fmt.rs create mode 100644 crates/brain-brew-cli/src/commands/import.rs create mode 100644 crates/brain-brew-cli/src/commands/lock.rs create mode 100644 crates/brain-brew-cli/src/commands/mod.rs create mode 100644 crates/brain-brew-cli/src/commands/targets.rs create mode 100644 crates/brain-brew-cli/src/commands/validate.rs create mode 100644 crates/brain-brew-cli/src/commands/verify.rs create mode 100644 crates/brain-brew-cli/src/help.rs create mode 100644 crates/brain-brew-cli/src/io.rs create mode 100644 crates/brain-brew-cli/src/main.rs create mode 100644 crates/brain-brew-cli/src/media_assets.rs create mode 100644 crates/brain-brew-cli/src/output.rs create mode 100644 crates/brain-brew-cli/src/overlay_draft.rs create mode 100644 crates/brain-brew-cli/src/package_resolver.rs create mode 100644 crates/brain-brew-core/Cargo.toml create mode 100644 crates/brain-brew-core/src/lib.rs create mode 100644 crates/brain-brew-formats/Cargo.toml create mode 100644 crates/brain-brew-formats/src/canonical_yaml.rs create mode 100644 crates/brain-brew-formats/src/crowdanki.rs create mode 100644 crates/brain-brew-formats/src/lib.rs create mode 100644 crates/brain-brew-formats/src/lockfile.rs create mode 100644 crates/brain-brew-formats/src/manifest.rs create mode 100644 crates/brain-brew-formats/src/media.rs create mode 100644 devbox.json create mode 100644 devbox.lock create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 rustfmt.toml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3a8c04e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml,json,toml}] +indent_size = 2 diff --git a/.gitignore b/.gitignore index 1506eb0..28dd34c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,134 +1,29 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class +# Rust +/target/ -# C extensions -*.so +# Devbox local state +/.devbox/ -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py +# Editor/OS noise +.DS_Store +.idea/ +.vscode/ +*.swp +*.swo -# Environments +# Local environment .env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject +.env.* +!.env.example -# Rope project settings -.ropeproject +# Nix build outputs +/result +/result-* -# mkdocs documentation -/site +# Documentation site generated files +/documentation/.docusaurus/ +/documentation/build/ +/documentation/node_modules/ -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - - - -.idea/ -.directory \ No newline at end of file +# Local generated/downloaded caches +/.cache/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..665e0cc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1265 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brain-brew-cli" +version = "0.1.0" +dependencies = [ + "base64", + "brain-brew-core", + "brain-brew-formats", + "flate2", + "nix-nar", + "serde_json", + "sha2", + "tar", + "tempfile", + "ureq", +] + +[[package]] +name = "brain-brew-core" +version = "0.1.0" + +[[package]] +name = "brain-brew-formats" +version = "0.1.0" +dependencies = [ + "brain-brew-core", + "serde", + "serde_json", + "serde_yaml", + "sha2", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_executable" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nix-nar" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a60e6f4acddbfaa0c363f8bdc6e4b2188d0b690c567176212f795fe008a30b3" +dependencies = [ + "camino", + "is_executable", + "symlink", + "thiserror", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..63bad74 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[workspace] +members = [ + "crates/brain-brew-core", + "crates/brain-brew-formats", + "crates/brain-brew-cli", +] +resolver = "3" + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.94" +license = "Unlicense" +authors = ["Brain Brew contributors"] +repository = "https://github.com/jeprecated/brain-brew" + +[workspace.dependencies] +brain-brew-core = { path = "crates/brain-brew-core" } +brain-brew-formats = { path = "crates/brain-brew-formats" } + +[workspace.lints.rust] +unsafe_code = "forbid" + +[workspace.lints.clippy] +all = "warn" diff --git a/crates/brain-brew-cli/Cargo.toml b/crates/brain-brew-cli/Cargo.toml new file mode 100644 index 0000000..bccfd79 --- /dev/null +++ b/crates/brain-brew-cli/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "brain-brew-cli" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +publish = false + +[[bin]] +name = "brainbrew" +path = "src/main.rs" + +[dependencies] +base64 = "0.22" +flate2 = "1" +nix-nar = "0.4" +brain-brew-core.workspace = true +brain-brew-formats.workspace = true +serde_json = "1" +sha2 = "0.10" +tar = "0.4" +tempfile = "3" +ureq = "2" + +[lints] +workspace = true diff --git a/crates/brain-brew-cli/src/args.rs b/crates/brain-brew-cli/src/args.rs new file mode 100644 index 0000000..2fb20f2 --- /dev/null +++ b/crates/brain-brew-cli/src/args.rs @@ -0,0 +1,335 @@ +use std::path::PathBuf; + +use brain_brew_core::{OverlayKind, StableId}; + +pub(crate) struct ManifestTargetArgs { + pub(crate) manifest_path: PathBuf, + pub(crate) target: String, + pub(crate) out_path: Option, + pub(crate) media_root: Option, + pub(crate) include_paths: Vec, + pub(crate) package_roots: Vec, +} + +pub(crate) struct VerifyArgs { + pub(crate) manifest_path: PathBuf, + pub(crate) target: Option, + pub(crate) all_targets: bool, + pub(crate) media_root: Option, + pub(crate) include_paths: Vec, + pub(crate) package_roots: Vec, +} + +pub(crate) struct ExportArgs { + pub(crate) overlay_paths: Vec, + pub(crate) out_path: Option, + pub(crate) media_root: Option, +} + +pub(crate) struct DiffOverlayArgs { + pub(crate) left_path: PathBuf, + pub(crate) right_path: PathBuf, + pub(crate) id: StableId, + pub(crate) kind: OverlayKind, +} + +pub(crate) struct TargetsArgs { + pub(crate) manifest_paths: Vec, + pub(crate) package_roots: Vec, +} + +pub(crate) fn split_json_flag(args: &[String]) -> (bool, Vec) { + let mut json_output = false; + let mut rest = Vec::new(); + for arg in args { + if arg == "--json" { + json_output = true; + } else { + rest.push(arg.clone()); + } + } + (json_output, rest) +} + +pub(crate) fn parse_targets_args(args: &[String]) -> Result { + let mut manifest_paths = Vec::new(); + let mut package_roots = Vec::new(); + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--manifest" | "--include" => { + let Some(path) = args.get(index + 1) else { + return Err(format!("{} requires a path", args[index])); + }; + manifest_paths.push(PathBuf::from(path)); + index += 2; + } + "--package-root" => { + let Some(path) = args.get(index + 1) else { + return Err("--package-root requires a path".to_owned()); + }; + package_roots.push(PathBuf::from(path)); + index += 2; + } + other => return Err(format!("unexpected targets argument {other:?}")), + } + } + if manifest_paths.is_empty() && package_roots.is_empty() { + manifest_paths.push(PathBuf::from("brainbrew.yaml")); + } + Ok(TargetsArgs { + manifest_paths, + package_roots, + }) +} + +pub(crate) fn parse_manifest_target_args(args: &[String]) -> Result { + let mut manifest_path = None; + let mut target = None; + let mut out_path = None; + let mut media_root = None; + let mut include_paths = Vec::new(); + let mut package_roots = Vec::new(); + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--manifest" => { + let Some(path) = args.get(index + 1) else { + return Err("--manifest requires a path".to_owned()); + }; + manifest_path = Some(PathBuf::from(path)); + index += 2; + } + "--target" => { + let Some(name) = args.get(index + 1) else { + return Err("--target requires a name".to_owned()); + }; + target = Some(name.clone()); + index += 2; + } + "--out" => { + let Some(path) = args.get(index + 1) else { + return Err("--out requires a path".to_owned()); + }; + out_path = Some(PathBuf::from(path)); + index += 2; + } + "--media-root" => { + let Some(path) = args.get(index + 1) else { + return Err("--media-root requires a path".to_owned()); + }; + media_root = Some(PathBuf::from(path)); + index += 2; + } + "--include" => { + let Some(path) = args.get(index + 1) else { + return Err("--include requires a path".to_owned()); + }; + include_paths.push(PathBuf::from(path)); + index += 2; + } + "--package-root" => { + let Some(path) = args.get(index + 1) else { + return Err("--package-root requires a path".to_owned()); + }; + package_roots.push(PathBuf::from(path)); + index += 2; + } + other => return Err(format!("unexpected argument {other:?}")), + } + } + let Some(target) = target else { + return Err("missing --target".to_owned()); + }; + Ok(ManifestTargetArgs { + manifest_path: manifest_path.unwrap_or_else(|| PathBuf::from("brainbrew.yaml")), + target, + out_path, + media_root, + include_paths, + package_roots, + }) +} + +pub(crate) fn parse_verify_args(args: &[String]) -> Result { + let mut manifest_path = None; + let mut target = None; + let mut all_targets = false; + let mut media_root = None; + let mut include_paths = Vec::new(); + let mut package_roots = Vec::new(); + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--manifest" => { + let Some(path) = args.get(index + 1) else { + return Err("--manifest requires a path".to_owned()); + }; + manifest_path = Some(PathBuf::from(path)); + index += 2; + } + "--target" => { + let Some(name) = args.get(index + 1) else { + return Err("--target requires a name".to_owned()); + }; + target = Some(name.clone()); + index += 2; + } + "--all-targets" => { + all_targets = true; + index += 1; + } + "--media-root" => { + let Some(path) = args.get(index + 1) else { + return Err("--media-root requires a path".to_owned()); + }; + media_root = Some(PathBuf::from(path)); + index += 2; + } + "--include" => { + let Some(path) = args.get(index + 1) else { + return Err("--include requires a path".to_owned()); + }; + include_paths.push(PathBuf::from(path)); + index += 2; + } + "--package-root" => { + let Some(path) = args.get(index + 1) else { + return Err("--package-root requires a path".to_owned()); + }; + package_roots.push(PathBuf::from(path)); + index += 2; + } + other => return Err(format!("unexpected verify argument {other:?}")), + } + } + if all_targets && target.is_some() { + return Err("choose --all-targets or --target, not both".to_owned()); + } + Ok(VerifyArgs { + manifest_path: manifest_path.unwrap_or_else(|| PathBuf::from("brainbrew.yaml")), + target, + all_targets, + media_root, + include_paths, + package_roots, + }) +} + +pub(crate) fn parse_overlay_and_optional_out( + args: &[String], +) -> Result<(Vec, Option), String> { + let export_args = parse_overlay_out_media(args)?; + if export_args.media_root.is_some() { + return Err("--media-root is only supported for media-aware commands".to_owned()); + } + Ok((export_args.overlay_paths, export_args.out_path)) +} + +pub(crate) fn parse_overlay_out_media(args: &[String]) -> Result { + let mut overlay_paths = Vec::new(); + let mut out_path = None; + let mut media_root = None; + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--overlay" => { + let Some(path) = args.get(index + 1) else { + return Err("--overlay requires a path".to_owned()); + }; + overlay_paths.push(path.clone()); + index += 2; + } + "--out" => { + let Some(path) = args.get(index + 1) else { + return Err("--out requires a path".to_owned()); + }; + out_path = Some(PathBuf::from(path)); + index += 2; + } + "--media-root" => { + let Some(path) = args.get(index + 1) else { + return Err("--media-root requires a path".to_owned()); + }; + media_root = Some(PathBuf::from(path)); + index += 2; + } + other => return Err(format!("unexpected argument {other:?}")), + } + } + Ok(ExportArgs { + overlay_paths, + out_path, + media_root, + }) +} + +pub(crate) fn parse_required_out(args: &[String]) -> Result { + let Some(index) = args.iter().position(|arg| arg == "--out") else { + return Err("missing --out".to_owned()); + }; + let Some(path) = args.get(index + 1) else { + return Err("--out requires a path".to_owned()); + }; + Ok(PathBuf::from(path)) +} + +pub(crate) fn parse_diff_overlay_args(args: &[String]) -> Result { + let mut paths = Vec::new(); + let mut id = None; + let mut kind = OverlayKind::Patch; + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--as-overlay" => index += 1, + "--id" => { + let Some(value) = args.get(index + 1) else { + return Err("--id requires an overlay stable id".to_owned()); + }; + id = Some(stable_id(value)?); + index += 2; + } + "--kind" => { + let Some(value) = args.get(index + 1) else { + return Err("--kind requires an overlay kind".to_owned()); + }; + kind = parse_overlay_kind(value)?; + index += 2; + } + other if !other.starts_with('-') => { + paths.push(PathBuf::from(other)); + index += 1; + } + other => return Err(format!("unexpected diff --as-overlay argument {other:?}")), + } + } + if paths.len() != 2 { + return Err( + "usage: brainbrew diff --as-overlay --id [--kind patch]" + .to_owned(), + ); + } + let Some(id) = id else { + return Err("diff --as-overlay requires --id".to_owned()); + }; + Ok(DiffOverlayArgs { + left_path: paths.remove(0), + right_path: paths.remove(0), + id, + kind, + }) +} + +fn parse_overlay_kind(value: &str) -> Result { + match value { + "translation" => Ok(OverlayKind::Translation), + "extension" => Ok(OverlayKind::Extension), + "patch" => Ok(OverlayKind::Patch), + "personal" => Ok(OverlayKind::Personal), + other => Err(format!("unknown overlay kind {other:?}")), + } +} + +fn stable_id(value: &str) -> Result { + StableId::new(value).map_err(|error| error.to_string()) +} diff --git a/crates/brain-brew-cli/src/commands/compose.rs b/crates/brain-brew-cli/src/commands/compose.rs new file mode 100644 index 0000000..6fec930 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/compose.rs @@ -0,0 +1,73 @@ +use std::fs; +use std::path::Path; + +use brain_brew_formats::canonical_yaml; + +use crate::args::{parse_manifest_target_args, parse_overlay_and_optional_out}; +use crate::help; +use crate::io::{read_and_compose_deck, read_and_compose_manifest_target_with_packages}; +use crate::output; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if args.len() == 1 && (args[0] == "--help" || args[0] == "-h") { + print!("{}", help::command("compose").expect("compose help exists")); + return Ok(()); + } + + if args + .iter() + .any(|arg| arg == "--manifest" || arg == "--target") + { + let manifest_args = parse_manifest_target_args(args)?; + let deck = read_and_compose_manifest_target_with_packages( + &manifest_args.manifest_path, + &manifest_args.target, + &manifest_args.include_paths, + &manifest_args.package_roots, + )?; + let yaml = canonical_yaml::to_string(&deck).map_err(|error| error.to_string())?; + if let Some(path) = manifest_args.out_path { + fs::write(&path, yaml).map_err(|error| format!("{}: {error}", path.display()))?; + let mut details = vec![ + ( + "manifest", + manifest_args.manifest_path.display().to_string(), + ), + ("target", manifest_args.target.clone()), + ("output", path.display().to_string()), + ]; + details.extend(output::deck_stats(&deck)); + output::print_success( + format!("composed target {}", manifest_args.target), + &details, + ); + } else { + print!("{yaml}"); + } + return Ok(()); + } + + if args.is_empty() { + return Err(help::usage_error( + "compose", + "usage: brainbrew compose [--overlay overlay.yaml ...] [--out resolved.yaml]", + )); + } + let deck_path = Path::new(&args[0]); + let (overlay_paths, out_path) = parse_overlay_and_optional_out(&args[1..])?; + let deck = read_and_compose_deck(deck_path, &overlay_paths)?; + let yaml = canonical_yaml::to_string(&deck).map_err(|error| error.to_string())?; + if let Some(path) = out_path { + fs::write(&path, yaml).map_err(|error| format!("{}: {error}", path.display()))?; + let mut details = vec![ + ("source", deck_path.display().to_string()), + ("overlays", overlay_paths.len().to_string()), + ("output", path.display().to_string()), + ]; + details.extend(output::deck_stats(&deck)); + output::print_success("composed deck", &details); + } else { + print!("{yaml}"); + } + Ok(()) +} diff --git a/crates/brain-brew-cli/src/commands/diff.rs b/crates/brain-brew-cli/src/commands/diff.rs new file mode 100644 index 0000000..8adeb50 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/diff.rs @@ -0,0 +1,33 @@ +use std::path::Path; + +use brain_brew_formats::canonical_yaml; + +use crate::args::{parse_diff_overlay_args, split_json_flag}; +use crate::io::read_deck; +use crate::output::{print_human_diff, print_json_diff}; +use crate::overlay_draft::draft_overlay_from_diff; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if args.iter().any(|arg| arg == "--as-overlay") { + let overlay_args = parse_diff_overlay_args(args)?; + let left = read_deck(&overlay_args.left_path)?; + let right = read_deck(&overlay_args.right_path)?; + let overlay = draft_overlay_from_diff(&left, &right, overlay_args.id, overlay_args.kind)?; + print!("{}", canonical_yaml::overlay_to_string(&overlay)); + return Ok(()); + } + + let (json_output, paths) = split_json_flag(args); + if paths.len() != 2 { + return Err("usage: brainbrew diff [--json]".to_owned()); + } + let left = read_deck(Path::new(&paths[0]))?; + let right = read_deck(Path::new(&paths[1]))?; + let diff = left.semantic_diff(&right); + if json_output { + print_json_diff(&diff); + } else { + print_human_diff(&diff); + } + Ok(()) +} diff --git a/crates/brain-brew-cli/src/commands/explain.rs b/crates/brain-brew-cli/src/commands/explain.rs new file mode 100644 index 0000000..d237bb4 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/explain.rs @@ -0,0 +1,122 @@ +use serde_json::json; + +use crate::args::{parse_manifest_target_args, split_json_flag}; +use crate::io::plan_manifest_target_with_packages; +use crate::output::{one_line, package_json, semantic_kind_name}; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + let (json_output, rest) = split_json_flag(args); + let manifest_args = parse_manifest_target_args(&rest)?; + let plan = plan_manifest_target_with_packages( + &manifest_args.manifest_path, + &manifest_args.target, + &manifest_args.include_paths, + &manifest_args.package_roots, + )?; + + if !json_output { + if let Some(package) = &plan.package { + println!("package: {}@{}", package.id, package.version); + } + println!("target: {}", plan.target); + println!("base: {}", plan.base_label); + println!("overlay stack:"); + if plan.overlays.is_empty() { + println!(" (none)"); + } else { + for (index, (overlay, _)) in plan.overlays.iter().enumerate() { + println!(" {}. {} ({})", index + 1, overlay.id, overlay.display_file); + } + } + } + + let overlay_stack = plan + .overlays + .iter() + .map(|(overlay, _)| json!({"id": overlay.id, "file": overlay.display_file})) + .collect::>(); + let overlays = plan + .overlays + .iter() + .map(|(_, overlay)| overlay.clone()) + .collect::>(); + match plan.base.compose(&overlays) { + Ok(deck) => { + let diff = plan.base.semantic_diff(&deck); + if json_output { + let changes = diff + .changes + .iter() + .map(|change| { + json!({ + "kind": semantic_kind_name(change.kind), + "path": change.path, + "before": change.before, + "after": change.after, + }) + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&json!({ + "package": plan.package.as_ref().map(package_json), + "target": plan.target, + "base": plan.base_label, + "overlay_stack": overlay_stack, + "changes": changes, + "errors": [], + })) + .unwrap() + ); + } else { + println!("changes:"); + if diff.is_empty() { + println!(" none"); + } else { + for change in diff.changes { + println!(" {} {}", semantic_kind_name(change.kind), change.path); + if let Some(before) = change.before { + println!(" before: {}", one_line(&before)); + } + if let Some(after) = change.after { + println!(" after: {}", one_line(&after)); + } + } + } + } + Ok(()) + } + Err(report) => { + if json_output { + let errors = report + .errors + .iter() + .map(|error| { + json!({ + "kind": format!("{:?}", error.kind), + "path": error.path, + "message": error.message, + }) + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&json!({ + "package": plan.package.as_ref().map(package_json), + "target": plan.target, + "base": plan.base_label, + "overlay_stack": overlay_stack, + "changes": [], + "errors": errors, + })) + .unwrap() + ); + } else { + for error in report.errors { + eprintln!("{:?} {}: {}", error.kind, error.path, error.message); + } + } + Err("target composition failed".to_owned()) + } + } +} diff --git a/crates/brain-brew-cli/src/commands/export.rs b/crates/brain-brew-cli/src/commands/export.rs new file mode 100644 index 0000000..65f7b6f --- /dev/null +++ b/crates/brain-brew-cli/src/commands/export.rs @@ -0,0 +1,109 @@ +use std::fs; +use std::path::Path; + +use brain_brew_core::CanonicalDeck; +use brain_brew_formats::crowdanki; + +use crate::args::{parse_manifest_target_args, parse_overlay_out_media}; +use crate::help; +use crate::io::{ + configured_crowdanki_out, manifest_root, read_and_compose_deck, + read_and_compose_manifest_target_with_packages, read_manifest, root_relative_path, +}; +use crate::media_assets::{copy_media_assets, validate_media_assets}; +use crate::output; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if matches!(args, [flag] if flag == "--help" || flag == "-h") + || matches!(args, [format, flag] if format == "crowdanki" && (flag == "--help" || flag == "-h")) + { + print!("{}", help::command("export").expect("export help exists")); + return Ok(()); + } + if args.first().map(String::as_str) != Some("crowdanki") { + return Err(help::usage_error( + "export", + "usage: brainbrew export crowdanki [--overlay overlay.yaml ...] --out build/deck-folder", + )); + } + if args + .iter() + .any(|arg| arg == "--manifest" || arg == "--target") + { + let manifest_args = parse_manifest_target_args(&args[1..])?; + let manifest = read_manifest(&manifest_args.manifest_path)?; + let root = manifest_root(&manifest_args.manifest_path); + let out_dir = if let Some(out_path) = manifest_args.out_path.clone() { + out_path + } else if let Some(path) = configured_crowdanki_out(&manifest, &manifest_args.target) { + root.join(path) + } else { + root.join("build") + .join("crowdanki") + .join(&manifest_args.target) + }; + let deck = read_and_compose_manifest_target_with_packages( + &manifest_args.manifest_path, + &manifest_args.target, + &manifest_args.include_paths, + &manifest_args.package_roots, + )?; + let media_root = manifest_args + .media_root + .as_ref() + .map(|path| root_relative_path(&root, path)); + return write_crowdanki_export(&deck, &out_dir, media_root.as_deref()); + } + + if args.len() < 4 { + return Err(help::usage_error( + "export", + "usage: brainbrew export crowdanki [--overlay overlay.yaml ...] --out build/deck-folder", + )); + } + let deck_path = Path::new(&args[1]); + let export_args = parse_overlay_out_media(&args[2..])?; + let Some(out_dir) = export_args.out_path else { + return Err("missing --out".to_owned()); + }; + + let deck = read_and_compose_deck(deck_path, &export_args.overlay_paths)?; + write_crowdanki_export(&deck, &out_dir, export_args.media_root.as_deref()) +} + +fn write_crowdanki_export( + deck: &CanonicalDeck, + out_dir: &Path, + media_root: Option<&Path>, +) -> Result<(), String> { + if let Some(media_root) = media_root { + validate_media_assets(deck, media_root)?; + } + let export = crowdanki::export_deck(deck).map_err(|error| error.to_string())?; + fs::create_dir_all(out_dir).map_err(|error| format!("{}: {error}", out_dir.display()))?; + fs::write(out_dir.join("deck.json"), export.deck_json) + .map_err(|error| format!("{}: {error}", out_dir.display()))?; + + if let Some(media_root) = media_root { + copy_media_assets(deck, media_root, out_dir)?; + } + + let mut details = vec![("output", out_dir.join("deck.json").display().to_string())]; + details.extend(output::deck_stats(deck)); + if let Some(media_root) = media_root { + details.push(("media root", media_root.display().to_string())); + } + if !export.omitted_tombstones.is_empty() { + details.push(( + "omitted tombstones", + export + .omitted_tombstones + .iter() + .map(ToString::to_string) + .collect::>() + .join(", "), + )); + } + output::print_success("exported CrowdAnki deck", &details); + Ok(()) +} diff --git a/crates/brain-brew-cli/src/commands/fmt.rs b/crates/brain-brew-cli/src/commands/fmt.rs new file mode 100644 index 0000000..b3b9799 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/fmt.rs @@ -0,0 +1,23 @@ +use std::fs; +use std::path::Path; + +use crate::help; +use crate::io::format_source; +use crate::output; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if args.len() == 1 && (args[0] == "--help" || args[0] == "-h") { + print!("{}", help::command("fmt").expect("fmt help exists")); + return Ok(()); + } + if args.len() != 1 { + return Err(help::usage_error("fmt", "usage: brainbrew fmt ")); + } + let path = Path::new(&args[0]); + let input = fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; + let formatted = + format_source(&input).map_err(|error| format!("{}: {error}", path.display()))?; + fs::write(path, formatted).map_err(|error| format!("{}: {error}", path.display()))?; + output::print_success("formatted source", &[("path", path.display().to_string())]); + Ok(()) +} diff --git a/crates/brain-brew-cli/src/commands/import.rs b/crates/brain-brew-cli/src/commands/import.rs new file mode 100644 index 0000000..fcd0723 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/import.rs @@ -0,0 +1,38 @@ +use std::fs; +use std::path::Path; + +use brain_brew_formats::{canonical_yaml, crowdanki}; + +use crate::args::parse_required_out; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if args.first().map(String::as_str) != Some("crowdanki") { + return Err( + "usage: brainbrew import crowdanki --accept-suggested-ids --out deck.yaml" + .to_owned(), + ); + } + if !args.iter().any(|arg| arg == "--accept-suggested-ids") { + return Err( + "non-interactive CrowdAnki import requires --accept-suggested-ids for now".to_owned(), + ); + } + if args.len() < 5 { + return Err( + "usage: brainbrew import crowdanki --accept-suggested-ids --out deck.yaml" + .to_owned(), + ); + } + + let deck_dir = Path::new(&args[1]); + let out_path = parse_required_out(&args[2..])?; + let deck_json_path = deck_dir.join("deck.json"); + let deck_json = fs::read_to_string(&deck_json_path) + .map_err(|error| format!("{}: {error}", deck_json_path.display()))?; + let deck = crowdanki::import_deck_accept_suggested_ids(&deck_json) + .map_err(|error| error.to_string())?; + let yaml = canonical_yaml::to_string(&deck).map_err(|error| error.to_string())?; + fs::write(&out_path, yaml).map_err(|error| format!("{}: {error}", out_path.display()))?; + println!("imported crowdanki deck"); + Ok(()) +} diff --git a/crates/brain-brew-cli/src/commands/lock.rs b/crates/brain-brew-cli/src/commands/lock.rs new file mode 100644 index 0000000..72cadd4 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/lock.rs @@ -0,0 +1,885 @@ +use std::collections::BTreeMap; +use std::env; +use std::fs; +use std::io::{Cursor, Read}; +use std::path::{Path, PathBuf}; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use brain_brew_formats::lockfile::{ + self, FederationLock, LockedPackage, LockedPackageMetadata, LockedSource, +}; +use flate2::read::GzDecoder; +use nix_nar::Encoder; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use tar::Archive; +use tempfile::TempDir; + +use crate::help; +use crate::io::read_manifest; +use crate::output; + +const USER_AGENT: &str = concat!("brainbrew/", env!("CARGO_PKG_VERSION")); + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if args.len() == 1 && (args[0] == "--help" || args[0] == "-h") { + print!("{}", help::command("lock").expect("lock help exists")); + return Ok(()); + } + let Some(subcommand) = args.first().map(String::as_str) else { + return Err(help::usage_error( + "lock", + "usage: brainbrew lock ", + )); + }; + match subcommand { + "update" => update(&args[1..]), + "verify" => verify(&args[1..]), + other => Err(format!("unknown lock subcommand {other:?}")), + } +} + +fn update(args: &[String]) -> Result<(), String> { + let args = parse_lock_update_args(args)?; + let requested = args.source.to_requested_source()?; + let fetched = fetch_source(&requested, None)?; + let package_manifest = fetched.source_path.join(&args.package_manifest); + let manifest = read_manifest(&package_manifest)?; + let package = manifest.package.as_ref().ok_or_else(|| { + format!( + "locked package source {} has no package metadata in {}", + fetched.source_path.display(), + args.package_manifest.display() + ) + })?; + if package.id != args.package_id { + return Err(format!( + "locked package source declares package id {}, expected {}", + package.id, args.package_id + )); + } + + let mut lock = read_lock_or_empty(&args.lock_path)?; + lock.packages.insert( + args.package_id.clone(), + LockedPackage { + manifest: args.package_manifest.display().to_string(), + package: LockedPackageMetadata { + version: package.version.clone(), + }, + original: Some(requested.original_source()), + locked: requested.locked_source(&fetched), + }, + ); + let formatted = lockfile::to_string(&lock); + if let Some(parent) = args.lock_path.parent() { + fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?; + } + fs::write(&args.lock_path, formatted) + .map_err(|error| format!("{}: {error}", args.lock_path.display()))?; + + output::print_success( + format!("updated lock package {}", args.package_id), + &[ + ("lock", args.lock_path.display().to_string()), + ("version", package.version.clone()), + ("nar_hash", fetched.nar_hash), + ], + ); + Ok(()) +} + +fn verify(args: &[String]) -> Result<(), String> { + let args = parse_lock_verify_args(args)?; + let lock = read_lock(&args.lock_path)?; + for (package_id, package) in &lock.packages { + let fetched = fetch_locked_source(&args.lock_path, package_id, &package.locked)?; + if let Some(expected_hash) = &package.locked.nar_hash + && &fetched.nar_hash != expected_hash + { + return Err(format!( + "locked package {package_id} nar_hash mismatch: expected {expected_hash}, found {}", + fetched.nar_hash + )); + } + let manifest_path = fetched.source_path.join(&package.manifest); + verify_locked_manifest_metadata(package_id, package, &manifest_path)?; + } + + let suffix = if lock.packages.len() == 1 { "" } else { "s" }; + output::print_success( + format!("verified {} locked package{suffix}", lock.packages.len()), + &[("lock", args.lock_path.display().to_string())], + ); + Ok(()) +} + +#[derive(Debug)] +struct LockUpdateArgs { + lock_path: PathBuf, + package_id: String, + package_manifest: PathBuf, + source: UpdateSource, +} + +#[derive(Debug)] +enum UpdateSource { + Path(PathBuf), + Git { + url: String, + reference: Option, + rev: Option, + }, + Tarball { + url: String, + }, +} + +impl UpdateSource { + fn to_requested_source(&self) -> Result { + match self { + Self::Path(path) => { + let path = canonicalize_for_lock(path)?; + Ok(RequestedSource { + source_type: "path".to_owned(), + url: None, + path: Some(path.display().to_string()), + reference: None, + rev: None, + }) + } + Self::Git { + url, + reference, + rev, + } => Ok(RequestedSource { + source_type: "git".to_owned(), + url: Some(url.clone()), + path: None, + reference: reference.clone(), + rev: rev.clone(), + }), + Self::Tarball { url } => Ok(RequestedSource { + source_type: "tarball".to_owned(), + url: Some(url.clone()), + path: None, + reference: None, + rev: None, + }), + } + } +} + +#[derive(Debug)] +struct LockVerifyArgs { + lock_path: PathBuf, +} + +fn parse_lock_update_args(args: &[String]) -> Result { + let mut lock_path = PathBuf::from("brainbrew.lock"); + let mut package_id = None; + let mut package_manifest = PathBuf::from("brainbrew.yaml"); + let mut path = None; + let mut git = None; + let mut tarball = None; + let mut reference = None; + let mut rev = None; + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--lock" => { + let Some(value) = args.get(index + 1) else { + return Err("--lock requires a path".to_owned()); + }; + lock_path = PathBuf::from(value); + index += 2; + } + "--package" => { + let Some(value) = args.get(index + 1) else { + return Err("--package requires a package id".to_owned()); + }; + package_id = Some(value.clone()); + index += 2; + } + "--package-manifest" => { + let Some(value) = args.get(index + 1) else { + return Err("--package-manifest requires a path".to_owned()); + }; + package_manifest = PathBuf::from(value); + index += 2; + } + "--path" => { + let Some(value) = args.get(index + 1) else { + return Err("--path requires a directory".to_owned()); + }; + path = Some(PathBuf::from(value)); + index += 2; + } + "--git" => { + let Some(value) = args.get(index + 1) else { + return Err("--git requires a URL".to_owned()); + }; + git = Some(value.clone()); + index += 2; + } + "--tarball" => { + let Some(value) = args.get(index + 1) else { + return Err("--tarball requires a URL".to_owned()); + }; + tarball = Some(value.clone()); + index += 2; + } + "--ref" => { + let Some(value) = args.get(index + 1) else { + return Err("--ref requires a ref".to_owned()); + }; + reference = Some(value.clone()); + index += 2; + } + "--rev" => { + let Some(value) = args.get(index + 1) else { + return Err("--rev requires a revision".to_owned()); + }; + rev = Some(value.clone()); + index += 2; + } + other => return Err(format!("unexpected lock update argument {other:?}")), + } + } + let Some(package_id) = package_id else { + return Err("lock update requires --package".to_owned()); + }; + let source_count = + usize::from(path.is_some()) + usize::from(git.is_some()) + usize::from(tarball.is_some()); + if source_count != 1 { + return Err("lock update requires exactly one of --path, --git, or --tarball".to_owned()); + } + let source = if let Some(path) = path { + if reference.is_some() || rev.is_some() { + return Err("--ref and --rev are only valid with --git".to_owned()); + } + UpdateSource::Path(path) + } else if let Some(url) = git { + UpdateSource::Git { + url, + reference, + rev, + } + } else { + if reference.is_some() || rev.is_some() { + return Err("--ref and --rev are only valid with --git".to_owned()); + } + UpdateSource::Tarball { + url: tarball.expect("source_count checked"), + } + }; + + Ok(LockUpdateArgs { + lock_path, + package_id, + package_manifest, + source, + }) +} + +fn parse_lock_verify_args(args: &[String]) -> Result { + let mut lock_path = PathBuf::from("brainbrew.lock"); + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--lock" => { + let Some(value) = args.get(index + 1) else { + return Err("--lock requires a path".to_owned()); + }; + lock_path = PathBuf::from(value); + index += 2; + } + other => return Err(format!("unexpected lock verify argument {other:?}")), + } + } + Ok(LockVerifyArgs { lock_path }) +} + +fn read_lock(path: &Path) -> Result { + let input = fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; + lockfile::from_str(&input).map_err(|error| error.to_string()) +} + +fn read_lock_or_empty(path: &Path) -> Result { + if path.exists() { + read_lock(path) + } else { + Ok(FederationLock { + version: 1, + packages: BTreeMap::new(), + }) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct RequestedSource { + pub(crate) source_type: String, + pub(crate) url: Option, + pub(crate) path: Option, + pub(crate) reference: Option, + pub(crate) rev: Option, +} + +impl RequestedSource { + fn original_source(&self) -> LockedSource { + LockedSource { + source_type: self.source_type.clone(), + url: self.url.clone(), + path: self.path.clone(), + reference: self.reference.clone(), + rev: self.rev.clone(), + nar_hash: None, + } + } + + fn locked_source(&self, fetched: &FetchedSource) -> LockedSource { + match self.source_type.as_str() { + "path" => LockedSource { + source_type: "path".to_owned(), + url: None, + path: self.path.clone(), + reference: None, + rev: None, + nar_hash: Some(fetched.nar_hash.clone()), + }, + "git" => LockedSource { + source_type: "git".to_owned(), + url: self.url.clone(), + path: None, + reference: None, + rev: fetched.rev.clone().or_else(|| self.rev.clone()), + nar_hash: Some(fetched.nar_hash.clone()), + }, + "tarball" => LockedSource { + source_type: "tarball".to_owned(), + url: self.url.clone(), + path: None, + reference: None, + rev: None, + nar_hash: Some(fetched.nar_hash.clone()), + }, + other => LockedSource { + source_type: other.to_owned(), + url: self.url.clone(), + path: self.path.clone(), + reference: self.reference.clone(), + rev: self.rev.clone(), + nar_hash: Some(fetched.nar_hash.clone()), + }, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct FetchedSource { + pub(crate) source_path: PathBuf, + pub(crate) nar_hash: String, + pub(crate) rev: Option, +} + +pub(crate) fn fetch_locked_source( + lock_path: &Path, + package_id: &str, + source: &LockedSource, +) -> Result { + let expected_hash = source.nar_hash.as_deref(); + if let Some(cached) = cached_source(expected_hash)? { + return Ok(cached); + } + + let requested = RequestedSource { + source_type: source.source_type.clone(), + url: source.url.clone(), + path: source + .path + .as_deref() + .map(|path| lock_relative_path(lock_path, path).display().to_string()), + reference: source.reference.clone(), + rev: source.rev.clone(), + }; + fetch_source(&requested, expected_hash) + .map_err(|error| format!("locked package {package_id}: {error}")) +} + +pub(crate) fn locked_package_manifest_paths(lock_path: &Path) -> Result, String> { + if !lock_path.exists() { + return Ok(Vec::new()); + } + let lock = read_lock(lock_path)?; + lock.packages + .iter() + .map(|(package_id, package)| { + let fetched = fetch_locked_source(lock_path, package_id, &package.locked)?; + if let Some(expected_hash) = &package.locked.nar_hash + && &fetched.nar_hash != expected_hash + { + return Err(format!( + "locked package {package_id} nar_hash mismatch: expected {expected_hash}, found {}", + fetched.nar_hash + )); + } + let manifest_path = fetched.source_path.join(&package.manifest); + verify_locked_manifest_metadata(package_id, package, &manifest_path)?; + Ok(manifest_path) + }) + .collect() +} + +fn fetch_source( + source: &RequestedSource, + expected_hash: Option<&str>, +) -> Result { + match source.source_type.as_str() { + "path" => { + let Some(path) = &source.path else { + return Err("path source requires path".to_owned()); + }; + snapshot_source_tree(Path::new(path), expected_hash, None) + } + "git" => fetch_git_source(source, expected_hash), + "tarball" => { + let Some(url) = &source.url else { + return Err("tarball source requires url".to_owned()); + }; + fetch_tarball_source(url, expected_hash, None) + } + other => Err(format!("unsupported locked source type {other:?}")), + } +} + +fn fetch_git_source( + source: &RequestedSource, + expected_hash: Option<&str>, +) -> Result { + let Some(url) = &source.url else { + return Err("git source requires url".to_owned()); + }; + let Some(repo) = GithubRepo::parse(url) else { + return Err(format!( + "native git locking currently supports GitHub HTTPS URLs; use --tarball for {url:?}" + )); + }; + let rev = if let Some(rev) = &source.rev { + rev.clone() + } else { + resolve_github_rev(&repo, source.reference.as_deref())? + }; + let tarball = repo.codeload_tarball_url(&rev); + fetch_tarball_source(&tarball, expected_hash, Some(rev)) +} + +fn fetch_tarball_source( + url: &str, + expected_hash: Option<&str>, + rev: Option, +) -> Result { + if let Some(cached) = cached_source(expected_hash)? { + return Ok(FetchedSource { rev, ..cached }); + } + + let bytes = read_url_or_file(url)?; + let extracted = TempDir::new().map_err(|error| error.to_string())?; + unpack_tarball(&bytes, extracted.path())?; + let source_root = normalized_extracted_root(extracted.path())?; + snapshot_source_tree(&source_root, expected_hash, rev) +} + +fn snapshot_source_tree( + source_path: &Path, + expected_hash: Option<&str>, + rev: Option, +) -> Result { + let source_path = source_path + .canonicalize() + .map_err(|error| format!("{}: {error}", source_path.display()))?; + if !source_path.is_dir() { + return Err(format!("{} is not a directory", source_path.display())); + } + + let staging = TempDir::new().map_err(|error| error.to_string())?; + let staged_source = staging.path().join("source"); + copy_source_tree_filtered(&source_path, &staged_source)?; + let nar_hash = nar_hash_path(&staged_source)?; + + if let Some(expected_hash) = expected_hash + && nar_hash != expected_hash + { + return Err(format!( + "nar_hash mismatch: expected {expected_hash}, found {nar_hash}" + )); + } + + let cache_path = cache_source_path(&nar_hash); + if !cache_path.exists() { + if let Some(parent) = cache_path.parent() { + fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?; + } + fs::rename(&staged_source, &cache_path).or_else(|_| { + copy_source_tree_filtered(&staged_source, &cache_path)?; + fs::remove_dir_all(&staged_source) + .map_err(|error| format!("{}: {error}", staged_source.display())) + })?; + } + + Ok(FetchedSource { + source_path: cache_path, + nar_hash, + rev, + }) +} + +fn cached_source(expected_hash: Option<&str>) -> Result, String> { + let Some(expected_hash) = expected_hash else { + return Ok(None); + }; + let path = cache_source_path(expected_hash); + if !path.exists() { + return Ok(None); + } + let actual_hash = nar_hash_path(&path)?; + if actual_hash == expected_hash { + return Ok(Some(FetchedSource { + source_path: path, + nar_hash: actual_hash, + rev: None, + })); + } + fs::remove_dir_all(&path).map_err(|error| format!("{}: {error}", path.display()))?; + Ok(None) +} + +fn read_url_or_file(url: &str) -> Result, String> { + if let Some(path) = url.strip_prefix("file://") { + return fs::read(path).map_err(|error| format!("{path}: {error}")); + } + let path = Path::new(url); + if path.exists() { + return fs::read(path).map_err(|error| format!("{}: {error}", path.display())); + } + + let response = ureq::get(url) + .set("User-Agent", USER_AGENT) + .call() + .map_err(|error| format!("failed to fetch {url}: {error}"))?; + let mut reader = response.into_reader(); + let mut bytes = Vec::new(); + reader + .read_to_end(&mut bytes) + .map_err(|error| format!("failed to read {url}: {error}"))?; + Ok(bytes) +} + +fn read_json_url(url: &str) -> Result { + let response = ureq::get(url) + .set("Accept", "application/vnd.github+json") + .set("User-Agent", USER_AGENT) + .call() + .map_err(|error| format!("failed to fetch {url}: {error}"))?; + let mut reader = response.into_reader(); + let mut body = String::new(); + reader + .read_to_string(&mut body) + .map_err(|error| format!("failed to read {url}: {error}"))?; + serde_json::from_str(&body).map_err(|error| format!("failed to parse {url} as JSON: {error}")) +} + +fn unpack_tarball(bytes: &[u8], destination: &Path) -> Result<(), String> { + if bytes.starts_with(&[0x1f, 0x8b]) { + let decoder = GzDecoder::new(Cursor::new(bytes)); + let mut archive = Archive::new(decoder); + archive + .unpack(destination) + .map_err(|error| format!("failed to unpack tar.gz: {error}"))?; + } else { + let mut archive = Archive::new(Cursor::new(bytes)); + archive + .unpack(destination) + .map_err(|error| format!("failed to unpack tar archive: {error}"))?; + } + Ok(()) +} + +fn normalized_extracted_root(path: &Path) -> Result { + let entries = fs::read_dir(path) + .map_err(|error| format!("{}: {error}", path.display()))? + .collect::, _>>() + .map_err(|error| error.to_string())?; + if entries.len() == 1 { + let only = entries[0].path(); + if only.is_dir() { + return Ok(only); + } + } + Ok(path.to_path_buf()) +} + +#[derive(Debug)] +struct GithubRepo { + owner: String, + name: String, +} + +impl GithubRepo { + fn parse(url: &str) -> Option { + let path = url + .strip_prefix("https://github.com/") + .or_else(|| url.strip_prefix("http://github.com/"))?; + let mut parts = path.trim_end_matches('/').split('/'); + let owner = parts.next()?.to_owned(); + let name = parts.next()?.trim_end_matches(".git").to_owned(); + if owner.is_empty() || name.is_empty() || parts.next().is_some() { + return None; + } + Some(Self { owner, name }) + } + + fn api_url(&self) -> String { + format!("https://api.github.com/repos/{}/{}", self.owner, self.name) + } + + fn commit_api_url(&self, reference: &str) -> String { + format!( + "https://api.github.com/repos/{}/{}/commits/{}", + self.owner, + self.name, + percent_encode_path_segment(reference) + ) + } + + fn codeload_tarball_url(&self, rev: &str) -> String { + format!( + "https://codeload.github.com/{}/{}/tar.gz/{}", + self.owner, self.name, rev + ) + } +} + +fn resolve_github_rev(repo: &GithubRepo, reference: Option<&str>) -> Result { + let reference = if let Some(reference) = reference { + reference.to_owned() + } else { + read_json_url(&repo.api_url())? + .get("default_branch") + .and_then(Value::as_str) + .ok_or_else(|| "GitHub repository response did not include default_branch".to_owned())? + .to_owned() + }; + read_json_url(&repo.commit_api_url(&reference))? + .get("sha") + .and_then(Value::as_str) + .map(str::to_owned) + .ok_or_else(|| format!("GitHub commit response did not include sha for {reference:?}")) +} + +fn percent_encode_path_segment(value: &str) -> String { + let mut encoded = String::new(); + for byte in value.bytes() { + if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~') { + encoded.push(byte as char); + } else { + encoded.push_str(&format!("%{byte:02X}")); + } + } + encoded +} + +fn verify_locked_manifest_metadata( + package_id: &str, + package: &LockedPackage, + manifest_path: &Path, +) -> Result<(), String> { + let manifest = read_manifest(manifest_path)?; + let metadata = manifest.package.as_ref().ok_or_else(|| { + format!( + "locked package {package_id} manifest {} has no package metadata", + manifest_path.display() + ) + })?; + if metadata.id != *package_id { + return Err(format!( + "locked package {package_id} manifest {} declares package id {}", + manifest_path.display(), + metadata.id + )); + } + if metadata.version != package.package.version { + return Err(format!( + "locked package {package_id} version mismatch: lock has {}, manifest has {}", + package.package.version, metadata.version + )); + } + Ok(()) +} + +fn nar_hash_path(path: &Path) -> Result { + let mut encoder = Encoder::new(path).map_err(|error| format!("{}: {error}", path.display()))?; + let mut hasher = Sha256::new(); + let mut buffer = [0_u8; 64 * 1024]; + loop { + let count = encoder + .read(&mut buffer) + .map_err(|error| format!("failed to encode {} as NAR: {error}", path.display()))?; + if count == 0 { + break; + } + hasher.update(&buffer[..count]); + } + Ok(format!( + "sha256-{}", + BASE64_STANDARD.encode(hasher.finalize()) + )) +} + +fn copy_source_tree_filtered(source: &Path, destination: &Path) -> Result<(), String> { + if destination.exists() { + fs::remove_dir_all(destination) + .map_err(|error| format!("{}: {error}", destination.display()))?; + } + fs::create_dir_all(destination) + .map_err(|error| format!("{}: {error}", destination.display()))?; + copy_dir_contents(source, destination) +} + +fn copy_dir_contents(source: &Path, destination: &Path) -> Result<(), String> { + for entry in fs::read_dir(source).map_err(|error| format!("{}: {error}", source.display()))? { + let entry = entry.map_err(|error| error.to_string())?; + let file_name = entry.file_name(); + if should_skip_source_entry(&file_name.to_string_lossy()) { + continue; + } + let source_path = entry.path(); + let destination_path = destination.join(&file_name); + let metadata = fs::symlink_metadata(&source_path) + .map_err(|error| format!("{}: {error}", source_path.display()))?; + if metadata.file_type().is_symlink() { + copy_symlink(&source_path, &destination_path)?; + } else if metadata.is_dir() { + fs::create_dir_all(&destination_path) + .map_err(|error| format!("{}: {error}", destination_path.display()))?; + copy_dir_contents(&source_path, &destination_path)?; + } else if metadata.is_file() { + fs::copy(&source_path, &destination_path).map_err(|error| { + format!( + "failed to copy {} to {}: {error}", + source_path.display(), + destination_path.display() + ) + })?; + let permissions = metadata.permissions(); + fs::set_permissions(&destination_path, permissions) + .map_err(|error| format!("{}: {error}", destination_path.display()))?; + } + } + Ok(()) +} + +fn should_skip_source_entry(name: &str) -> bool { + matches!(name, ".git" | ".jj" | ".hg" | ".svn" | "target" | "result") + || name.starts_with("result-") +} + +#[cfg(unix)] +fn copy_symlink(source: &Path, destination: &Path) -> Result<(), String> { + use std::os::unix::fs::symlink; + + let target = fs::read_link(source).map_err(|error| format!("{}: {error}", source.display()))?; + symlink(&target, destination).map_err(|error| { + format!( + "failed to copy symlink {} to {}: {error}", + source.display(), + destination.display() + ) + }) +} + +#[cfg(windows)] +fn copy_symlink(source: &Path, destination: &Path) -> Result<(), String> { + use std::os::windows::fs::{symlink_dir, symlink_file}; + + let target = fs::read_link(source).map_err(|error| format!("{}: {error}", source.display()))?; + let metadata = + fs::metadata(source).map_err(|error| format!("{}: {error}", source.display()))?; + if metadata.is_dir() { + symlink_dir(&target, destination) + } else { + symlink_file(&target, destination) + } + .map_err(|error| { + format!( + "failed to copy symlink {} to {}: {error}", + source.display(), + destination.display() + ) + }) +} + +fn canonicalize_for_lock(path: &Path) -> Result { + path.canonicalize() + .map_err(|error| format!("{}: {error}", path.display())) +} + +fn lock_relative_path(lock_path: &Path, path: &str) -> PathBuf { + let path = PathBuf::from(path); + if path.is_absolute() { + path + } else { + lock_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .join(path) + } +} + +fn cache_source_path(nar_hash: &str) -> PathBuf { + cache_root().join("sources").join(cache_key(nar_hash)) +} + +fn cache_key(value: &str) -> String { + value + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { + ch + } else { + '_' + } + }) + .collect() +} + +fn cache_root() -> PathBuf { + if let Some(path) = env::var_os("BRAINBREW_CACHE_DIR") { + return PathBuf::from(path); + } + + #[cfg(windows)] + { + if let Some(path) = env::var_os("LOCALAPPDATA") { + return PathBuf::from(path).join("BrainBrew").join("cache"); + } + } + + #[cfg(target_os = "macos")] + { + if let Some(home) = env::var_os("HOME") { + return PathBuf::from(home) + .join("Library") + .join("Caches") + .join("brainbrew"); + } + } + + if let Some(path) = env::var_os("XDG_CACHE_HOME") { + return PathBuf::from(path).join("brainbrew"); + } + if let Some(home) = env::var_os("HOME") { + return PathBuf::from(home).join(".cache").join("brainbrew"); + } + env::temp_dir().join("brainbrew-cache") +} diff --git a/crates/brain-brew-cli/src/commands/mod.rs b/crates/brain-brew-cli/src/commands/mod.rs new file mode 100644 index 0000000..d9fc279 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/mod.rs @@ -0,0 +1,10 @@ +pub(crate) mod compose; +pub(crate) mod diff; +pub(crate) mod explain; +pub(crate) mod export; +pub(crate) mod fmt; +pub(crate) mod import; +pub(crate) mod lock; +pub(crate) mod targets; +pub(crate) mod validate; +pub(crate) mod verify; diff --git a/crates/brain-brew-cli/src/commands/targets.rs b/crates/brain-brew-cli/src/commands/targets.rs new file mode 100644 index 0000000..ad24d5c --- /dev/null +++ b/crates/brain-brew-cli/src/commands/targets.rs @@ -0,0 +1,78 @@ +use serde_json::json; + +use crate::args::{parse_targets_args, split_json_flag}; +use crate::commands::lock::locked_package_manifest_paths; +use crate::io::{manifest_root, read_manifest, target_package_json}; +use crate::output::package_json; +use crate::package_resolver::{discover_package_manifests, validate_package_dependencies}; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + let (json_output, rest) = split_json_flag(args); + let target_args = parse_targets_args(&rest)?; + let mut manifest_paths = target_args.manifest_paths; + manifest_paths.extend(discover_package_manifests(&target_args.package_roots)?); + manifest_paths.sort(); + manifest_paths.dedup(); + let lock_manifest_paths = manifest_paths + .iter() + .map(|path| locked_package_manifest_paths(&manifest_root(path).join("brainbrew.lock"))) + .collect::, String>>()? + .into_iter() + .flatten() + .collect::>(); + let has_lock_manifest_paths = !lock_manifest_paths.is_empty(); + manifest_paths.extend(lock_manifest_paths); + manifest_paths.sort(); + manifest_paths.dedup(); + let manifests = manifest_paths + .iter() + .map(|path| Ok((path, read_manifest(path)?))) + .collect::, String>>()?; + if !target_args.package_roots.is_empty() || has_lock_manifest_paths { + validate_package_dependencies( + &manifests + .iter() + .map(|(path, manifest)| (*path, manifest)) + .collect::>(), + )?; + } + + if json_output { + let packages = manifests + .iter() + .map(|(path, manifest)| target_package_json(path, manifest)) + .collect::, String>>()?; + let package = manifests + .first() + .and_then(|(_, manifest)| manifest.package.as_ref()) + .map(package_json); + let targets = packages + .iter() + .flat_map(|package| package["targets"].as_array().cloned().unwrap_or_default()) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty( + &json!({"package": package, "targets": targets, "packages": packages}) + ) + .unwrap() + ); + } else { + let qualify = manifests.len() > 1; + for (_, manifest) in manifests { + let prefix = manifest.package.as_ref().map(|package| package.id.as_str()); + for target in manifest.targets.keys() { + if qualify { + if let Some(prefix) = prefix { + println!("{prefix}:{target}"); + } else { + println!("{target}"); + } + } else { + println!("{target}"); + } + } + } + } + Ok(()) +} diff --git a/crates/brain-brew-cli/src/commands/validate.rs b/crates/brain-brew-cli/src/commands/validate.rs new file mode 100644 index 0000000..de57a9b --- /dev/null +++ b/crates/brain-brew-cli/src/commands/validate.rs @@ -0,0 +1,128 @@ +use std::path::Path; + +use brain_brew_core::ValidationReport; +use serde_json::json; + +use crate::args::{parse_manifest_target_args, split_json_flag}; +use crate::help; +use crate::io::{read_and_compose_deck, read_and_compose_manifest_target_with_packages}; +use crate::output; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if args + .iter() + .any(|arg| arg == "--manifest" || arg == "--target") + { + let (json_output, rest) = split_json_flag(args); + let manifest_args = parse_manifest_target_args(&rest)?; + let deck = read_and_compose_manifest_target_with_packages( + &manifest_args.manifest_path, + &manifest_args.target, + &manifest_args.include_paths, + &manifest_args.package_roots, + )?; + let mut details = vec![ + ( + "manifest", + manifest_args.manifest_path.display().to_string(), + ), + ("target", manifest_args.target.clone()), + ]; + details.extend(output::deck_stats(&deck)); + return report_validation( + deck.validate(), + json_output, + format!("valid target {}", manifest_args.target), + details, + ); + } + + let mut json_output = false; + let mut deck_path = None; + let mut overlay_paths = Vec::new(); + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--json" => { + json_output = true; + index += 1; + } + "--overlay" => { + let Some(path) = args.get(index + 1) else { + return Err("--overlay requires a path".to_owned()); + }; + overlay_paths.push(path.clone()); + index += 2; + } + value if deck_path.is_none() => { + deck_path = Some(value.to_owned()); + index += 1; + } + other => return Err(format!("unexpected validate argument {other:?}")), + } + } + let Some(deck_path) = deck_path else { + return Err(help::usage_error( + "validate", + "usage: brainbrew validate [--overlay overlay.yaml ...] [--json]", + )); + }; + + let deck = read_and_compose_deck(Path::new(&deck_path), &overlay_paths)?; + let mut details = vec![("source", deck_path.clone())]; + if !overlay_paths.is_empty() { + details.push(("overlays", overlay_paths.len().to_string())); + } + details.extend(output::deck_stats(&deck)); + report_validation( + deck.validate(), + json_output, + "valid deck".to_owned(), + details, + ) +} + +fn report_validation( + result: Result<(), ValidationReport>, + json_output: bool, + success_message: String, + details: Vec<(&str, String)>, +) -> Result<(), String> { + match result { + Ok(()) => { + if json_output { + println!( + "{}", + serde_json::to_string_pretty(&json!({"status": "valid", "errors": []})) + .unwrap() + ); + } else { + output::print_success(success_message, &details); + } + Ok(()) + } + Err(report) => { + if json_output { + let errors = report + .errors + .iter() + .map(|error| { + json!({ + "kind": format!("{:?}", error.kind), + "path": error.path, + "message": error.message, + }) + }) + .collect::>(); + eprintln!( + "{}", + serde_json::to_string_pretty(&json!({"status": "invalid", "errors": errors})) + .unwrap() + ); + } else { + eprintln!("{report}"); + } + Err("invalid deck".to_owned()) + } + } +} diff --git a/crates/brain-brew-cli/src/commands/verify.rs b/crates/brain-brew-cli/src/commands/verify.rs new file mode 100644 index 0000000..6188c09 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/verify.rs @@ -0,0 +1,120 @@ +use std::fs; +use std::path::Path; + +use brain_brew_core::CanonicalDeck; +use brain_brew_formats::{crowdanki, manifest}; + +use crate::args::parse_verify_args; +use crate::help; +use crate::io::{ + manifest_root, plan_manifest_target_with_packages, read_manifest, root_relative_path, + verify_canonical_deck_format, verify_manifest_format, verify_overlay_format, +}; +use crate::media_assets::validate_media_assets; +use crate::output; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if args.len() == 1 && (args[0] == "--help" || args[0] == "-h") { + print!("{}", help::command("verify").expect("verify help exists")); + return Ok(()); + } + let verify_args = parse_verify_args(args)?; + verify_manifest_format(&verify_args.manifest_path)?; + let manifest = read_manifest(&verify_args.manifest_path)?; + let root = manifest_root(&verify_args.manifest_path); + let target_names = if verify_args.all_targets { + manifest.targets.keys().cloned().collect::>() + } else if let Some(target) = verify_args.target { + vec![target] + } else { + return Err(help::usage_error( + "verify", + "usage: brainbrew verify [--manifest brainbrew.yaml] --all-targets", + )); + }; + + verify_canonical_deck_format(&root.join(&manifest.base))?; + let media_root = verify_args + .media_root + .as_ref() + .map(|path| root_relative_path(&root, path)); + for target in &target_names { + let plan = plan_manifest_target_with_packages( + &verify_args.manifest_path, + target, + &verify_args.include_paths, + &verify_args.package_roots, + )?; + for (overlay, _) in &plan.overlays { + verify_overlay_format(&overlay.file)?; + } + let deck = plan.compose()?; + deck.validate().map_err(|error| error.to_string())?; + if let Some(media_root) = &media_root { + validate_media_assets(&deck, media_root)?; + } + verify_configured_exports(&root, &manifest, target, &deck)?; + } + + let suffix = if target_names.len() == 1 { "" } else { "s" }; + let mut details = vec![("manifest", verify_args.manifest_path.display().to_string())]; + if let Some(media_root) = &media_root { + details.push(("media root", media_root.display().to_string())); + } + output::print_success( + format!("verified {} target{suffix}", target_names.len()), + &details, + ); + Ok(()) +} + +fn verify_configured_exports( + root: &Path, + manifest: &manifest::FederatedDeckManifest, + target: &str, + deck: &CanonicalDeck, +) -> Result<(), String> { + let Some(target_entry) = manifest.targets.get(target) else { + return Ok(()); + }; + let Some(export) = &target_entry.exports.crowdanki else { + return Ok(()); + }; + if let Some(golden) = &export.golden { + verify_crowdanki_golden(root, target, golden, &export.golden_allowlist, deck)?; + } + Ok(()) +} + +fn verify_crowdanki_golden( + root: &Path, + target: &str, + golden: &str, + golden_allowlist: &[String], + deck: &CanonicalDeck, +) -> Result<(), String> { + let mut golden_path = root.join(golden); + if golden_path.is_dir() { + golden_path = golden_path.join("deck.json"); + } + let expected = fs::read_to_string(&golden_path) + .map_err(|error| format!("{}: {error}", golden_path.display()))?; + let actual = crowdanki::export_deck(deck) + .map_err(|error| error.to_string())? + .deck_json; + let expected_json = serde_json::from_str::(&expected) + .map_err(|error| format!("{}: {error}", golden_path.display()))?; + let actual_json = + serde_json::from_str::(&actual).expect("CrowdAnki export is valid JSON"); + let options = crowdanki::CrowdAnkiParityOptions { + allowed_path_globs: golden_allowlist.iter().cloned().collect(), + }; + if let Err(report) = crowdanki::compare_deck_json_values(&expected_json, &actual_json, &options) + { + return Err(format!( + "CrowdAnki golden mismatch for target {target}: {}\n{report}", + golden_path.display() + )); + } + Ok(()) +} diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs new file mode 100644 index 0000000..b5f6598 --- /dev/null +++ b/crates/brain-brew-cli/src/help.rs @@ -0,0 +1,73 @@ +pub(crate) fn general() -> String { + format!( + concat!( + "Brain Brew {}\n", + "Local-first deck federation and round-trip tooling for Anki-compatible decks.\n\n", + "Usage:\n", + " brainbrew [options]\n\n", + "Commands:\n", + " fmt Format deck, overlay, manifest, or lock YAML in place\n", + " validate Validate a deck file or manifest target\n", + " compose Compose a base deck plus overlays into resolved CanonicalDeck YAML\n", + " export Export a resolved deck to an adapter format, currently CrowdAnki\n", + " import Import CrowdAnki into CanonicalDeck YAML\n", + " lock Update or verify locked federated package inputs\n", + " targets List manifest targets\n", + " verify Run manifest formatting, composition, validation, media, and golden checks\n", + " explain Explain a manifest target and its overlay stack\n", + " diff Compare decks semantically, or emit an overlay draft\n\n", + "Examples:\n", + " brainbrew targets --manifest brainbrew.yaml\n", + " brainbrew validate --manifest brainbrew.yaml --target da-standard\n", + " brainbrew compose --manifest brainbrew.yaml --target da-standard --out build/da.yaml\n", + " brainbrew compose --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n", + " brainbrew lock update --package anki-geo.ultimate-geography --path ../ultimate-geography\n", + " brainbrew export crowdanki --manifest brainbrew.yaml --target de-extended --media-root media/\n", + " brainbrew verify --manifest brainbrew.yaml --all-targets\n\n", + "Run `brainbrew --help` for command-specific examples.\n", + ), + env!("CARGO_PKG_VERSION") + ) +} + +pub(crate) fn command(name: &str) -> Option<&'static str> { + match name { + "fmt" => Some( + "Usage:\n brainbrew fmt \n\nExamples:\n brainbrew fmt deck.yaml\n brainbrew fmt overlays/languages/da.yaml\n brainbrew fmt brainbrew.yaml\n brainbrew fmt brainbrew.lock\n", + ), + "validate" => Some( + "Usage:\n brainbrew validate [--overlay overlay.yaml ...] [--json]\n brainbrew validate --manifest brainbrew.yaml --target [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew validate deck.yaml\n brainbrew validate deck.yaml --overlay overlays/languages/da.yaml\n brainbrew validate --manifest brainbrew.yaml --target da-standard\n brainbrew validate --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew validate --manifest brainbrew.yaml --target de-extended --json\n", + ), + "compose" => Some( + "Usage:\n brainbrew compose [--overlay overlay.yaml ...] [--out resolved.yaml]\n brainbrew compose [--manifest brainbrew.yaml] --target [--include package/brainbrew.yaml ...] [--package-root packages/] [--out resolved.yaml]\n\nExamples:\n brainbrew compose deck.yaml --overlay overlays/languages/da.yaml --out build/da.yaml\n brainbrew compose --manifest brainbrew.yaml --target da-standard --out build/da.yaml\n brainbrew compose --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew compose --manifest brainbrew.yaml --target da-standard\n", + ), + "export" => Some( + "Usage:\n brainbrew export crowdanki [--overlay overlay.yaml ...] --out build/deck-folder\n brainbrew export crowdanki [--manifest brainbrew.yaml] --target [--include package/brainbrew.yaml ...] [--package-root packages/] [--out build/deck-folder] [--media-root media/]\n\nExamples:\n brainbrew export crowdanki deck.yaml --overlay overlays/languages/da.yaml --out build/da-crowdanki\n brainbrew export crowdanki --manifest brainbrew.yaml --target da-standard\n brainbrew export crowdanki --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america --out build/en-america\n brainbrew export crowdanki --manifest brainbrew.yaml --target de-extended --media-root media/\n\nWhen --out is omitted for a manifest target, the output defaults to build/crowdanki/ unless exports.crowdanki.out is configured.\n", + ), + "import" => Some( + "Usage:\n brainbrew import crowdanki --accept-suggested-ids --out deck.yaml\n\nExamples:\n brainbrew import crowdanki build/de-extended --accept-suggested-ids --out deck.yaml\n", + ), + "lock" => Some( + "Usage:\n brainbrew lock update --package (--path

| --git [--ref ] [--rev ] | --tarball ) [--package-manifest brainbrew.yaml] [--lock brainbrew.lock]\n brainbrew lock verify [--lock brainbrew.lock]\n\nExamples:\n brainbrew lock update --package anki-geo.ultimate-geography --path ../ultimate-geography\n brainbrew lock update --package anki-geo.ultimate-geography --git https://github.com/anki-geo/ultimate-geography.git --ref main\n brainbrew lock verify\n", + ), + "targets" => Some( + "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", + ), + "verify" => Some( + "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n", + ), + "explain" => Some( + "Usage:\n brainbrew explain [--manifest brainbrew.yaml] --target [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew explain --manifest brainbrew.yaml --target da-standard\n brainbrew explain --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew explain --manifest brainbrew.yaml --target de-extended --json\n", + ), + "diff" => Some( + "Usage:\n brainbrew diff [--json]\n brainbrew diff --as-overlay --id [--kind patch]\n\nExamples:\n brainbrew diff deck.yaml edited.yaml\n brainbrew diff deck.yaml edited.yaml --json\n brainbrew diff deck.yaml edited.yaml --as-overlay --id overlay.patch.capitals --kind patch\n", + ), + _ => None, + } +} + +pub(crate) fn usage_error(command: &str, fallback: &str) -> String { + self::command(command) + .map(str::to_owned) + .unwrap_or_else(|| fallback.to_owned()) +} diff --git a/crates/brain-brew-cli/src/io.rs b/crates/brain-brew-cli/src/io.rs new file mode 100644 index 0000000..a03f31b --- /dev/null +++ b/crates/brain-brew-cli/src/io.rs @@ -0,0 +1,482 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +use brain_brew_core::{CanonicalDeck, Overlay}; +use brain_brew_formats::{canonical_yaml, lockfile, manifest}; +use serde_json::json; + +use crate::commands::lock::locked_package_manifest_paths; +use crate::output::package_json; +use crate::package_resolver::{discover_package_manifests, validate_package_dependencies}; + +pub(crate) fn format_source(input: &str) -> Result { + let mut errors = Vec::new(); + match canonical_yaml::format_str(input) { + Ok(formatted) => return Ok(formatted), + Err(error) => errors.push(format!("deck: {error}")), + } + match canonical_yaml::overlay_format_str(input) { + Ok(formatted) => return Ok(formatted), + Err(error) => errors.push(format!("overlay: {error}")), + } + match manifest::format_str(input) { + Ok(formatted) => return Ok(formatted), + Err(error) => errors.push(format!("manifest: {error}")), + } + match lockfile::format_str(input) { + Ok(formatted) => return Ok(formatted), + Err(error) => errors.push(format!("lockfile: {error}")), + } + Err(format!( + "unrecognized Brain Brew source file ({})", + errors.join("; ") + )) +} + +pub(crate) fn read_deck(path: &Path) -> Result { + let input = fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; + canonical_yaml::from_str(&input).map_err(|error| error.to_string()) +} + +pub(crate) fn read_overlay(path: &Path) -> Result { + let input = fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; + canonical_yaml::overlay_from_str(&input).map_err(|error| error.to_string()) +} + +pub(crate) fn read_manifest(path: &Path) -> Result { + let input = fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; + manifest::from_str(&input).map_err(|error| error.to_string()) +} + +pub(crate) fn read_and_compose_deck( + deck_path: &Path, + overlay_paths: &[String], +) -> Result { + let deck = read_deck(deck_path)?; + let overlays = overlay_paths + .iter() + .map(|path| read_overlay(Path::new(path))) + .collect::, _>>()?; + deck.compose(&overlays).map_err(|error| error.to_string()) +} + +pub(crate) fn read_and_compose_manifest_target_with_packages( + manifest_path: &Path, + target: &str, + include_paths: &[PathBuf], + package_roots: &[PathBuf], +) -> Result { + let plan = + plan_manifest_target_with_packages(manifest_path, target, include_paths, package_roots)?; + plan.compose() +} + +#[derive(Clone)] +pub(crate) struct PlannedOverlay { + pub(crate) id: String, + pub(crate) file: PathBuf, + pub(crate) display_file: String, +} + +pub(crate) struct ManifestTargetPlan { + pub(crate) package: Option, + pub(crate) target: String, + pub(crate) base_label: String, + pub(crate) base: CanonicalDeck, + pub(crate) overlays: Vec<(PlannedOverlay, Overlay)>, +} + +impl ManifestTargetPlan { + pub(crate) fn compose(&self) -> Result { + self.base + .compose( + &self + .overlays + .iter() + .map(|(_, overlay)| overlay.clone()) + .collect::>(), + ) + .map_err(|error| error.to_string()) + } +} + +pub(crate) fn plan_manifest_target_with_packages( + manifest_path: &Path, + target: &str, + include_paths: &[PathBuf], + package_roots: &[PathBuf], +) -> Result { + let registry = ManifestRegistry::load(manifest_path, include_paths, package_roots)?; + registry.plan_root_target(target) +} + +struct LoadedManifest { + path: PathBuf, + root: PathBuf, + manifest: manifest::FederatedDeckManifest, +} + +struct ManifestRegistry { + root_index: usize, + manifests: Vec, + packages: BTreeMap, +} + +impl ManifestRegistry { + fn load( + manifest_path: &Path, + include_paths: &[PathBuf], + package_roots: &[PathBuf], + ) -> Result { + let mut paths = vec![manifest_path.to_path_buf()]; + paths.extend(include_paths.iter().cloned()); + paths.extend(discover_package_manifests(package_roots)?); + paths.sort(); + paths.dedup(); + + let lock_path = manifest_root(manifest_path).join("brainbrew.lock"); + let locked_paths = locked_package_manifest_paths(&lock_path)?; + let has_locked_paths = !locked_paths.is_empty(); + paths.extend(locked_paths); + paths.sort(); + paths.dedup(); + + let root_path = manifest_path.to_path_buf(); + let root_index = paths + .iter() + .position(|path| path == &root_path) + .unwrap_or(0); + let manifests = paths + .iter() + .map(|path| { + let manifest = read_manifest(path)?; + Ok(LoadedManifest { + path: path.clone(), + root: manifest_root(path), + manifest, + }) + }) + .collect::, String>>()?; + if !package_roots.is_empty() || has_locked_paths { + validate_package_dependencies( + &manifests + .iter() + .map(|loaded| (&loaded.path, &loaded.manifest)) + .collect::>(), + )?; + } + + let mut packages = BTreeMap::new(); + for (index, loaded) in manifests.iter().enumerate() { + let Some(package) = &loaded.manifest.package else { + continue; + }; + if let Some(previous) = packages.insert(package.id.clone(), index) { + return Err(format!( + "duplicate package id {} in {} and {}", + package.id, + manifests[previous].path.display(), + loaded.path.display() + )); + } + } + + Ok(Self { + root_index, + manifests, + packages, + }) + } + + fn plan_root_target(&self, target: &str) -> Result { + self.plan_target(self.root_index, target, &mut Vec::new()) + } + + fn plan_target( + &self, + manifest_index: usize, + target: &str, + stack: &mut Vec<(usize, String)>, + ) -> Result { + if stack + .iter() + .any(|(index, name)| *index == manifest_index && name == target) + { + return Err(format!("manifest target dependency cycle at {target}")); + } + stack.push((manifest_index, target.to_owned())); + + let loaded = &self.manifests[manifest_index]; + let target_entry = loaded.manifest.targets.get(target).ok_or_else(|| { + format!( + "manifest target {target:?} does not exist in {}; available targets: {}", + loaded.path.display(), + loaded + .manifest + .targets + .keys() + .cloned() + .collect::>() + .join(", ") + ) + })?; + + let (base_label, base) = if let Some(extends) = &target_entry.extends { + let (base_manifest_index, base_target) = + self.resolve_target_ref(manifest_index, extends)?; + let base_plan = self.plan_target(base_manifest_index, base_target, stack)?; + (extends.clone(), base_plan.compose()?) + } else { + ( + loaded.manifest.base.clone(), + read_deck(&loaded.root.join(&loaded.manifest.base))?, + ) + }; + + let planned_overlays = self.expand_target_overlays(manifest_index, target)?; + let overlays = planned_overlays + .into_iter() + .map(|planned| { + let overlay = read_overlay(&planned.file)?; + Ok((planned, overlay)) + }) + .collect::, String>>()?; + + stack.pop(); + Ok(ManifestTargetPlan { + package: loaded.manifest.package.clone(), + target: target.to_owned(), + base_label, + base, + overlays, + }) + } + + fn expand_target_overlays( + &self, + manifest_index: usize, + target: &str, + ) -> Result, String> { + let loaded = &self.manifests[manifest_index]; + let target_entry = loaded + .manifest + .targets + .get(target) + .ok_or_else(|| format!("manifest target {target:?} does not exist"))?; + let mut visited = BTreeSet::new(); + let mut stack = Vec::new(); + let mut expanded = Vec::new(); + for overlay in &target_entry.overlays { + self.visit_overlay_ref( + manifest_index, + overlay, + &mut visited, + &mut stack, + &mut expanded, + )?; + } + Ok(expanded) + } + + fn visit_overlay_ref( + &self, + current_manifest_index: usize, + overlay_ref: &str, + visited: &mut BTreeSet<(usize, String)>, + stack: &mut Vec<(usize, String)>, + expanded: &mut Vec, + ) -> Result<(), String> { + let (manifest_index, overlay_id) = + self.resolve_overlay_ref(current_manifest_index, overlay_ref)?; + let key = (manifest_index, overlay_id.to_owned()); + if visited.contains(&key) { + return Ok(()); + } + if stack.contains(&key) { + return Err(format!( + "manifest overlay dependency cycle at {overlay_ref}" + )); + } + let loaded = &self.manifests[manifest_index]; + let entry = loaded.manifest.overlays.get(overlay_id).ok_or_else(|| { + format!( + "manifest overlay {overlay_id:?} does not exist in {}", + loaded.path.display() + ) + })?; + stack.push(key.clone()); + for dependency in &entry.depends_on { + self.visit_overlay_ref(manifest_index, dependency, visited, stack, expanded)?; + } + stack.pop(); + + visited.insert(key); + let should_qualify = manifest_index != self.root_index; + let display_file = if should_qualify { + if let Some(package) = &loaded.manifest.package { + format!("{}:{}", package.id, entry.file) + } else { + entry.file.clone() + } + } else { + entry.file.clone() + }; + let id = if should_qualify { + if let Some(package) = &loaded.manifest.package { + format!("{}:{}", package.id, overlay_id) + } else { + overlay_id.to_owned() + } + } else { + overlay_id.to_owned() + }; + expanded.push(PlannedOverlay { + id, + file: loaded.root.join(&entry.file), + display_file, + }); + Ok(()) + } + + fn resolve_target_ref<'a>( + &self, + current_manifest_index: usize, + target_ref: &'a str, + ) -> Result<(usize, &'a str), String> { + if let Some((package_id, target)) = target_ref.split_once(':') { + let Some(index) = self.packages.get(package_id) else { + return Err(format!( + "package {package_id:?} required by target ref {target_ref:?} was not included" + )); + }; + Ok((*index, target)) + } else { + Ok((current_manifest_index, target_ref)) + } + } + + fn resolve_overlay_ref<'a>( + &self, + current_manifest_index: usize, + overlay_ref: &'a str, + ) -> Result<(usize, &'a str), String> { + if let Some((package_id, overlay_id)) = overlay_ref.split_once(':') { + let Some(index) = self.packages.get(package_id) else { + return Err(format!( + "package {package_id:?} required by overlay ref {overlay_ref:?} was not included" + )); + }; + Ok((*index, overlay_id)) + } else { + Ok((current_manifest_index, overlay_ref)) + } + } +} + +pub(crate) fn target_package_json( + path: &Path, + manifest: &manifest::FederatedDeckManifest, +) -> Result { + let targets = manifest + .targets + .keys() + .map(|target| { + let expanded = expand_manifest_target(manifest, target)?; + let overlays = expanded + .overlays + .iter() + .map(|overlay| json!({"id": overlay.id, "file": overlay.file})) + .collect::>(); + let qualified_name = manifest + .package + .as_ref() + .map(|package| format!("{}:{target}", package.id)); + Ok(json!({ + "name": target, + "qualified_name": qualified_name, + "extends": manifest.targets[target].extends.as_ref(), + "overlays": overlays, + })) + }) + .collect::, String>>()?; + Ok(json!({ + "manifest": path.display().to_string(), + "package": manifest.package.as_ref().map(package_json), + "targets": targets, + })) +} + +pub(crate) fn expand_manifest_target( + manifest: &manifest::FederatedDeckManifest, + target: &str, +) -> Result { + manifest.expand_target(target).map_err(|error| match error { + manifest::ManifestError::MissingTarget(_) => format!( + "manifest target {target:?} does not exist; available targets: {}", + manifest + .targets + .keys() + .cloned() + .collect::>() + .join(", ") + ), + other => other.to_string(), + }) +} + +pub(crate) fn verify_canonical_deck_format(path: &Path) -> Result<(), String> { + verify_format_with(path, canonical_yaml::format_str) +} + +pub(crate) fn verify_overlay_format(path: &Path) -> Result<(), String> { + verify_format_with(path, canonical_yaml::overlay_format_str) +} + +pub(crate) fn verify_manifest_format(path: &Path) -> Result<(), String> { + verify_format_with(path, manifest::format_str) +} + +fn verify_format_with( + path: &Path, + format: impl FnOnce(&str) -> Result, +) -> Result<(), String> +where + E: ToString, +{ + let input = fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; + let formatted = format(&input).map_err(|error| error.to_string())?; + if formatted != input { + return Err(format!("{} is not in canonical format", path.display())); + } + Ok(()) +} + +pub(crate) fn root_relative_path(root: &Path, path: &Path) -> PathBuf { + if path.is_absolute() { + path.to_path_buf() + } else { + root.join(path) + } +} + +pub(crate) fn configured_crowdanki_out( + manifest: &manifest::FederatedDeckManifest, + target: &str, +) -> Option { + manifest + .targets + .get(target)? + .exports + .crowdanki + .as_ref()? + .out + .as_deref() + .map(PathBuf::from) +} + +pub(crate) fn manifest_root(path: &Path) -> PathBuf { + path.parent() + .unwrap_or_else(|| Path::new(".")) + .to_path_buf() +} diff --git a/crates/brain-brew-cli/src/main.rs b/crates/brain-brew-cli/src/main.rs new file mode 100644 index 0000000..d50489f --- /dev/null +++ b/crates/brain-brew-cli/src/main.rs @@ -0,0 +1,63 @@ +//! Command-line entry point for Brain Brew. + +use std::env; +use std::process; + +mod args; +mod commands; +mod help; +mod io; +mod media_assets; +mod output; +mod overlay_draft; +mod package_resolver; + +fn main() { + if let Err(error) = run() { + output::print_error(&error); + process::exit(1); + } +} + +fn run() -> Result<(), String> { + let args = env::args().skip(1).collect::>(); + let Some(command) = args.first().map(String::as_str) else { + print_usage(); + return Ok(()); + }; + + if args + .get(1) + .is_some_and(|arg| arg == "--help" || arg == "-h") + && let Some(command_help) = help::command(command) + { + print!("{command_help}"); + return Ok(()); + } + + match command { + "fmt" => commands::fmt::run(&args[1..]), + "validate" => commands::validate::run(&args[1..]), + "compose" => commands::compose::run(&args[1..]), + "export" => commands::export::run(&args[1..]), + "import" => commands::import::run(&args[1..]), + "lock" => commands::lock::run(&args[1..]), + "targets" => commands::targets::run(&args[1..]), + "verify" => commands::verify::run(&args[1..]), + "explain" => commands::explain::run(&args[1..]), + "diff" => commands::diff::run(&args[1..]), + "--help" | "-h" => { + print_usage(); + Ok(()) + } + "--version" | "-V" => { + println!("brainbrew {}", env!("CARGO_PKG_VERSION")); + Ok(()) + } + other => Err(format!("unknown command {other:?}")), + } +} + +fn print_usage() { + print!("{}", help::general()); +} diff --git a/crates/brain-brew-cli/src/media_assets.rs b/crates/brain-brew-cli/src/media_assets.rs new file mode 100644 index 0000000..5d79c3c --- /dev/null +++ b/crates/brain-brew-cli/src/media_assets.rs @@ -0,0 +1,58 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::{Component, Path, PathBuf}; + +use brain_brew_core::CanonicalDeck; +use brain_brew_formats::media; + +pub(crate) fn validate_media_assets(deck: &CanonicalDeck, media_root: &Path) -> Result<(), String> { + media::validate_references(deck).map_err(|error| error.to_string())?; + let assets = read_media_assets(deck, media_root)?; + media::validate_hashes(deck, &assets).map_err(|error| error.to_string()) +} + +fn read_media_assets( + deck: &CanonicalDeck, + media_root: &Path, +) -> Result>, String> { + let mut assets = BTreeMap::new(); + for media in deck.media.values() { + let relative_path = safe_media_relative_path(&media.path)?; + let full_path = media_root.join(&relative_path); + let bytes = + fs::read(&full_path).map_err(|error| format!("{}: {error}", full_path.display()))?; + assets.insert(media.path.clone(), bytes); + } + Ok(assets) +} + +pub(crate) fn copy_media_assets( + deck: &CanonicalDeck, + media_root: &Path, + out_dir: &Path, +) -> Result<(), String> { + for media in deck.media.values() { + let relative_path = safe_media_relative_path(&media.path)?; + let source = media_root.join(&relative_path); + let destination = out_dir.join("media").join(&relative_path); + if let Some(parent) = destination.parent() { + fs::create_dir_all(parent).map_err(|error| format!("{}: {error}", parent.display()))?; + } + fs::copy(&source, &destination).map_err(|error| { + format!("{} -> {}: {error}", source.display(), destination.display()) + })?; + } + Ok(()) +} + +fn safe_media_relative_path(path: &str) -> Result { + let path = Path::new(path); + if path.is_absolute() + || path + .components() + .any(|component| matches!(component, Component::ParentDir | Component::Prefix(_))) + { + return Err(format!("unsafe media path {}", path.display())); + } + Ok(path.to_path_buf()) +} diff --git a/crates/brain-brew-cli/src/output.rs b/crates/brain-brew-cli/src/output.rs new file mode 100644 index 0000000..32668fa --- /dev/null +++ b/crates/brain-brew-cli/src/output.rs @@ -0,0 +1,174 @@ +use std::env; +use std::io::{self, IsTerminal}; + +use brain_brew_core::{CanonicalDeck, SemanticChangeKind, SemanticDiff}; +use brain_brew_formats::manifest; +use serde_json::json; + +pub(crate) fn print_json_diff(diff: &SemanticDiff) { + let changes = diff + .changes + .iter() + .map(|change| { + json!({ + "kind": semantic_kind_name(change.kind), + "path": change.path, + "before": change.before, + "after": change.after, + }) + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&json!({"changes": changes})).unwrap() + ); +} + +pub(crate) fn print_human_diff(diff: &SemanticDiff) { + if diff.is_empty() { + println!("{} no semantic changes", success_marker()); + return; + } + + let suffix = if diff.changes.len() == 1 { "" } else { "s" }; + println!("{} semantic change{suffix}", diff.changes.len()); + + for change in &diff.changes { + println!(); + println!("{} {}", change_marker(change.kind), change.path); + print_change_values(change); + } +} + +pub(crate) fn print_success(message: impl AsRef, details: &[(&str, String)]) { + println!("{} {}", success_marker(), message.as_ref()); + for (label, value) in details { + println!(" {}: {}", subtle(label), value); + } +} + +pub(crate) fn print_error(message: &str) { + eprintln!("{}", error_text(message)); +} + +pub(crate) fn deck_stats(deck: &CanonicalDeck) -> Vec<(&'static str, String)> { + vec![ + ("deck", deck.name.clone()), + ("notes", deck.notes.len().to_string()), + ("note types", deck.note_types.len().to_string()), + ("card templates", card_template_count(deck).to_string()), + ("media references", deck.media.len().to_string()), + ] +} + +pub(crate) fn semantic_kind_name(kind: SemanticChangeKind) -> &'static str { + match kind { + SemanticChangeKind::Added => "added", + SemanticChangeKind::Removed => "removed", + SemanticChangeKind::Modified => "modified", + SemanticChangeKind::Tombstoned => "tombstoned", + } +} + +pub(crate) fn package_json(package: &manifest::PackageMetadata) -> serde_json::Value { + json!({ + "id": package.id, + "version": package.version, + "compatible_base_versions": package.compatible_base_versions, + "depends_on": package.depends_on, + }) +} + +pub(crate) fn one_line(value: &str) -> String { + value.replace('\n', "\\n") +} + +fn print_change_values(change: &brain_brew_core::SemanticChange) { + match (&change.before, &change.after) { + (Some(before), Some(after)) => { + print_value_lines('-', before); + print_value_lines('+', after); + } + (Some(before), None) => print_value_lines('-', before), + (None, Some(after)) => print_value_lines('+', after), + (None, None) => println!(" {} entity", semantic_kind_name(change.kind)), + } +} + +fn print_value_lines(prefix: char, value: &str) { + let marker = match prefix { + '-' => removed_marker(), + '+' => added_marker(), + _ => prefix.to_string(), + }; + if value.contains('\n') { + println!(" {marker} |"); + for line in value.lines() { + println!(" {line}"); + } + } else if value.is_empty() { + println!(" {marker} \"\""); + } else { + println!(" {marker} {value}"); + } +} + +fn card_template_count(deck: &CanonicalDeck) -> usize { + deck.note_types + .values() + .map(|note_type| note_type.card_templates.len()) + .sum() +} + +fn success_marker() -> String { + color_stdout("✓", "32") +} + +fn change_marker(kind: SemanticChangeKind) -> String { + match kind { + SemanticChangeKind::Added => added_marker(), + SemanticChangeKind::Removed => removed_marker(), + SemanticChangeKind::Modified => color_stdout("~", "33"), + SemanticChangeKind::Tombstoned => color_stdout("×", "31"), + } +} + +fn added_marker() -> String { + color_stdout("+", "32") +} + +fn removed_marker() -> String { + color_stdout("-", "31") +} + +fn subtle(text: &str) -> String { + color_stdout(text, "2") +} + +fn error_text(text: &str) -> String { + color_stderr(text, "31") +} + +fn color_stdout(text: &str, code: &str) -> String { + if color_enabled(io::stdout().is_terminal()) { + format!("\x1b[{code}m{text}\x1b[0m") + } else { + text.to_owned() + } +} + +fn color_stderr(text: &str, code: &str) -> String { + if color_enabled(io::stderr().is_terminal()) { + format!("\x1b[{code}m{text}\x1b[0m") + } else { + text.to_owned() + } +} + +fn color_enabled(is_terminal: bool) -> bool { + match env::var("BRAINBREW_COLOR") { + Ok(value) if value == "always" => true, + Ok(value) if value == "never" => false, + _ => env::var_os("NO_COLOR").is_none() && is_terminal, + } +} diff --git a/crates/brain-brew-cli/src/overlay_draft.rs b/crates/brain-brew-cli/src/overlay_draft.rs new file mode 100644 index 0000000..53d6fc3 --- /dev/null +++ b/crates/brain-brew-cli/src/overlay_draft.rs @@ -0,0 +1,299 @@ +use std::collections::BTreeMap; + +use brain_brew_core::{ + AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplateChange, ChangeIntent, DeckChange, + ExpectedBase, FieldChange, FieldDefinitionChange, MediaChange, MediaReference, NoteChange, + NoteTypeChange, Overlay, OverlayKind, PropertyChange, StableId, TagChange, +}; + +pub(crate) fn draft_overlay_from_diff( + left: &CanonicalDeck, + right: &CanonicalDeck, + id: StableId, + kind: OverlayKind, +) -> Result { + let mut overlay = Overlay { + id, + kind, + translations: None, + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + draft_deck_changes(left, right, &mut overlay); + draft_note_type_adapter_changes(left, right, &mut overlay); + draft_note_changes(left, right, &mut overlay); + draft_media_changes(left, right, &mut overlay); + + if overlay.deck_change.is_none() + && overlay.note_changes.is_empty() + && overlay.note_type_changes.is_empty() + && overlay.media_changes.is_empty() + && !left.semantic_diff(right).is_empty() + { + return Err( + "diff --as-overlay currently supports deck name/description, adapter IDs, tags, media references, note additions/removals, and existing note field changes" + .to_owned(), + ); + } + + Ok(overlay) +} + +fn draft_deck_changes(left: &CanonicalDeck, right: &CanonicalDeck, overlay: &mut Overlay) { + let mut deck_change = DeckChange { + name: None, + description: None, + variables: BTreeMap::new(), + adapter_ids: adapter_id_changes(&left.adapter_ids, &right.adapter_ids), + }; + if left.name != right.name { + deck_change.name = Some(replace_property_change(&left.name, &right.name)); + } + if left.description != right.description { + deck_change.description = Some(replace_property_change( + &left.description, + &right.description, + )); + } + if deck_change.name.is_some() + || deck_change.description.is_some() + || !deck_change.adapter_ids.is_empty() + { + overlay.deck_change = Some(deck_change); + } +} + +fn draft_note_type_adapter_changes( + left: &CanonicalDeck, + right: &CanonicalDeck, + overlay: &mut Overlay, +) { + for (note_type_id, left_note_type) in &left.note_types { + let Some(right_note_type) = right.note_types.get(note_type_id) else { + continue; + }; + let adapter_ids = + adapter_id_changes(&left_note_type.adapter_ids, &right_note_type.adapter_ids); + if !adapter_ids.is_empty() { + overlay.note_type_changes.insert( + note_type_id.clone(), + NoteTypeChange { + intent: ChangeIntent::Merge, + note_type: None, + name: None, + variables: BTreeMap::new(), + styling: None, + fields: BTreeMap::::new(), + card_templates: BTreeMap::::new(), + adapter_ids, + expected_base: None, + }, + ); + } + } +} + +fn draft_note_changes(left: &CanonicalDeck, right: &CanonicalDeck, overlay: &mut Overlay) { + for (note_id, left_note) in &left.notes { + if !right.notes.contains_key(note_id) { + overlay.note_changes.insert( + note_id.clone(), + NoteChange { + intent: ChangeIntent::Remove, + note: None, + variables: BTreeMap::new(), + fields: BTreeMap::new(), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: Some(ExpectedBase::EntityPresent), + }, + ); + continue; + } + + let right_note = &right.notes[note_id]; + let mut fields = BTreeMap::new(); + for (field_id, left_value) in &left_note.fields { + let Some(right_value) = right_note.fields.get(field_id) else { + continue; + }; + if left_value != right_value { + fields.insert( + field_id.clone(), + FieldChange { + intent: ChangeIntent::Replace, + value: Some(right_value.clone()), + expected_base: Some(ExpectedBase::Value(left_value.clone())), + }, + ); + } + } + + let mut tags = BTreeMap::new(); + for tag in left_note.tags.difference(&right_note.tags) { + tags.insert( + tag.clone(), + TagChange { + intent: ChangeIntent::Remove, + expected_base: Some(ExpectedBase::EntityPresent), + }, + ); + } + for tag in right_note.tags.difference(&left_note.tags) { + tags.insert( + tag.clone(), + TagChange { + intent: ChangeIntent::Add, + expected_base: None, + }, + ); + } + + let adapter_ids = adapter_id_changes(&left_note.adapter_ids, &right_note.adapter_ids); + + if !fields.is_empty() || !tags.is_empty() || !adapter_ids.is_empty() { + overlay.note_changes.insert( + note_id.clone(), + NoteChange { + intent: ChangeIntent::Merge, + note: None, + variables: BTreeMap::new(), + fields, + tags, + adapter_ids, + expected_base: None, + }, + ); + } + } + + for (note_id, right_note) in &right.notes { + if !left.notes.contains_key(note_id) { + overlay.note_changes.insert( + note_id.clone(), + NoteChange { + intent: ChangeIntent::Add, + note: Some(right_note.clone()), + variables: BTreeMap::new(), + fields: BTreeMap::new(), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + ); + } + } +} + +fn draft_media_changes(left: &CanonicalDeck, right: &CanonicalDeck, overlay: &mut Overlay) { + for (media_id, left_media) in &left.media { + match right.media.get(media_id) { + Some(right_media) if left_media != right_media => { + overlay.media_changes.insert( + media_id.clone(), + MediaChange { + intent: ChangeIntent::Replace, + media: Some(right_media.clone()), + expected_base: Some(ExpectedBase::Value(media_reference_summary( + left_media, + ))), + }, + ); + } + Some(_) => {} + None => { + overlay.media_changes.insert( + media_id.clone(), + MediaChange { + intent: ChangeIntent::Remove, + media: None, + expected_base: Some(ExpectedBase::Value(media_reference_summary( + left_media, + ))), + }, + ); + } + } + } + + for (media_id, right_media) in &right.media { + if !left.media.contains_key(media_id) { + overlay.media_changes.insert( + media_id.clone(), + MediaChange { + intent: ChangeIntent::Add, + media: Some(right_media.clone()), + expected_base: None, + }, + ); + } + } +} + +fn adapter_id_changes(left: &AdapterIds, right: &AdapterIds) -> BTreeMap { + let left = left + .iter() + .map(|(key, value)| (key.to_owned(), value.to_owned())) + .collect::>(); + let right = right + .iter() + .map(|(key, value)| (key.to_owned(), value.to_owned())) + .collect::>(); + let mut changes = BTreeMap::new(); + + for (key, left_value) in &left { + match right.get(key) { + Some(right_value) if left_value != right_value => { + changes.insert( + key.clone(), + AdapterIdChange { + intent: ChangeIntent::Replace, + value: Some(right_value.clone()), + expected_base: Some(ExpectedBase::Value(left_value.clone())), + }, + ); + } + Some(_) => {} + None => { + changes.insert( + key.clone(), + AdapterIdChange { + intent: ChangeIntent::Remove, + value: None, + expected_base: Some(ExpectedBase::Value(left_value.clone())), + }, + ); + } + } + } + + for (key, right_value) in &right { + if !left.contains_key(key) { + changes.insert( + key.clone(), + AdapterIdChange { + intent: ChangeIntent::Add, + value: Some(right_value.clone()), + expected_base: None, + }, + ); + } + } + + changes +} + +fn media_reference_summary(media: &MediaReference) -> String { + format!("path={};sha256={}", media.path, media.sha256) +} + +fn replace_property_change(before: &str, after: &str) -> PropertyChange { + PropertyChange { + intent: ChangeIntent::Replace, + value: Some(after.to_owned()), + expected_base: Some(ExpectedBase::Value(before.to_owned())), + } +} diff --git a/crates/brain-brew-cli/src/package_resolver.rs b/crates/brain-brew-cli/src/package_resolver.rs new file mode 100644 index 0000000..9648106 --- /dev/null +++ b/crates/brain-brew-cli/src/package_resolver.rs @@ -0,0 +1,93 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +use brain_brew_formats::manifest; + +/// Discover local Federated Deck package manifests and validate their package dependencies. +pub(crate) fn discover_package_manifests(roots: &[PathBuf]) -> Result, String> { + let mut paths = BTreeSet::new(); + for root in roots { + collect_manifests(root, &mut paths)?; + } + Ok(paths.into_iter().collect()) +} + +pub(crate) fn validate_package_dependencies( + packages: &[(&PathBuf, &manifest::FederatedDeckManifest)], +) -> Result<(), String> { + let mut by_id = BTreeMap::new(); + for (path, manifest) in packages { + let Some(package) = &manifest.package else { + continue; + }; + if let Some(previous) = by_id.insert(package.id.clone(), (*path, package.version.clone())) { + return Err(format!( + "duplicate package id {} in {} and {}", + package.id, + previous.0.display(), + path.display() + )); + } + } + + for (path, manifest) in packages { + let Some(package) = &manifest.package else { + continue; + }; + for dependency in &package.depends_on { + let (dependency_id, expected_version) = parse_dependency(dependency); + let Some((dependency_path, actual_version)) = by_id.get(dependency_id) else { + return Err(format!( + "package dependency {dependency_id} required by {} in {} was not found", + package.id, + path.display() + )); + }; + if let Some(expected_version) = expected_version + && actual_version != expected_version + { + return Err(format!( + "package dependency {dependency_id}@{expected_version} required by {} in {} resolved to version {} in {}", + package.id, + path.display(), + actual_version, + dependency_path.display() + )); + } + } + } + + Ok(()) +} + +fn collect_manifests(root: &Path, paths: &mut BTreeSet) -> Result<(), String> { + let metadata = fs::metadata(root).map_err(|error| format!("{}: {error}", root.display()))?; + if metadata.is_file() { + if root.file_name().and_then(|name| name.to_str()) == Some("brainbrew.yaml") { + paths.insert(root.to_path_buf()); + } + return Ok(()); + } + + let manifest = root.join("brainbrew.yaml"); + if manifest.exists() { + paths.insert(manifest); + } + + for entry in fs::read_dir(root).map_err(|error| format!("{}: {error}", root.display()))? { + let entry = entry.map_err(|error| error.to_string())?; + let path = entry.path(); + if path.is_dir() { + collect_manifests(&path, paths)?; + } + } + + Ok(()) +} + +fn parse_dependency(dependency: &str) -> (&str, Option<&str>) { + dependency + .split_once('@') + .map_or((dependency, None), |(id, version)| (id, Some(version))) +} diff --git a/crates/brain-brew-core/Cargo.toml b/crates/brain-brew-core/Cargo.toml new file mode 100644 index 0000000..f031610 --- /dev/null +++ b/crates/brain-brew-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "brain-brew-core" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs new file mode 100644 index 0000000..5502d30 --- /dev/null +++ b/crates/brain-brew-core/src/lib.rs @@ -0,0 +1,2720 @@ +//! Pure domain model and behavior for Brain Brew. +//! +//! This crate intentionally contains no file formats, filesystem access, terminal UI, +//! or command-line concerns. It owns the CanonicalDeck domain model, validation, +//! composition, and semantic diffing as they are introduced through TDD. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +/// Name of the core crate. +pub const CRATE_NAME: &str = env!("CARGO_PKG_NAME"); + +/// Human-readable identity for a deck entity inside a CanonicalDeck. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct StableId(String); + +impl StableId { + /// Create a stable ID after checking the conservative CanonicalDeck ID syntax. + pub fn new(value: impl Into) -> Result { + let value = value.into(); + let is_valid = !value.is_empty() + && value + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-' | ':')); + + if is_valid { + Ok(Self(value)) + } else { + Err(InvalidStableId { value }) + } + } + + /// Borrow the stable ID as text. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for StableId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Error returned when text is not a valid stable ID. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InvalidStableId { + value: String, +} + +impl InvalidStableId { + /// The rejected stable ID text. + pub fn value(&self) -> &str { + &self.value + } +} + +impl fmt::Display for InvalidStableId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid stable id {:?}", self.value) + } +} + +impl std::error::Error for InvalidStableId {} + +/// Adapter-specific identities keyed by adapter namespace. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct AdapterIds(BTreeMap); + +impl AdapterIds { + /// Create an empty adapter ID collection. + pub fn new() -> Self { + Self::default() + } + + /// Insert an adapter identity. + pub fn insert(&mut self, key: impl Into, value: impl Into) -> Option { + self.0.insert(key.into(), value.into()) + } + + /// Look up an adapter identity by namespace key. + pub fn get(&self, key: &str) -> Option<&str> { + self.0.get(key).map(String::as_str) + } + + /// Returns true when an adapter identity exists for the namespace key. + pub fn contains_key(&self, key: &str) -> bool { + self.0.contains_key(key) + } + + /// Remove an adapter identity by namespace key. + pub fn remove(&mut self, key: &str) -> Option { + self.0.remove(key) + } + + /// Iterate adapter identities in deterministic key order. + pub fn iter(&self) -> impl Iterator { + self.0 + .iter() + .map(|(key, value)| (key.as_str(), value.as_str())) + } + + /// Returns true when no adapter identities are present. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// The format-independent representation of a deck's content and structure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CanonicalDeck { + pub id: StableId, + pub name: String, + pub description: String, + pub variables: BTreeMap, + pub note_types: BTreeMap, + pub notes: BTreeMap, + pub media: BTreeMap, + pub tombstones: BTreeSet, + pub adapter_ids: AdapterIds, +} + +impl CanonicalDeck { + /// Apply an ordered overlay stack to this base deck. + pub fn compose(&self, overlays: &[Overlay]) -> Result { + let mut resolved = self.clone(); + let mut errors = Vec::new(); + let mut changed_paths = BTreeMap::::new(); + + for overlay in overlays { + apply_overlay(&mut resolved, overlay, &mut changed_paths, &mut errors); + } + + if !errors.is_empty() { + return Err(ComposeReport { errors }); + } + + resolved.validate().map_err(|report| ComposeReport { + errors: report + .errors + .into_iter() + .map(|error| { + ComposeError::new( + ComposeErrorKind::ValidationFailed, + error.path, + error.message, + ) + }) + .collect(), + })?; + + Ok(resolved) + } + + /// Compare this deck with another deck by stable IDs and deck entities. + pub fn semantic_diff(&self, other: &Self) -> SemanticDiff { + let mut changes = Vec::new(); + + push_modified_if_changed( + &mut changes, + "deck.name".to_owned(), + &self.name, + &other.name, + ); + push_modified_if_changed( + &mut changes, + "deck.description".to_owned(), + &self.description, + &other.description, + ); + push_modified_if_changed( + &mut changes, + "deck.variables".to_owned(), + &string_map_summary(&self.variables), + &string_map_summary(&other.variables), + ); + + diff_note_types(&self.note_types, &other.note_types, &mut changes); + diff_notes(&self.notes, &other.notes, &mut changes); + diff_media(&self.media, &other.media, &mut changes); + diff_tombstones(&self.tombstones, &other.tombstones, &mut changes); + + SemanticDiff { changes } + } + + /// Render `${variable}` references in deck text using deck, note type, card template, and note scopes. + pub fn render_variables(&self) -> Result { + render_deck_variables(self) + } + + /// Validate strict core invariants that do not require filesystem or format access. + pub fn validate(&self) -> Result<(), ValidationReport> { + let mut errors = Vec::new(); + + for (id, note_type) in &self.note_types { + if ¬e_type.id != id { + errors.push(ValidationError::new( + ValidationErrorKind::MismatchedEntityId, + format!("note_types.{id}.id"), + format!("note type stored under {id} contains id {}", note_type.id), + )); + } + + push_duplicate_id_errors( + note_type.fields.iter().map(|field| &field.id), + ValidationErrorKind::DuplicateFieldDefinition, + |duplicate_id| format!("note_types.{id}.fields.{duplicate_id}"), + &mut errors, + ); + + push_duplicate_id_errors( + note_type.card_templates.iter().map(|template| &template.id), + ValidationErrorKind::DuplicateCardTemplate, + |duplicate_id| format!("note_types.{id}.card_templates.{duplicate_id}"), + &mut errors, + ); + } + + for (id, media) in &self.media { + if &media.id != id { + errors.push(ValidationError::new( + ValidationErrorKind::MismatchedEntityId, + format!("media.{id}.id"), + format!("media stored under {id} contains id {}", media.id), + )); + } + } + + for (id, note) in &self.notes { + if ¬e.id != id { + errors.push(ValidationError::new( + ValidationErrorKind::MismatchedEntityId, + format!("notes.{id}.id"), + format!("note stored under {id} contains id {}", note.id), + )); + } + + let Some(note_type) = self.note_types.get(¬e.note_type_id) else { + errors.push(ValidationError::new( + ValidationErrorKind::MissingNoteType, + format!("notes.{id}.note_type_id"), + format!("note references missing note type {}", note.note_type_id), + )); + continue; + }; + + let expected_field_ids = note_type + .fields + .iter() + .map(|field| field.id.clone()) + .collect::>(); + + for field_id in note.fields.keys() { + if !expected_field_ids.contains(field_id) { + errors.push(ValidationError::new( + ValidationErrorKind::UnknownNoteField, + format!("notes.{id}.fields.{field_id}"), + format!( + "note field {field_id} is not defined by note type {}", + note.note_type_id + ), + )); + } + } + + for field_id in expected_field_ids { + if !note.fields.contains_key(&field_id) { + errors.push(ValidationError::new( + ValidationErrorKind::MissingNoteField, + format!("notes.{id}.fields.{field_id}"), + format!( + "note is missing field {field_id} defined by note type {}", + note.note_type_id + ), + )); + } + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(ValidationReport { errors }) + } + } +} + +fn apply_overlay( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + if let Some(translations) = &overlay.translations { + apply_translation_dictionary(resolved, overlay, translations, changed_paths, errors); + } + + if let Some(change) = &overlay.deck_change { + apply_deck_change(resolved, overlay, change, changed_paths, errors); + } + + let mut added_fields = Vec::new(); + for (note_type_id, change) in &overlay.note_type_changes { + match change.intent { + ChangeIntent::Add => apply_note_type_add( + resolved, + overlay, + note_type_id, + change, + changed_paths, + errors, + ), + ChangeIntent::Merge | ChangeIntent::Replace | ChangeIntent::Override => { + added_fields.extend(apply_note_type_change( + resolved, + overlay, + note_type_id, + change, + changed_paths, + errors, + )); + } + ChangeIntent::Remove => apply_note_type_remove( + resolved, + overlay, + note_type_id, + change, + changed_paths, + errors, + ), + } + } + + for (note_id, change) in &overlay.note_changes { + match change.intent { + ChangeIntent::Add => { + apply_note_add(resolved, overlay, note_id, change, changed_paths, errors) + } + ChangeIntent::Merge | ChangeIntent::Replace | ChangeIntent::Override => { + apply_note_merge(resolved, overlay, note_id, change, changed_paths, errors); + } + ChangeIntent::Remove => { + apply_note_remove(resolved, overlay, note_id, change, changed_paths, errors); + } + } + } + + for (note_type_id, field_id) in added_fields { + fill_added_field_blanks(resolved, ¬e_type_id, &field_id); + } + + for (media_id, change) in &overlay.media_changes { + apply_media_change(resolved, overlay, media_id, change, changed_paths, errors); + } +} + +fn fill_added_field_blanks( + resolved: &mut CanonicalDeck, + note_type_id: &StableId, + field_id: &StableId, +) { + for note in resolved + .notes + .values_mut() + .filter(|note| ¬e.note_type_id == note_type_id) + { + note.fields.entry(field_id.clone()).or_default(); + } +} + +fn apply_translation_dictionary( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + translations: &TranslationDictionary, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + let mut seen_global_changes = BTreeSet::new(); + let mut seen_path_changes = BTreeSet::new(); + let mut seen_additions = BTreeSet::new(); + let mut seen_variables = BTreeSet::new(); + let mut seen_adapter_ids = BTreeSet::new(); + + { + let mut context = TranslationApplyContext { + overlay, + translations, + seen_global_changes: &mut seen_global_changes, + seen_path_changes: &mut seen_path_changes, + seen_additions: &mut seen_additions, + seen_variables: &mut seen_variables, + seen_adapter_ids: &mut seen_adapter_ids, + changed_paths, + errors, + }; + context.translate_string(&mut resolved.name, "deck.name".to_owned(), None); + context.translate_string( + &mut resolved.description, + "deck.description".to_owned(), + None, + ); + context.translate_variables(&mut resolved.variables, "deck.variables"); + context.translate_adapter_ids(&mut resolved.adapter_ids, "deck.adapter_ids"); + + for (note_type_id, note_type) in &mut resolved.note_types { + context.translate_string( + &mut note_type.name, + format!("note_types.{note_type_id}.name"), + None, + ); + context.translate_variables( + &mut note_type.variables, + &format!("note_types.{note_type_id}.variables"), + ); + for field in &mut note_type.fields { + context.translate_string( + &mut field.name, + format!("note_types.{note_type_id}.fields.{}.name", field.id), + None, + ); + } + for template in &mut note_type.card_templates { + context.translate_string( + &mut template.name, + format!( + "note_types.{note_type_id}.card_templates.{}.name", + template.id + ), + None, + ); + context.translate_variables( + &mut template.variables, + &format!( + "note_types.{note_type_id}.card_templates.{}.variables", + template.id + ), + ); + context.translate_adapter_ids( + &mut template.adapter_ids, + &format!( + "note_types.{note_type_id}.card_templates.{}.adapter_ids", + template.id + ), + ); + } + context.translate_adapter_ids( + &mut note_type.adapter_ids, + &format!("note_types.{note_type_id}.adapter_ids"), + ); + } + + for (note_id, note) in &mut resolved.notes { + context.translate_variables(&mut note.variables, &format!("notes.{note_id}.variables")); + for (field_id, value) in &mut note.fields { + context.translate_string(value, format!("notes.{note_id}.fields.{field_id}"), None); + } + context.translate_tags(&mut note.tags, &format!("notes.{note_id}.tags")); + context.translate_adapter_ids( + &mut note.adapter_ids, + &format!("notes.{note_id}.adapter_ids"), + ); + } + } + + for (source, change) in &translations.changes { + match change { + TranslationChange::Global(_) => { + if !seen_global_changes.contains(source) { + errors.push(ComposeError::new( + ComposeErrorKind::StaleTranslationEntry, + format!("translations.changes.{source}"), + format!("translation source {source:?} did not match any extracted text"), + )); + } + } + TranslationChange::AtPaths(paths) => { + for path in paths.keys() { + if !seen_path_changes.contains(&(source.clone(), path.clone())) { + errors.push(ComposeError::new( + ComposeErrorKind::StaleTranslationEntry, + format!("translations.changes.{source}.{path}"), + format!( + "translation source {source:?} did not match extracted text at {path}" + ), + )); + } + } + } + } + } + for path in translations.additions.keys() { + if !seen_additions.contains(path) { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + format!("translations.additions.{path}"), + format!("translation addition path {path} did not match any extracted text"), + )); + } + } + for (variable_key, replacements) in &translations.variables { + for source in replacements.keys() { + if !seen_variables.contains(&(variable_key.clone(), source.clone())) { + errors.push(ComposeError::new( + ComposeErrorKind::StaleTranslationEntry, + format!("translations.variables.{variable_key}.{source}"), + format!( + "variable translation source {variable_key}={source:?} did not match any variable" + ), + )); + } + } + } + for (adapter_key, replacements) in &translations.adapter_ids { + for source in replacements.keys() { + if !seen_adapter_ids.contains(&(adapter_key.clone(), source.clone())) { + errors.push(ComposeError::new( + ComposeErrorKind::StaleTranslationEntry, + format!("translations.adapter_ids.{adapter_key}.{source}"), + format!( + "adapter id translation source {adapter_key}={source:?} did not match any adapter id" + ), + )); + } + } + } +} + +struct TranslationApplyContext<'a, 'b> { + overlay: &'a Overlay, + translations: &'a TranslationDictionary, + seen_global_changes: &'b mut BTreeSet, + seen_path_changes: &'b mut BTreeSet<(String, String)>, + seen_additions: &'b mut BTreeSet, + seen_variables: &'b mut BTreeSet<(String, String)>, + seen_adapter_ids: &'b mut BTreeSet<(String, String)>, + changed_paths: &'b mut BTreeMap, + errors: &'b mut Vec, +} + +impl TranslationApplyContext<'_, '_> { + fn translate_variables(&mut self, variables: &mut BTreeMap, path_prefix: &str) { + for (key, value) in variables { + self.translate_string(value, format!("{path_prefix}.{key}"), Some(key)); + } + } + + fn translate_string(&mut self, value: &mut String, path: String, variable_key: Option<&str>) { + if let Some(addition) = self.translations.additions.get(&path) { + self.seen_additions.insert(path.clone()); + if !value.is_empty() { + self.errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!("translation addition expected blank value, found {value:?}"), + )); + return; + } + if !record_change_path( + &path, + self.overlay, + ChangeIntent::Replace, + self.changed_paths, + self.errors, + ) { + return; + } + *value = addition.clone(); + return; + } + + if value.is_empty() || is_ignored_translation_path(self.translations, &path) { + return; + } + + if let Some(variable_key) = variable_key + && let Some(replacements) = self.translations.variables.get(variable_key) + && let Some(translated) = replacements.get(value.as_str()) + { + let source = value.clone(); + self.seen_variables + .insert((variable_key.to_owned(), source)); + if value != translated { + if !record_change_path( + &path, + self.overlay, + ChangeIntent::Replace, + self.changed_paths, + self.errors, + ) { + return; + } + *value = translated.clone(); + } + return; + } + + if let Some(change) = self.translations.changes.get(value.as_str()) { + match change { + TranslationChange::Global(translated) => { + let source = value.clone(); + self.seen_global_changes.insert(source); + if value != translated { + if !record_change_path( + &path, + self.overlay, + ChangeIntent::Replace, + self.changed_paths, + self.errors, + ) { + return; + } + *value = translated.clone(); + } + return; + } + TranslationChange::AtPaths(paths) => { + if let Some(translated) = paths.get(&path) { + let source = value.clone(); + self.seen_path_changes.insert((source, path.clone())); + if value != translated { + if !record_change_path( + &path, + self.overlay, + ChangeIntent::Replace, + self.changed_paths, + self.errors, + ) { + return; + } + *value = translated.clone(); + } + return; + } + } + } + } + + if self.translations.require_complete { + self.errors.push(ComposeError::new( + ComposeErrorKind::MissingTranslation, + path, + format!("missing translation for {value:?}"), + )); + } + } + + fn translate_tags(&mut self, tags: &mut BTreeSet, path_prefix: &str) { + for tag in tags.iter().cloned().collect::>() { + let mut translated = tag.clone(); + self.translate_string(&mut translated, format!("{path_prefix}.{tag}"), None); + if translated != tag { + tags.remove(&tag); + tags.insert(translated); + } + } + } + + fn translate_adapter_ids(&mut self, adapter_ids: &mut AdapterIds, path_prefix: &str) { + let current = adapter_ids + .iter() + .map(|(key, value)| (key.to_owned(), value.to_owned())) + .collect::>(); + for (key, value) in current { + let Some(replacements) = self.translations.adapter_ids.get(&key) else { + continue; + }; + let Some(translated) = replacements.get(&value) else { + continue; + }; + self.seen_adapter_ids.insert((key.clone(), value.clone())); + if value != *translated { + let path = format!("{path_prefix}.{key}"); + if !record_change_path( + &path, + self.overlay, + ChangeIntent::Replace, + self.changed_paths, + self.errors, + ) { + continue; + } + adapter_ids.insert(key, translated.clone()); + } + } + } +} + +fn is_ignored_translation_path(translations: &TranslationDictionary, path: &str) -> bool { + translations + .ignore_paths + .iter() + .any(|pattern| glob_matches(pattern, path)) +} + +fn glob_matches(pattern: &str, value: &str) -> bool { + fn matches_parts(pattern: &[u8], value: &[u8]) -> bool { + match pattern.split_first() { + None => value.is_empty(), + Some((&b'*', rest)) => { + matches_parts(rest, value) + || (!value.is_empty() && matches_parts(pattern, &value[1..])) + } + Some((&expected, rest)) => value.split_first().is_some_and(|(&actual, rest_value)| { + actual == expected && matches_parts(rest, rest_value) + }), + } + } + + matches_parts(pattern.as_bytes(), value.as_bytes()) +} + +fn apply_deck_change( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + change: &DeckChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + if let Some(name) = &change.name { + apply_string_property_change( + &mut resolved.name, + overlay, + "deck.name".to_owned(), + name, + changed_paths, + errors, + ); + } + if let Some(description) = &change.description { + apply_string_property_change( + &mut resolved.description, + overlay, + "deck.description".to_owned(), + description, + changed_paths, + errors, + ); + } + apply_variable_changes( + &mut resolved.variables, + overlay, + "deck.variables", + &change.variables, + changed_paths, + errors, + ); + for (key, adapter_change) in &change.adapter_ids { + apply_adapter_id_change( + &mut resolved.adapter_ids, + overlay, + format!("deck.adapter_ids.{key}"), + key, + adapter_change, + changed_paths, + errors, + ); + } +} + +fn apply_note_type_add( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + note_type_id: &StableId, + change: &NoteTypeChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + let path = format!("note_types.{note_type_id}"); + if resolved.note_types.contains_key(note_type_id) { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + format!("note type {note_type_id} already exists"), + )); + return; + } + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + let Some(note_type) = &change.note_type else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayPayload, + path, + format!("add change for note type {note_type_id} must include a note_type"), + )); + return; + }; + if ¬e_type.id != note_type_id { + errors.push(ComposeError::new( + ComposeErrorKind::ValidationFailed, + path, + format!( + "note type payload id {} does not match target {note_type_id}", + note_type.id + ), + )); + return; + } + resolved + .note_types + .insert(note_type_id.clone(), note_type.clone()); +} + +fn apply_note_type_remove( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + note_type_id: &StableId, + change: &NoteTypeChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + let path = format!("note_types.{note_type_id}"); + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + if !has_expected_base(&change.expected_base, path.clone(), errors) { + return; + } + if !resolved.note_types.contains_key(note_type_id) { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + path, + format!("note type {note_type_id} does not exist"), + )); + return; + } + if resolved + .notes + .values() + .any(|note| ¬e.note_type_id == note_type_id) + { + errors.push(ComposeError::new( + ComposeErrorKind::ValidationFailed, + path, + format!("cannot remove note type {note_type_id} while notes still reference it"), + )); + return; + } + if let Some(ExpectedBase::Value(expected_value)) = &change.expected_base { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!("note type removal expected an entity marker, not value {expected_value:?}"), + )); + return; + } + resolved.note_types.remove(note_type_id); + resolved.tombstones.insert(note_type_id.clone()); +} + +fn apply_note_type_change( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + note_type_id: &StableId, + change: &NoteTypeChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) -> Vec<(StableId, StableId)> { + let mut added_fields = Vec::new(); + if requires_expected_base(change.intent) + && !has_expected_base( + &change.expected_base, + format!("note_types.{note_type_id}"), + errors, + ) + { + return added_fields; + } + + let Some(note_type) = resolved.note_types.get_mut(note_type_id) else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + format!("note_types.{note_type_id}"), + format!("note type {note_type_id} does not exist"), + )); + return added_fields; + }; + + if let Some(name) = &change.name { + apply_string_property_change( + &mut note_type.name, + overlay, + format!("note_types.{note_type_id}.name"), + name, + changed_paths, + errors, + ); + } + apply_variable_changes( + &mut note_type.variables, + overlay, + &format!("note_types.{note_type_id}.variables"), + &change.variables, + changed_paths, + errors, + ); + if let Some(styling) = &change.styling { + apply_string_property_change( + &mut note_type.styling, + overlay, + format!("note_types.{note_type_id}.styling"), + styling, + changed_paths, + errors, + ); + } + for (key, adapter_change) in &change.adapter_ids { + apply_adapter_id_change( + &mut note_type.adapter_ids, + overlay, + format!("note_types.{note_type_id}.adapter_ids.{key}"), + key, + adapter_change, + changed_paths, + errors, + ); + } + for (template_id, template_change) in &change.card_templates { + apply_card_template_change( + note_type, + overlay, + note_type_id, + template_id, + template_change, + changed_paths, + errors, + ); + } + + for (field_id, field_change) in &change.fields { + if apply_field_definition_change( + note_type, + overlay, + note_type_id, + field_id, + field_change, + changed_paths, + errors, + ) { + added_fields.push((note_type_id.clone(), field_id.clone())); + } + } + + added_fields +} + +fn apply_card_template_change( + note_type: &mut NoteType, + overlay: &Overlay, + note_type_id: &StableId, + template_id: &StableId, + change: &CardTemplateChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + let path = format!("note_types.{note_type_id}.card_templates.{template_id}"); + if requires_expected_base(change.intent) + && !has_expected_base(&change.expected_base, path.clone(), errors) + { + return; + } + + let existing_index = note_type + .card_templates + .iter() + .position(|template| &template.id == template_id); + + match change.intent { + ChangeIntent::Add if existing_index.is_some() => { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + format!("card template {template_id} already exists on note type {note_type_id}"), + )); + return; + } + ChangeIntent::Add => { + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + let Some(template) = &change.template else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayPayload, + path, + format!("add change for card template {template_id} must include a template"), + )); + return; + }; + if &template.id != template_id { + errors.push(ComposeError::new( + ComposeErrorKind::ValidationFailed, + path, + format!( + "card template payload id {} does not match target {template_id}", + template.id + ), + )); + return; + } + let insert_index = match &change.insert_after { + Some(after_id) => match note_type + .card_templates + .iter() + .position(|template| &template.id == after_id) + { + Some(index) => index + 1, + None => { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + path, + format!("insert_after template {after_id} does not exist"), + )); + return; + } + }, + None => note_type.card_templates.len(), + }; + note_type + .card_templates + .insert(insert_index, template.clone()); + } + ChangeIntent::Remove => { + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + if let Some(index) = existing_index { + note_type.card_templates.remove(index); + } else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + path, + format!( + "card template {template_id} does not exist on note type {note_type_id}" + ), + )); + } + return; + } + ChangeIntent::Merge | ChangeIntent::Replace | ChangeIntent::Override => { + if let Some(template) = &change.template { + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + let Some(index) = existing_index else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + path, + format!( + "card template {template_id} does not exist on note type {note_type_id}" + ), + )); + return; + }; + if &template.id != template_id { + errors.push(ComposeError::new( + ComposeErrorKind::ValidationFailed, + path, + format!( + "card template payload id {} does not match target {template_id}", + template.id + ), + )); + return; + } + note_type.card_templates[index] = template.clone(); + } + } + } + + let Some(template) = note_type + .card_templates + .iter_mut() + .find(|template| &template.id == template_id) + else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + path, + format!("card template {template_id} does not exist on note type {note_type_id}"), + )); + return; + }; + + if let Some(name) = &change.name { + apply_string_property_change( + &mut template.name, + overlay, + format!("note_types.{note_type_id}.card_templates.{template_id}.name"), + name, + changed_paths, + errors, + ); + } + apply_variable_changes( + &mut template.variables, + overlay, + &format!("note_types.{note_type_id}.card_templates.{template_id}.variables"), + &change.variables, + changed_paths, + errors, + ); + if let Some(question_format) = &change.question_format { + apply_string_property_change( + &mut template.question_format, + overlay, + format!("note_types.{note_type_id}.card_templates.{template_id}.question_format"), + question_format, + changed_paths, + errors, + ); + } + if let Some(answer_format) = &change.answer_format { + apply_string_property_change( + &mut template.answer_format, + overlay, + format!("note_types.{note_type_id}.card_templates.{template_id}.answer_format"), + answer_format, + changed_paths, + errors, + ); + } + for (key, adapter_change) in &change.adapter_ids { + apply_adapter_id_change( + &mut template.adapter_ids, + overlay, + format!("note_types.{note_type_id}.card_templates.{template_id}.adapter_ids.{key}"), + key, + adapter_change, + changed_paths, + errors, + ); + } +} + +fn apply_string_property_change( + value: &mut String, + overlay: &Overlay, + path: String, + change: &PropertyChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + + if requires_expected_base(change.intent) + && !has_expected_base(&change.expected_base, path.clone(), errors) + { + return; + } + + if let Some(expected_base) = &change.expected_base { + match expected_base { + ExpectedBase::Value(expected_value) => { + if value != expected_value { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!( + "expected base value {:?}, found {:?}", + expected_value, value + ), + )); + return; + } + } + ExpectedBase::EntityPresent => { + if value.is_empty() { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + "expected property to be present".to_owned(), + )); + return; + } + } + } + } + + match change.intent { + ChangeIntent::Add if !value.is_empty() => { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + "property already has a value".to_owned(), + )); + } + ChangeIntent::Remove => value.clear(), + ChangeIntent::Add + | ChangeIntent::Merge + | ChangeIntent::Replace + | ChangeIntent::Override => { + let Some(new_value) = &change.value else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayPayload, + path, + "property change must include a value".to_owned(), + )); + return; + }; + *value = new_value.clone(); + } + } +} + +fn apply_variable_changes( + variables: &mut BTreeMap, + overlay: &Overlay, + path_prefix: &str, + changes: &BTreeMap, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + for (key, change) in changes { + apply_map_string_property_change( + variables, + overlay, + format!("{path_prefix}.{key}"), + key, + change, + changed_paths, + errors, + ); + } +} + +fn apply_map_string_property_change( + values: &mut BTreeMap, + overlay: &Overlay, + path: String, + key: &str, + change: &PropertyChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + + if requires_expected_base(change.intent) + && !has_expected_base(&change.expected_base, path.clone(), errors) + { + return; + } + + if let Some(expected_base) = &change.expected_base { + match expected_base { + ExpectedBase::Value(expected_value) => { + let current_value = values.get(key); + if current_value != Some(expected_value) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!( + "expected base value {:?}, found {:?}", + expected_value, current_value + ), + )); + return; + } + } + ExpectedBase::EntityPresent => { + if !values.contains_key(key) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!("expected variable {key} to be present"), + )); + return; + } + } + } + } + + match change.intent { + ChangeIntent::Add if values.contains_key(key) => { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + format!("variable {key} already exists"), + )); + } + ChangeIntent::Remove => { + values.remove(key); + } + ChangeIntent::Add + | ChangeIntent::Merge + | ChangeIntent::Replace + | ChangeIntent::Override => { + let Some(value) = &change.value else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayPayload, + path, + format!("variable change for {key} must include a value"), + )); + return; + }; + values.insert(key.to_owned(), value.clone()); + } + } +} + +fn apply_adapter_id_change( + adapter_ids: &mut AdapterIds, + overlay: &Overlay, + path: String, + key: &str, + change: &AdapterIdChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + + if requires_expected_base(change.intent) + && !has_expected_base(&change.expected_base, path.clone(), errors) + { + return; + } + + if let Some(expected_base) = &change.expected_base { + match expected_base { + ExpectedBase::Value(expected_value) => { + let current_value = adapter_ids.get(key); + if current_value != Some(expected_value.as_str()) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!( + "expected base value {:?}, found {:?}", + expected_value, current_value + ), + )); + return; + } + } + ExpectedBase::EntityPresent => { + if !adapter_ids.contains_key(key) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!("expected adapter id {key} to be present"), + )); + return; + } + } + } + } + + match change.intent { + ChangeIntent::Add if adapter_ids.contains_key(key) => { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + format!("adapter id {key} already exists"), + )); + } + ChangeIntent::Remove => { + adapter_ids.remove(key); + } + ChangeIntent::Add + | ChangeIntent::Merge + | ChangeIntent::Replace + | ChangeIntent::Override => { + let Some(value) = &change.value else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayPayload, + path, + format!("adapter id change for {key} must include a value"), + )); + return; + }; + adapter_ids.insert(key.to_owned(), value.clone()); + } + } +} + +fn apply_field_definition_change( + note_type: &mut NoteType, + overlay: &Overlay, + note_type_id: &StableId, + field_id: &StableId, + change: &FieldDefinitionChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) -> bool { + let path = format!("note_types.{note_type_id}.fields.{field_id}"); + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return false; + } + + if requires_expected_base(change.intent) + && !has_expected_base(&change.expected_base, path.clone(), errors) + { + return false; + } + + let existing_index = note_type + .fields + .iter() + .position(|field| &field.id == field_id); + match change.intent { + ChangeIntent::Add if existing_index.is_some() => { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + format!("field {field_id} already exists on note type {note_type_id}"), + )); + false + } + ChangeIntent::Remove => { + if let Some(index) = existing_index { + note_type.fields.remove(index); + } else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + path, + format!("field {field_id} does not exist on note type {note_type_id}"), + )); + } + false + } + ChangeIntent::Add + | ChangeIntent::Merge + | ChangeIntent::Replace + | ChangeIntent::Override => { + let Some(field) = &change.field else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayPayload, + path, + format!("field definition change for {field_id} must include a field"), + )); + return false; + }; + if &field.id != field_id { + errors.push(ComposeError::new( + ComposeErrorKind::ValidationFailed, + path, + format!( + "field payload id {} does not match target {field_id}", + field.id + ), + )); + return false; + } + if let Some(index) = existing_index { + note_type.fields[index] = field.clone(); + false + } else { + note_type.fields.push(field.clone()); + change.intent == ChangeIntent::Add + } + } + } +} + +fn apply_note_add( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + note_id: &StableId, + change: &NoteChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + let path = format!("notes.{note_id}"); + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + + if resolved.notes.contains_key(note_id) && !resolved.tombstones.contains(note_id) { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + format!("note {note_id} already exists"), + )); + return; + } + + let Some(note) = &change.note else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayPayload, + path, + format!("add change for note {note_id} must include a note payload"), + )); + return; + }; + + resolved.notes.insert(note_id.clone(), note.clone()); + resolved.tombstones.remove(note_id); +} + +fn apply_note_merge( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + note_id: &StableId, + change: &NoteChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + if requires_expected_base(change.intent) + && !has_expected_base(&change.expected_base, format!("notes.{note_id}"), errors) + { + return; + } + + let Some(note) = resolved.notes.get_mut(note_id) else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + format!("notes.{note_id}"), + format!("note {note_id} does not exist"), + )); + return; + }; + + apply_variable_changes( + &mut note.variables, + overlay, + &format!("notes.{note_id}.variables"), + &change.variables, + changed_paths, + errors, + ); + + for (tag, tag_change) in &change.tags { + apply_tag_change( + note, + overlay, + note_id, + tag, + tag_change, + changed_paths, + errors, + ); + } + + for (key, adapter_change) in &change.adapter_ids { + apply_adapter_id_change( + &mut note.adapter_ids, + overlay, + format!("notes.{note_id}.adapter_ids.{key}"), + key, + adapter_change, + changed_paths, + errors, + ); + } + + for (field_id, field_change) in &change.fields { + apply_field_change( + note, + overlay, + note_id, + field_id, + field_change, + changed_paths, + errors, + ); + } +} + +fn apply_tag_change( + note: &mut Note, + overlay: &Overlay, + note_id: &StableId, + tag: &str, + change: &TagChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + let path = format!("notes.{note_id}.tags.{tag}"); + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + + if requires_expected_base(change.intent) + && !has_expected_base(&change.expected_base, path.clone(), errors) + { + return; + } + + if let Some(expected_base) = &change.expected_base { + match expected_base { + ExpectedBase::EntityPresent => { + if !note.tags.contains(tag) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!("expected tag {tag} to be present"), + )); + return; + } + } + ExpectedBase::Value(expected_value) => { + if expected_value != tag || !note.tags.contains(tag) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!("expected tag value {:?} to be present", expected_value), + )); + return; + } + } + } + } + + match change.intent { + ChangeIntent::Add if note.tags.contains(tag) => { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + format!("tag {tag} already exists on note {note_id}"), + )); + } + ChangeIntent::Remove => { + note.tags.remove(tag); + } + ChangeIntent::Add + | ChangeIntent::Merge + | ChangeIntent::Replace + | ChangeIntent::Override => { + note.tags.insert(tag.to_owned()); + } + } +} + +fn apply_field_change( + note: &mut Note, + overlay: &Overlay, + note_id: &StableId, + field_id: &StableId, + change: &FieldChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + let path = format!("notes.{note_id}.fields.{field_id}"); + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + + if requires_expected_base(change.intent) + && !has_expected_base(&change.expected_base, path.clone(), errors) + { + return; + } + + if let Some(expected_base) = &change.expected_base { + match expected_base { + ExpectedBase::Value(expected_value) => { + let current_value = note.fields.get(field_id).map(String::as_str); + if current_value != Some(expected_value.as_str()) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!( + "expected base value {:?}, found {:?}", + expected_value, current_value + ), + )); + return; + } + } + ExpectedBase::EntityPresent => { + if !note.fields.contains_key(field_id) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!("expected field {field_id} to be present"), + )); + return; + } + } + } + } + + match change.intent { + ChangeIntent::Add if note.fields.contains_key(field_id) => { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + format!("field {field_id} already exists on note {note_id}"), + )); + } + ChangeIntent::Remove => { + note.fields.remove(field_id); + } + ChangeIntent::Add + | ChangeIntent::Merge + | ChangeIntent::Replace + | ChangeIntent::Override => { + let Some(value) = &change.value else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayPayload, + path, + format!("field change for {field_id} must include a value"), + )); + return; + }; + note.fields.insert(field_id.clone(), value.clone()); + } + } +} + +fn apply_media_change( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + media_id: &StableId, + change: &MediaChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + let path = format!("media.{media_id}"); + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + + if requires_expected_base(change.intent) + && !has_expected_base(&change.expected_base, path.clone(), errors) + { + return; + } + + if let Some(expected_base) = &change.expected_base { + match expected_base { + ExpectedBase::EntityPresent => { + if !resolved.media.contains_key(media_id) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!("expected media reference {media_id} to be present"), + )); + return; + } + } + ExpectedBase::Value(expected_value) => { + let current_value = resolved.media.get(media_id).map(media_reference_summary); + if current_value.as_deref() != Some(expected_value.as_str()) { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!( + "expected base value {:?}, found {:?}", + expected_value, current_value + ), + )); + return; + } + } + } + } + + match change.intent { + ChangeIntent::Add if resolved.media.contains_key(media_id) => { + errors.push(ComposeError::new( + ComposeErrorKind::AlreadyExists, + path, + format!("media reference {media_id} already exists"), + )); + } + ChangeIntent::Remove => { + resolved.media.remove(media_id); + } + ChangeIntent::Add + | ChangeIntent::Merge + | ChangeIntent::Replace + | ChangeIntent::Override => { + let Some(media) = &change.media else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayPayload, + path, + format!("media change for {media_id} must include a media reference"), + )); + return; + }; + if &media.id != media_id { + errors.push(ComposeError::new( + ComposeErrorKind::ValidationFailed, + path, + format!( + "media payload id {} does not match target {media_id}", + media.id + ), + )); + return; + } + resolved.media.insert(media_id.clone(), media.clone()); + } + } +} + +fn media_reference_summary(media: &MediaReference) -> String { + format!("path={};sha256={}", media.path, media.sha256) +} + +fn apply_note_remove( + resolved: &mut CanonicalDeck, + overlay: &Overlay, + note_id: &StableId, + change: &NoteChange, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) { + let path = format!("notes.{note_id}"); + if !record_change_path(&path, overlay, change.intent, changed_paths, errors) { + return; + } + + if !has_expected_base(&change.expected_base, path.clone(), errors) { + return; + } + + if !resolved.notes.contains_key(note_id) { + errors.push(ComposeError::new( + ComposeErrorKind::MissingOverlayTarget, + path, + format!("note {note_id} does not exist"), + )); + return; + } + + if let Some(ExpectedBase::Value(expected_value)) = &change.expected_base { + errors.push(ComposeError::new( + ComposeErrorKind::ExpectedBaseMismatch, + path, + format!("note removal expected an entity marker, not value {expected_value:?}"), + )); + return; + } + + resolved.tombstones.insert(note_id.clone()); +} + +fn record_change_path( + path: &str, + overlay: &Overlay, + intent: ChangeIntent, + changed_paths: &mut BTreeMap, + errors: &mut Vec, +) -> bool { + if let Some(previous_overlay_id) = changed_paths.get(path) + && intent != ChangeIntent::Override + { + errors.push(ComposeError::new( + ComposeErrorKind::Conflict, + path.to_owned(), + format!( + "overlay {} conflicts with earlier overlay {} at {path}", + overlay.id, previous_overlay_id + ), + )); + return false; + } + + changed_paths.insert(path.to_owned(), overlay.id.clone()); + true +} + +fn has_expected_base( + expected_base: &Option, + path: String, + errors: &mut Vec, +) -> bool { + if expected_base.is_some() { + true + } else { + errors.push(ComposeError::new( + ComposeErrorKind::MissingExpectedBase, + path, + "destructive overlay change must declare an expected base".to_owned(), + )); + false + } +} + +fn requires_expected_base(intent: ChangeIntent) -> bool { + matches!( + intent, + ChangeIntent::Replace | ChangeIntent::Remove | ChangeIntent::Override + ) +} + +fn render_deck_variables(deck: &CanonicalDeck) -> Result { + let mut rendered = deck.clone(); + let mut errors = Vec::new(); + let deck_variables = rendered.variables.clone(); + + render_string_with_variables( + &mut rendered.name, + "deck.name", + &[&deck_variables], + &mut errors, + ); + render_string_with_variables( + &mut rendered.description, + "deck.description", + &[&deck_variables], + &mut errors, + ); + + for (note_type_id, note_type) in &mut rendered.note_types { + let note_type_variables = note_type.variables.clone(); + render_string_with_variables( + &mut note_type.name, + &format!("note_types.{note_type_id}.name"), + &[¬e_type_variables, &deck_variables], + &mut errors, + ); + render_string_with_variables( + &mut note_type.styling, + &format!("note_types.{note_type_id}.styling"), + &[¬e_type_variables, &deck_variables], + &mut errors, + ); + for field in &mut note_type.fields { + render_string_with_variables( + &mut field.name, + &format!("note_types.{note_type_id}.fields.{}.name", field.id), + &[¬e_type_variables, &deck_variables], + &mut errors, + ); + } + for template in &mut note_type.card_templates { + let template_variables = template.variables.clone(); + let scopes = [&template_variables, ¬e_type_variables, &deck_variables]; + render_string_with_variables( + &mut template.name, + &format!( + "note_types.{note_type_id}.card_templates.{}.name", + template.id + ), + &scopes, + &mut errors, + ); + render_string_with_variables( + &mut template.question_format, + &format!( + "note_types.{note_type_id}.card_templates.{}.question_format", + template.id + ), + &scopes, + &mut errors, + ); + render_string_with_variables( + &mut template.answer_format, + &format!( + "note_types.{note_type_id}.card_templates.{}.answer_format", + template.id + ), + &scopes, + &mut errors, + ); + } + } + + for (note_id, note) in &mut rendered.notes { + let note_variables = note.variables.clone(); + let note_type_variables = rendered + .note_types + .get(¬e.note_type_id) + .map(|note_type| ¬e_type.variables); + for (field_id, value) in &mut note.fields { + let path = format!("notes.{note_id}.fields.{field_id}"); + if let Some(note_type_variables) = note_type_variables { + render_string_with_variables( + value, + &path, + &[¬e_variables, note_type_variables, &deck_variables], + &mut errors, + ); + } else { + render_string_with_variables( + value, + &path, + &[¬e_variables, &deck_variables], + &mut errors, + ); + } + } + } + + if errors.is_empty() { + Ok(rendered) + } else { + Err(VariableRenderReport { errors }) + } +} + +fn render_string_with_variables( + value: &mut String, + path: &str, + scopes: &[&BTreeMap], + errors: &mut Vec, +) { + let mut rendered = String::new(); + let mut remaining = value.as_str(); + while let Some(start) = remaining.find("${") { + rendered.push_str(&remaining[..start]); + let after_start = &remaining[start + 2..]; + let Some(end) = after_start.find('}') else { + rendered.push_str(&remaining[start..]); + *value = rendered; + return; + }; + let key = &after_start[..end]; + if let Some(replacement) = lookup_variable(scopes, key) { + rendered.push_str(replacement); + } else { + errors.push(VariableRenderError { + path: path.to_owned(), + variable: key.to_owned(), + }); + rendered.push_str(&remaining[start..start + end + 3]); + } + remaining = &after_start[end + 1..]; + } + rendered.push_str(remaining); + *value = rendered; +} + +fn lookup_variable<'a>(scopes: &[&'a BTreeMap], key: &str) -> Option<&'a str> { + scopes + .iter() + .find_map(|scope| scope.get(key).map(String::as_str)) +} + +fn diff_note_types( + left: &BTreeMap, + right: &BTreeMap, + changes: &mut Vec, +) { + for id in left.keys() { + if !right.contains_key(id) { + changes.push(SemanticChange::removed(format!("note_types.{id}"))); + } + } + + for (id, right_note_type) in right { + let Some(left_note_type) = left.get(id) else { + changes.push(SemanticChange::added(format!("note_types.{id}"))); + continue; + }; + + push_modified_if_changed( + changes, + format!("note_types.{id}.name"), + &left_note_type.name, + &right_note_type.name, + ); + push_modified_if_changed( + changes, + format!("note_types.{id}.styling"), + &left_note_type.styling, + &right_note_type.styling, + ); + push_modified_if_changed( + changes, + format!("note_types.{id}.variables"), + &string_map_summary(&left_note_type.variables), + &string_map_summary(&right_note_type.variables), + ); + push_modified_if_changed( + changes, + format!("note_types.{id}.fields"), + &field_summary(&left_note_type.fields), + &field_summary(&right_note_type.fields), + ); + push_modified_if_changed( + changes, + format!("note_types.{id}.card_templates"), + &template_summary(&left_note_type.card_templates), + &template_summary(&right_note_type.card_templates), + ); + push_modified_if_changed( + changes, + format!("note_types.{id}.adapter_ids"), + &adapter_ids_summary(&left_note_type.adapter_ids), + &adapter_ids_summary(&right_note_type.adapter_ids), + ); + } +} + +fn diff_notes( + left: &BTreeMap, + right: &BTreeMap, + changes: &mut Vec, +) { + for id in left.keys() { + if !right.contains_key(id) { + changes.push(SemanticChange::removed(format!("notes.{id}"))); + } + } + + for (id, right_note) in right { + let Some(left_note) = left.get(id) else { + changes.push(SemanticChange::added(format!("notes.{id}"))); + continue; + }; + + push_modified_if_changed( + changes, + format!("notes.{id}.note_type_id"), + &left_note.note_type_id.to_string(), + &right_note.note_type_id.to_string(), + ); + push_modified_if_changed( + changes, + format!("notes.{id}.variables"), + &string_map_summary(&left_note.variables), + &string_map_summary(&right_note.variables), + ); + diff_note_fields(id, &left_note.fields, &right_note.fields, changes); + push_modified_if_changed( + changes, + format!("notes.{id}.tags"), + &set_summary(&left_note.tags), + &set_summary(&right_note.tags), + ); + push_modified_if_changed( + changes, + format!("notes.{id}.adapter_ids"), + &adapter_ids_summary(&left_note.adapter_ids), + &adapter_ids_summary(&right_note.adapter_ids), + ); + } +} + +fn diff_note_fields( + note_id: &StableId, + left: &BTreeMap, + right: &BTreeMap, + changes: &mut Vec, +) { + for field_id in left.keys() { + if !right.contains_key(field_id) { + changes.push(SemanticChange::new( + SemanticChangeKind::Removed, + format!("notes.{note_id}.fields.{field_id}"), + left.get(field_id).cloned(), + None, + )); + } + } + + for (field_id, right_value) in right { + let Some(left_value) = left.get(field_id) else { + changes.push(SemanticChange::new( + SemanticChangeKind::Added, + format!("notes.{note_id}.fields.{field_id}"), + None, + Some(right_value.clone()), + )); + continue; + }; + + push_modified_if_changed( + changes, + format!("notes.{note_id}.fields.{field_id}"), + left_value, + right_value, + ); + } +} + +fn diff_media( + left: &BTreeMap, + right: &BTreeMap, + changes: &mut Vec, +) { + for id in left.keys() { + if !right.contains_key(id) { + changes.push(SemanticChange::removed(format!("media.{id}"))); + } + } + + for (id, right_media) in right { + let Some(left_media) = left.get(id) else { + changes.push(SemanticChange::added(format!("media.{id}"))); + continue; + }; + + push_modified_if_changed( + changes, + format!("media.{id}.path"), + &left_media.path, + &right_media.path, + ); + push_modified_if_changed( + changes, + format!("media.{id}.sha256"), + &left_media.sha256, + &right_media.sha256, + ); + } +} + +fn diff_tombstones( + left: &BTreeSet, + right: &BTreeSet, + changes: &mut Vec, +) { + for id in left { + if !right.contains(id) { + changes.push(SemanticChange::removed(format!("tombstones.{id}"))); + } + } + + for id in right { + if !left.contains(id) { + changes.push(SemanticChange::new( + SemanticChangeKind::Tombstoned, + format!("tombstones.{id}"), + None, + Some(id.to_string()), + )); + } + } +} + +fn push_modified_if_changed( + changes: &mut Vec, + path: String, + left: &str, + right: &str, +) { + if left != right { + changes.push(SemanticChange::new( + SemanticChangeKind::Modified, + path, + Some(left.to_owned()), + Some(right.to_owned()), + )); + } +} + +fn field_summary(fields: &[FieldDefinition]) -> String { + fields + .iter() + .map(|field| format!("{}={}", field.id, field.name)) + .collect::>() + .join("|") +} + +fn template_summary(templates: &[CardTemplate]) -> String { + templates + .iter() + .map(|template| { + format!( + "{}={}:{}:{}:{}:{}", + template.id, + template.name, + string_map_summary(&template.variables), + template.question_format, + template.answer_format, + adapter_ids_summary(&template.adapter_ids) + ) + }) + .collect::>() + .join("|") +} + +fn set_summary(values: &BTreeSet) -> String { + values.iter().cloned().collect::>().join("|") +} + +fn string_map_summary(values: &BTreeMap) -> String { + values + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>() + .join("|") +} + +fn adapter_ids_summary(adapter_ids: &AdapterIds) -> String { + adapter_ids + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>() + .join("|") +} + +fn push_duplicate_id_errors<'a>( + ids: impl Iterator, + kind: ValidationErrorKind, + path: impl Fn(&StableId) -> String, + errors: &mut Vec, +) { + let mut seen = BTreeSet::new(); + for id in ids { + if !seen.insert(id) { + errors.push(ValidationError::new( + kind, + path(id), + format!("duplicate stable id {id}"), + )); + } + } +} + +/// A sparse CanonicalDeck-shaped fragment applied to a base deck. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Overlay { + pub id: StableId, + pub kind: OverlayKind, + pub translations: Option, + pub deck_change: Option, + pub note_changes: BTreeMap, + pub note_type_changes: BTreeMap, + pub media_changes: BTreeMap, +} + +/// Translation dictionary applied by a translation overlay. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TranslationDictionary { + /// Exact source text replacements, either globally or scoped to stable deck paths. + pub changes: BTreeMap, + /// Stable deck paths to fill only when the current source value is blank. + pub additions: BTreeMap, + /// Variable-specific source text to translated text replacements by variable key. + pub variables: BTreeMap>, + /// Adapter-specific source ID to translated ID replacements by adapter namespace. + pub adapter_ids: BTreeMap>, + /// When true, every extracted translatable string must be translated or ignored. + pub require_complete: bool, + /// Glob-style paths ignored by complete-coverage checks. + pub ignore_paths: BTreeSet, +} + +/// Translation replacement for one exact source string. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TranslationChange { + /// Replace the source string wherever it is extracted. + Global(String), + /// Replace the source string only at the listed stable deck paths. + AtPaths(BTreeMap), +} + +/// Maintainer-facing category for an overlay. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OverlayKind { + Translation, + Extension, + Patch, + Personal, +} + +/// The declared meaning of an overlay change. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ChangeIntent { + Add, + Merge, + Replace, + Remove, + Override, +} + +/// Base value or condition an overlay expects before applying a destructive change. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ExpectedBase { + Value(String), + EntityPresent, +} + +/// Sparse change for deck-level metadata. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DeckChange { + pub name: Option, + pub description: Option, + pub variables: BTreeMap, + pub adapter_ids: BTreeMap, +} + +/// Sparse change for a scalar string property. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PropertyChange { + pub intent: ChangeIntent, + pub value: Option, + pub expected_base: Option, +} + +/// Sparse change for one adapter identity. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AdapterIdChange { + pub intent: ChangeIntent, + pub value: Option, + pub expected_base: Option, +} + +/// Sparse change for one note type. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NoteTypeChange { + pub intent: ChangeIntent, + pub note_type: Option, + pub name: Option, + pub variables: BTreeMap, + pub styling: Option, + pub fields: BTreeMap, + pub card_templates: BTreeMap, + pub adapter_ids: BTreeMap, + pub expected_base: Option, +} + +/// Sparse change for one card template. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CardTemplateChange { + pub intent: ChangeIntent, + pub template: Option, + pub insert_after: Option, + pub name: Option, + pub variables: BTreeMap, + pub question_format: Option, + pub answer_format: Option, + pub adapter_ids: BTreeMap, + pub expected_base: Option, +} + +/// Sparse change for one note type field definition. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FieldDefinitionChange { + pub intent: ChangeIntent, + pub field: Option, + pub expected_base: Option, +} + +/// Sparse change for one note. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NoteChange { + pub intent: ChangeIntent, + pub note: Option, + pub variables: BTreeMap, + pub fields: BTreeMap, + pub tags: BTreeMap, + pub adapter_ids: BTreeMap, + pub expected_base: Option, +} + +/// Sparse change for one note tag. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TagChange { + pub intent: ChangeIntent, + pub expected_base: Option, +} + +/// Sparse change for one media reference. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MediaChange { + pub intent: ChangeIntent, + pub media: Option, + pub expected_base: Option, +} + +/// Sparse change for one note field value. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FieldChange { + pub intent: ChangeIntent, + pub value: Option, + pub expected_base: Option, +} + +/// An Anki-compatible note type. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct NoteType { + pub id: StableId, + pub name: String, + pub variables: BTreeMap, + pub fields: Vec, + pub card_templates: Vec, + pub styling: String, + pub adapter_ids: AdapterIds, +} + +/// A field declared by a note type. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FieldDefinition { + pub id: StableId, + pub name: String, +} + +/// Raw Anki-compatible card template text plus identity metadata. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CardTemplate { + pub id: StableId, + pub name: String, + pub variables: BTreeMap, + pub question_format: String, + pub answer_format: String, + pub adapter_ids: AdapterIds, +} + +/// A note belonging to a note type. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Note { + pub id: StableId, + pub note_type_id: StableId, + pub variables: BTreeMap, + pub fields: BTreeMap, + pub tags: BTreeSet, + pub adapter_ids: AdapterIds, +} + +/// A reference to an external media asset. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MediaReference { + pub id: StableId, + pub path: String, + pub sha256: String, +} + +/// A failed attempt to render source variables. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VariableRenderReport { + pub errors: Vec, +} + +impl fmt::Display for VariableRenderReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (index, error) in self.errors.iter().enumerate() { + if index > 0 { + writeln!(f)?; + } + write!(f, "{}: missing variable ${}", error.path, error.variable)?; + } + Ok(()) + } +} + +impl std::error::Error for VariableRenderReport {} + +/// One variable rendering failure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VariableRenderError { + pub path: String, + pub variable: String, +} + +/// A failed attempt to compose an overlay stack. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ComposeReport { + pub errors: Vec, +} + +impl ComposeReport { + /// Returns true when the report contains at least one error of the given kind. + pub fn has_kind(&self, kind: ComposeErrorKind) -> bool { + self.errors.iter().any(|error| error.kind == kind) + } +} + +impl fmt::Display for ComposeReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (index, error) in self.errors.iter().enumerate() { + if index > 0 { + writeln!(f)?; + } + write!(f, "{}: {}", error.path, error.message)?; + } + Ok(()) + } +} + +impl std::error::Error for ComposeReport {} + +/// One overlay composition error. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ComposeError { + pub kind: ComposeErrorKind, + pub path: String, + pub message: String, +} + +impl ComposeError { + fn new(kind: ComposeErrorKind, path: String, message: String) -> Self { + Self { + kind, + path, + message, + } + } +} + +/// Machine-readable overlay composition error category. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ComposeErrorKind { + MissingExpectedBase, + ExpectedBaseMismatch, + Conflict, + MissingOverlayTarget, + AlreadyExists, + MissingOverlayPayload, + MissingTranslation, + StaleTranslationEntry, + ValidationFailed, +} + +/// A semantic comparison between two CanonicalDeck values. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct SemanticDiff { + pub changes: Vec, +} + +impl SemanticDiff { + /// Returns true when no deck entity differs semantically. + pub fn is_empty(&self) -> bool { + self.changes.is_empty() + } + + /// Returns true when a change with this kind and stable path exists. + pub fn has_change(&self, kind: SemanticChangeKind, path: &str) -> bool { + self.changes + .iter() + .any(|change| change.kind == kind && change.path == path) + } +} + +/// One semantic change at a stable deck path. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SemanticChange { + pub kind: SemanticChangeKind, + pub path: String, + pub before: Option, + pub after: Option, +} + +impl SemanticChange { + fn new( + kind: SemanticChangeKind, + path: String, + before: Option, + after: Option, + ) -> Self { + Self { + kind, + path, + before, + after, + } + } + + fn added(path: String) -> Self { + Self::new(SemanticChangeKind::Added, path, None, None) + } + + fn removed(path: String) -> Self { + Self::new(SemanticChangeKind::Removed, path, None, None) + } +} + +/// Machine-readable semantic change category. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SemanticChangeKind { + Added, + Removed, + Modified, + Tombstoned, +} + +/// A strict validation failure report. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ValidationReport { + pub errors: Vec, +} + +impl ValidationReport { + /// Returns true when the report contains at least one error of the given kind. + pub fn has_kind(&self, kind: ValidationErrorKind) -> bool { + self.errors.iter().any(|error| error.kind == kind) + } +} + +impl fmt::Display for ValidationReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (index, error) in self.errors.iter().enumerate() { + if index > 0 { + writeln!(f)?; + } + write!(f, "{}: {}", error.path, error.message)?; + } + Ok(()) + } +} + +impl std::error::Error for ValidationReport {} + +/// One strict validation failure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ValidationError { + pub kind: ValidationErrorKind, + pub path: String, + pub message: String, +} + +impl ValidationError { + pub fn new(kind: ValidationErrorKind, path: String, message: String) -> Self { + Self { + kind, + path, + message, + } + } +} + +/// Machine-readable validation error category. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ValidationErrorKind { + MissingNoteType, + UnknownNoteField, + MissingNoteField, + MismatchedEntityId, + DuplicateFieldDefinition, + DuplicateCardTemplate, +} + +#[cfg(test)] +mod tests { + use super::CRATE_NAME; + + #[test] + fn exposes_core_crate_name() { + assert_eq!(CRATE_NAME, "brain-brew-core"); + } +} diff --git a/crates/brain-brew-formats/Cargo.toml b/crates/brain-brew-formats/Cargo.toml new file mode 100644 index 0000000..24264f0 --- /dev/null +++ b/crates/brain-brew-formats/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "brain-brew-formats" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +publish = false + +[dependencies] +brain-brew-core.workspace = true +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_yaml = "0.9" +sha2 = "0.10" + +[lints] +workspace = true diff --git a/crates/brain-brew-formats/src/canonical_yaml.rs b/crates/brain-brew-formats/src/canonical_yaml.rs new file mode 100644 index 0000000..0588995 --- /dev/null +++ b/crates/brain-brew-formats/src/canonical_yaml.rs @@ -0,0 +1,1827 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::{self, Write as _}; + +use brain_brew_core::{ + AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplate, CardTemplateChange, ChangeIntent, + DeckChange, ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, InvalidStableId, + MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, Overlay, OverlayKind, + PropertyChange, StableId, TagChange, TranslationChange, TranslationDictionary, + ValidationReport, +}; +use serde::Deserialize; + +/// Parse a CanonicalDeck from strict canonical YAML. +pub fn from_str(input: &str) -> Result { + let file: CanonicalDeckYaml = serde_yaml::from_str(input).map_err(CanonicalYamlError::Parse)?; + let deck = file.into_deck()?; + deck.validate().map_err(CanonicalYamlError::Validation)?; + Ok(deck) +} + +/// Parse a sparse overlay YAML file. +pub fn overlay_from_str(input: &str) -> Result { + let file: OverlayYaml = serde_yaml::from_str(input).map_err(CanonicalYamlError::Parse)?; + file.into_overlay() +} + +/// Parse and re-emit a CanonicalDeck YAML file using deterministic formatting. +pub fn format_str(input: &str) -> Result { + let deck = from_str(input)?; + to_string(&deck) +} + +/// Parse and re-emit a sparse overlay YAML file using deterministic formatting. +pub fn overlay_format_str(input: &str) -> Result { + let overlay = overlay_from_str(input)?; + Ok(overlay_to_string(&overlay)) +} + +/// Emit a CanonicalDeck as deterministic canonical YAML. +pub fn to_string(deck: &CanonicalDeck) -> Result { + deck.validate().map_err(CanonicalYamlError::Validation)?; + + let mut out = String::new(); + writeln!(out, "deck:").expect("writing to a string cannot fail"); + writeln!(out, " id: {}", deck.id).expect("writing to a string cannot fail"); + writeln!(out, " name: {}", yaml_scalar(&deck.name)).expect("writing to a string cannot fail"); + write_multiline_or_scalar(&mut out, " ", "description", &deck.description); + write_variables(&mut out, " ", &deck.variables); + write_adapter_ids(&mut out, " ", &deck.adapter_ids); + + writeln!(out, "note_types:").expect("writing to a string cannot fail"); + for (id, note_type) in &deck.note_types { + writeln!(out, " {id}:").expect("writing to a string cannot fail"); + writeln!(out, " name: {}", yaml_scalar(¬e_type.name)) + .expect("writing to a string cannot fail"); + write_variables(&mut out, " ", ¬e_type.variables); + writeln!(out, " field_order:").expect("writing to a string cannot fail"); + for field in ¬e_type.fields { + writeln!(out, " - {}", field.id).expect("writing to a string cannot fail"); + } + writeln!(out, " fields:").expect("writing to a string cannot fail"); + let fields_by_id = note_type + .fields + .iter() + .map(|field| (&field.id, field)) + .collect::>(); + for (field_id, field) in fields_by_id { + writeln!(out, " {field_id}:").expect("writing to a string cannot fail"); + writeln!(out, " name: {}", yaml_scalar(&field.name)) + .expect("writing to a string cannot fail"); + } + writeln!(out, " card_template_order:").expect("writing to a string cannot fail"); + for template in ¬e_type.card_templates { + writeln!(out, " - {}", template.id).expect("writing to a string cannot fail"); + } + writeln!(out, " card_templates:").expect("writing to a string cannot fail"); + let templates_by_id = note_type + .card_templates + .iter() + .map(|template| (&template.id, template)) + .collect::>(); + for (template_id, template) in templates_by_id { + writeln!(out, " {template_id}:").expect("writing to a string cannot fail"); + writeln!(out, " name: {}", yaml_scalar(&template.name)) + .expect("writing to a string cannot fail"); + write_variables(&mut out, " ", &template.variables); + write_multiline_or_scalar( + &mut out, + " ", + "question_format", + &template.question_format, + ); + write_multiline_or_scalar( + &mut out, + " ", + "answer_format", + &template.answer_format, + ); + write_adapter_ids(&mut out, " ", &template.adapter_ids); + } + write_multiline_or_scalar(&mut out, " ", "styling", ¬e_type.styling); + write_adapter_ids(&mut out, " ", ¬e_type.adapter_ids); + } + + writeln!(out, "notes:").expect("writing to a string cannot fail"); + for (id, note) in &deck.notes { + writeln!(out, " {id}:").expect("writing to a string cannot fail"); + writeln!(out, " note_type_id: {}", note.note_type_id) + .expect("writing to a string cannot fail"); + write_variables(&mut out, " ", ¬e.variables); + writeln!(out, " fields:").expect("writing to a string cannot fail"); + for (field_id, value) in ¬e.fields { + writeln!(out, " {field_id}: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + writeln!(out, " tags:").expect("writing to a string cannot fail"); + for tag in ¬e.tags { + writeln!(out, " - {}", yaml_scalar(tag)).expect("writing to a string cannot fail"); + } + write_adapter_ids(&mut out, " ", ¬e.adapter_ids); + } + + writeln!(out, "media:").expect("writing to a string cannot fail"); + for (id, media) in &deck.media { + writeln!(out, " {id}:").expect("writing to a string cannot fail"); + writeln!(out, " path: {}", yaml_scalar(&media.path)) + .expect("writing to a string cannot fail"); + writeln!(out, " sha256: {}", yaml_scalar(&media.sha256)) + .expect("writing to a string cannot fail"); + } + + if deck.tombstones.is_empty() { + writeln!(out, "tombstones: []").expect("writing to a string cannot fail"); + } else { + writeln!(out, "tombstones:").expect("writing to a string cannot fail"); + for tombstone in &deck.tombstones { + writeln!(out, " - {tombstone}").expect("writing to a string cannot fail"); + } + } + + Ok(out) +} + +/// Emit a sparse overlay as deterministic YAML. +pub fn overlay_to_string(overlay: &Overlay) -> String { + let mut out = String::new(); + writeln!(out, "id: {}", overlay.id).expect("writing to a string cannot fail"); + writeln!(out, "kind: {}", overlay_kind_name(overlay.kind)) + .expect("writing to a string cannot fail"); + + let (field_additions, note_type_changes, note_changes) = + split_field_additions_for_format(overlay); + let (field_fills, note_changes) = if overlay.kind == OverlayKind::Translation { + (BTreeMap::new(), note_changes) + } else { + split_field_fills_for_format(note_changes) + }; + + if let Some(translations) = &overlay.translations { + write_translation_dictionary(&mut out, translations); + } + + if !field_additions.is_empty() { + write_field_additions(&mut out, &field_additions); + } + + if !field_fills.is_empty() { + write_field_fills(&mut out, &field_fills); + } + + if let Some(deck_change) = &overlay.deck_change { + writeln!(out, "deck:").expect("writing to a string cannot fail"); + if let Some(change) = &deck_change.name { + write_property_change(&mut out, " ", "name", change); + } + if let Some(change) = &deck_change.description { + write_property_change(&mut out, " ", "description", change); + } + write_property_changes(&mut out, " ", "variables", &deck_change.variables); + write_adapter_id_changes(&mut out, " ", &deck_change.adapter_ids); + } + + if !note_type_changes.is_empty() { + writeln!(out, "note_types:").expect("writing to a string cannot fail"); + for (id, change) in ¬e_type_changes { + writeln!(out, " {id}:").expect("writing to a string cannot fail"); + writeln!(out, " intent: {}", change_intent_name(change.intent)) + .expect("writing to a string cannot fail"); + if let Some(expected_base) = &change.expected_base { + write_expected_base(&mut out, " ", expected_base); + } + if let Some(note_type) = &change.note_type { + writeln!(out, " note_type:").expect("writing to a string cannot fail"); + write_note_type_payload(&mut out, " ", note_type); + } + if let Some(name) = &change.name { + write_property_change(&mut out, " ", "name", name); + } + write_property_changes(&mut out, " ", "variables", &change.variables); + if let Some(styling) = &change.styling { + write_property_change(&mut out, " ", "styling", styling); + } + if !change.fields.is_empty() { + writeln!(out, " fields:").expect("writing to a string cannot fail"); + for (field_id, field_change) in &change.fields { + writeln!(out, " {field_id}:").expect("writing to a string cannot fail"); + writeln!( + out, + " intent: {}", + change_intent_name(field_change.intent) + ) + .expect("writing to a string cannot fail"); + if let Some(field) = &field_change.field { + writeln!(out, " name: {}", yaml_scalar(&field.name)) + .expect("writing to a string cannot fail"); + } + if let Some(expected_base) = &field_change.expected_base { + write_expected_base(&mut out, " ", expected_base); + } + } + } + if !change.card_templates.is_empty() { + writeln!(out, " card_templates:").expect("writing to a string cannot fail"); + for (template_id, template_change) in &change.card_templates { + writeln!(out, " {template_id}:").expect("writing to a string cannot fail"); + writeln!( + out, + " intent: {}", + change_intent_name(template_change.intent) + ) + .expect("writing to a string cannot fail"); + if let Some(expected_base) = &template_change.expected_base { + write_expected_base(&mut out, " ", expected_base); + } + if let Some(insert_after) = &template_change.insert_after { + writeln!(out, " insert_after: {insert_after}") + .expect("writing to a string cannot fail"); + } + if let Some(template) = &template_change.template { + writeln!(out, " template:") + .expect("writing to a string cannot fail"); + write_card_template_payload(&mut out, " ", template); + } + if let Some(name) = &template_change.name { + write_property_change(&mut out, " ", "name", name); + } + write_property_changes( + &mut out, + " ", + "variables", + &template_change.variables, + ); + if let Some(question_format) = &template_change.question_format { + write_property_change( + &mut out, + " ", + "question_format", + question_format, + ); + } + if let Some(answer_format) = &template_change.answer_format { + write_property_change(&mut out, " ", "answer_format", answer_format); + } + write_adapter_id_changes(&mut out, " ", &template_change.adapter_ids); + } + } + write_adapter_id_changes(&mut out, " ", &change.adapter_ids); + } + } + + if !note_changes.is_empty() { + writeln!(out, "notes:").expect("writing to a string cannot fail"); + for (id, change) in ¬e_changes { + writeln!(out, " {id}:").expect("writing to a string cannot fail"); + writeln!(out, " intent: {}", change_intent_name(change.intent)) + .expect("writing to a string cannot fail"); + if let Some(expected_base) = &change.expected_base { + write_expected_base(&mut out, " ", expected_base); + } + if let Some(note) = &change.note { + writeln!(out, " note:").expect("writing to a string cannot fail"); + write_note_payload(&mut out, " ", note); + } + write_property_changes(&mut out, " ", "variables", &change.variables); + if !change.fields.is_empty() { + writeln!(out, " fields:").expect("writing to a string cannot fail"); + for (field_id, field_change) in &change.fields { + write_field_change(&mut out, " ", field_id, field_change); + } + } + if !change.tags.is_empty() { + writeln!(out, " tags:").expect("writing to a string cannot fail"); + for (tag, tag_change) in &change.tags { + writeln!(out, " {}:", yaml_scalar(tag)) + .expect("writing to a string cannot fail"); + writeln!( + out, + " intent: {}", + change_intent_name(tag_change.intent) + ) + .expect("writing to a string cannot fail"); + if let Some(expected_base) = &tag_change.expected_base { + write_expected_base(&mut out, " ", expected_base); + } + } + } + write_adapter_id_changes(&mut out, " ", &change.adapter_ids); + } + } + + if !overlay.media_changes.is_empty() { + writeln!(out, "media:").expect("writing to a string cannot fail"); + for (id, change) in &overlay.media_changes { + writeln!(out, " {id}:").expect("writing to a string cannot fail"); + writeln!(out, " intent: {}", change_intent_name(change.intent)) + .expect("writing to a string cannot fail"); + if let Some(media) = &change.media { + writeln!(out, " path: {}", yaml_scalar(&media.path)) + .expect("writing to a string cannot fail"); + writeln!(out, " sha256: {}", yaml_scalar(&media.sha256)) + .expect("writing to a string cannot fail"); + } + if let Some(expected_base) = &change.expected_base { + write_expected_base(&mut out, " ", expected_base); + } + } + } + + out +} + +#[derive(Default)] +struct FieldAdditionsForFormat { + fields: BTreeMap, + values: BTreeMap>, +} + +fn split_field_additions_for_format( + overlay: &Overlay, +) -> ( + BTreeMap, + BTreeMap, + BTreeMap, +) { + let mut field_additions = BTreeMap::::new(); + let mut note_type_changes = overlay.note_type_changes.clone(); + let mut note_changes = overlay.note_changes.clone(); + let mut field_to_note_type = BTreeMap::::new(); + let mut ambiguous_fields = BTreeSet::::new(); + + for (note_type_id, change) in &overlay.note_type_changes { + if change.intent != ChangeIntent::Merge { + continue; + } + for (field_id, field_change) in &change.fields { + if field_change.intent == ChangeIntent::Add + && field_change.expected_base.is_none() + && let Some(field) = &field_change.field + { + field_additions + .entry(note_type_id.clone()) + .or_default() + .fields + .insert(field_id.clone(), field.name.clone()); + if field_to_note_type + .insert(field_id.clone(), note_type_id.clone()) + .is_some() + { + ambiguous_fields.insert(field_id.clone()); + } + } + } + } + + for field_id in ambiguous_fields { + field_to_note_type.remove(&field_id); + } + + for (note_id, change) in &overlay.note_changes { + if change.intent != ChangeIntent::Merge { + continue; + } + for (field_id, field_change) in &change.fields { + if field_change.intent == ChangeIntent::Add && field_change.expected_base.is_none() { + let Some(value) = &field_change.value else { + continue; + }; + let Some(note_type_id) = field_to_note_type.get(field_id) else { + continue; + }; + field_additions + .entry(note_type_id.clone()) + .or_default() + .values + .entry(note_id.clone()) + .or_default() + .insert(field_id.clone(), value.clone()); + } + } + } + + for (note_type_id, additions) in &field_additions { + let Some(change) = note_type_changes.get_mut(note_type_id) else { + continue; + }; + for field_id in additions.fields.keys() { + change.fields.remove(field_id); + } + } + note_type_changes.retain(|_, change| !is_empty_note_type_merge_change(change)); + + for additions in field_additions.values() { + for (note_id, values) in &additions.values { + let Some(change) = note_changes.get_mut(note_id) else { + continue; + }; + for field_id in values.keys() { + change.fields.remove(field_id); + } + } + } + note_changes.retain(|_, change| !is_empty_note_merge_change(change)); + + field_additions.retain(|_, additions| !additions.fields.is_empty()); + (field_additions, note_type_changes, note_changes) +} + +fn split_field_fills_for_format( + mut note_changes: BTreeMap, +) -> ( + BTreeMap>, + BTreeMap, +) { + let mut field_fills = BTreeMap::>::new(); + + for (note_id, change) in ¬e_changes { + if change.intent != ChangeIntent::Merge { + continue; + } + for (field_id, field_change) in &change.fields { + if let Some(value) = field_fill_value(field_change) { + field_fills + .entry(note_id.clone()) + .or_default() + .insert(field_id.clone(), value.to_owned()); + } + } + } + + for (note_id, fields) in &field_fills { + let Some(change) = note_changes.get_mut(note_id) else { + continue; + }; + for field_id in fields.keys() { + change.fields.remove(field_id); + } + } + note_changes.retain(|_, change| !is_empty_note_merge_change(change)); + + (field_fills, note_changes) +} + +fn field_fill_value(change: &FieldChange) -> Option<&str> { + if change.intent == ChangeIntent::Replace + && matches!(change.expected_base, Some(ExpectedBase::Value(ref value)) if value.is_empty()) + { + change.value.as_deref() + } else { + None + } +} + +fn is_empty_note_type_merge_change(change: &NoteTypeChange) -> bool { + change.intent == ChangeIntent::Merge + && change.note_type.is_none() + && change.name.is_none() + && change.variables.is_empty() + && change.styling.is_none() + && change.fields.is_empty() + && change.card_templates.is_empty() + && change.adapter_ids.is_empty() + && change.expected_base.is_none() +} + +fn is_empty_note_merge_change(change: &NoteChange) -> bool { + change.intent == ChangeIntent::Merge + && change.note.is_none() + && change.variables.is_empty() + && change.fields.is_empty() + && change.tags.is_empty() + && change.adapter_ids.is_empty() + && change.expected_base.is_none() +} + +fn write_field_additions( + out: &mut String, + field_additions: &BTreeMap, +) { + writeln!(out, "field_additions:").expect("writing to a string cannot fail"); + for (note_type_id, additions) in field_additions { + writeln!(out, " {note_type_id}:").expect("writing to a string cannot fail"); + writeln!(out, " fields:").expect("writing to a string cannot fail"); + for (field_id, name) in &additions.fields { + writeln!(out, " {field_id}: {}", yaml_scalar(name)) + .expect("writing to a string cannot fail"); + } + if !additions.values.is_empty() { + writeln!(out, " values:").expect("writing to a string cannot fail"); + for (note_id, values) in &additions.values { + writeln!(out, " {note_id}:").expect("writing to a string cannot fail"); + for (field_id, value) in values { + writeln!(out, " {field_id}: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + } + } + } +} + +fn write_field_fills( + out: &mut String, + field_fills: &BTreeMap>, +) { + writeln!(out, "field_fills:").expect("writing to a string cannot fail"); + for (note_id, fields) in field_fills { + writeln!(out, " {note_id}:").expect("writing to a string cannot fail"); + for (field_id, value) in fields { + writeln!(out, " {field_id}: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + } +} + +fn write_translation_dictionary(out: &mut String, translations: &TranslationDictionary) { + writeln!(out, "translations:").expect("writing to a string cannot fail"); + if translations.require_complete { + writeln!(out, " require_complete: true").expect("writing to a string cannot fail"); + } + if !translations.ignore_paths.is_empty() { + writeln!(out, " ignore_paths:").expect("writing to a string cannot fail"); + for path in &translations.ignore_paths { + writeln!(out, " - {}", yaml_scalar(path)).expect("writing to a string cannot fail"); + } + } + if !translations.changes.is_empty() { + writeln!(out, " changes:").expect("writing to a string cannot fail"); + for (source, change) in &translations.changes { + match change { + TranslationChange::Global(translated) => { + writeln!( + out, + " {}: {}", + yaml_scalar(source), + yaml_scalar(translated) + ) + .expect("writing to a string cannot fail"); + } + TranslationChange::AtPaths(paths) => { + writeln!(out, " {}:", yaml_scalar(source)) + .expect("writing to a string cannot fail"); + for (path, translated) in paths { + writeln!( + out, + " {}: {}", + yaml_scalar(path), + yaml_scalar(translated) + ) + .expect("writing to a string cannot fail"); + } + } + } + } + } + if !translations.additions.is_empty() { + writeln!(out, " additions:").expect("writing to a string cannot fail"); + for (path, value) in &translations.additions { + writeln!(out, " {}: {}", yaml_scalar(path), yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + } + if !translations.variables.is_empty() { + writeln!(out, " variables:").expect("writing to a string cannot fail"); + for (variable_key, replacements) in &translations.variables { + writeln!(out, " {variable_key}:").expect("writing to a string cannot fail"); + for (source, translated) in replacements { + writeln!( + out, + " {}: {}", + yaml_scalar(source), + yaml_scalar(translated) + ) + .expect("writing to a string cannot fail"); + } + } + } + if !translations.adapter_ids.is_empty() { + writeln!(out, " adapter_ids:").expect("writing to a string cannot fail"); + for (adapter_key, replacements) in &translations.adapter_ids { + writeln!(out, " {adapter_key}:").expect("writing to a string cannot fail"); + for (source, translated) in replacements { + writeln!( + out, + " {}: {}", + yaml_scalar(source), + yaml_scalar(translated) + ) + .expect("writing to a string cannot fail"); + } + } + } +} + +fn write_variables(out: &mut String, indent: &str, variables: &BTreeMap) { + if variables.is_empty() { + return; + } + writeln!(out, "{indent}variables:").expect("writing to a string cannot fail"); + for (key, value) in variables { + write_multiline_or_scalar(out, &format!("{indent} "), key, value); + } +} + +fn write_property_changes( + out: &mut String, + indent: &str, + key: &str, + changes: &BTreeMap, +) { + if changes.is_empty() { + return; + } + writeln!(out, "{indent}{key}:").expect("writing to a string cannot fail"); + for (change_key, change) in changes { + write_property_change(out, &format!("{indent} "), change_key, change); + } +} + +fn write_adapter_ids(out: &mut String, indent: &str, adapter_ids: &AdapterIds) { + if adapter_ids.is_empty() { + writeln!(out, "{indent}adapter_ids: {{}}").expect("writing to a string cannot fail"); + return; + } + + writeln!(out, "{indent}adapter_ids:").expect("writing to a string cannot fail"); + for (key, value) in adapter_ids.iter() { + writeln!(out, "{indent} {key}: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } +} + +fn write_property_change(out: &mut String, indent: &str, key: &str, change: &PropertyChange) { + writeln!(out, "{indent}{key}:").expect("writing to a string cannot fail"); + writeln!( + out, + "{indent} intent: {}", + change_intent_name(change.intent) + ) + .expect("writing to a string cannot fail"); + if let Some(value) = &change.value { + write_multiline_or_scalar(out, &format!("{indent} "), "value", value); + } + if let Some(expected_base) = &change.expected_base { + write_expected_base(out, &format!("{indent} "), expected_base); + } +} + +fn write_adapter_id_changes( + out: &mut String, + indent: &str, + adapter_ids: &BTreeMap, +) { + if adapter_ids.is_empty() { + return; + } + writeln!(out, "{indent}adapter_ids:").expect("writing to a string cannot fail"); + for (key, change) in adapter_ids { + writeln!(out, "{indent} {key}:").expect("writing to a string cannot fail"); + writeln!( + out, + "{indent} intent: {}", + change_intent_name(change.intent) + ) + .expect("writing to a string cannot fail"); + if let Some(value) = &change.value { + write_multiline_or_scalar(out, &format!("{indent} "), "value", value); + } + if let Some(expected_base) = &change.expected_base { + write_expected_base(out, &format!("{indent} "), expected_base); + } + } +} + +fn write_field_change(out: &mut String, indent: &str, field_id: &StableId, change: &FieldChange) { + writeln!(out, "{indent}{field_id}:").expect("writing to a string cannot fail"); + writeln!( + out, + "{indent} intent: {}", + change_intent_name(change.intent) + ) + .expect("writing to a string cannot fail"); + if let Some(value) = &change.value { + write_multiline_or_scalar(out, &format!("{indent} "), "value", value); + } + if let Some(expected_base) = &change.expected_base { + write_expected_base(out, &format!("{indent} "), expected_base); + } +} + +fn write_note_type_payload(out: &mut String, indent: &str, note_type: &NoteType) { + writeln!(out, "{indent}name: {}", yaml_scalar(¬e_type.name)) + .expect("writing to a string cannot fail"); + write_variables(out, indent, ¬e_type.variables); + writeln!(out, "{indent}field_order:").expect("writing to a string cannot fail"); + for field in ¬e_type.fields { + writeln!(out, "{indent} - {}", field.id).expect("writing to a string cannot fail"); + } + writeln!(out, "{indent}fields:").expect("writing to a string cannot fail"); + let fields_by_id = note_type + .fields + .iter() + .map(|field| (&field.id, field)) + .collect::>(); + for (field_id, field) in fields_by_id { + writeln!(out, "{indent} {field_id}:").expect("writing to a string cannot fail"); + writeln!(out, "{indent} name: {}", yaml_scalar(&field.name)) + .expect("writing to a string cannot fail"); + } + writeln!(out, "{indent}card_template_order:").expect("writing to a string cannot fail"); + for template in ¬e_type.card_templates { + writeln!(out, "{indent} - {}", template.id).expect("writing to a string cannot fail"); + } + writeln!(out, "{indent}card_templates:").expect("writing to a string cannot fail"); + let templates_by_id = note_type + .card_templates + .iter() + .map(|template| (&template.id, template)) + .collect::>(); + for (template_id, template) in templates_by_id { + writeln!(out, "{indent} {template_id}:").expect("writing to a string cannot fail"); + writeln!(out, "{indent} name: {}", yaml_scalar(&template.name)) + .expect("writing to a string cannot fail"); + write_variables(out, &format!("{indent} "), &template.variables); + write_multiline_or_scalar( + out, + &format!("{indent} "), + "question_format", + &template.question_format, + ); + write_multiline_or_scalar( + out, + &format!("{indent} "), + "answer_format", + &template.answer_format, + ); + write_adapter_ids(out, &format!("{indent} "), &template.adapter_ids); + } + write_multiline_or_scalar(out, indent, "styling", ¬e_type.styling); + write_adapter_ids(out, indent, ¬e_type.adapter_ids); +} + +fn write_note_payload(out: &mut String, indent: &str, note: &Note) { + writeln!(out, "{indent}note_type_id: {}", note.note_type_id) + .expect("writing to a string cannot fail"); + write_variables(out, indent, ¬e.variables); + writeln!(out, "{indent}fields:").expect("writing to a string cannot fail"); + for (field_id, value) in ¬e.fields { + writeln!(out, "{indent} {field_id}: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + writeln!(out, "{indent}tags:").expect("writing to a string cannot fail"); + for tag in ¬e.tags { + writeln!(out, "{indent} - {}", yaml_scalar(tag)).expect("writing to a string cannot fail"); + } + write_adapter_ids(out, indent, ¬e.adapter_ids); +} + +fn write_card_template_payload(out: &mut String, indent: &str, template: &CardTemplate) { + writeln!(out, "{indent}name: {}", yaml_scalar(&template.name)) + .expect("writing to a string cannot fail"); + write_variables(out, indent, &template.variables); + write_multiline_or_scalar(out, indent, "question_format", &template.question_format); + write_multiline_or_scalar(out, indent, "answer_format", &template.answer_format); + write_adapter_ids(out, indent, &template.adapter_ids); +} + +fn write_expected_base(out: &mut String, indent: &str, expected_base: &ExpectedBase) { + match expected_base { + ExpectedBase::EntityPresent => { + writeln!(out, "{indent}expected_base: entity_present") + .expect("writing to a string cannot fail"); + } + ExpectedBase::Value(value) => { + writeln!(out, "{indent}expected_base:").expect("writing to a string cannot fail"); + write_multiline_or_scalar(out, &format!("{indent} "), "value", value); + } + } +} + +fn overlay_kind_name(kind: OverlayKind) -> &'static str { + match kind { + OverlayKind::Translation => "translation", + OverlayKind::Extension => "extension", + OverlayKind::Patch => "patch", + OverlayKind::Personal => "personal", + } +} + +fn change_intent_name(intent: ChangeIntent) -> &'static str { + match intent { + ChangeIntent::Add => "add", + ChangeIntent::Merge => "merge", + ChangeIntent::Replace => "replace", + ChangeIntent::Remove => "remove", + ChangeIntent::Override => "override", + } +} + +fn write_multiline_or_scalar(out: &mut String, indent: &str, key: &str, value: &str) { + if value.contains('\n') { + let chomp = if value.ends_with('\n') { "|" } else { "|-" }; + writeln!(out, "{indent}{key}: {chomp}").expect("writing to a string cannot fail"); + for line in value.lines() { + writeln!(out, "{indent} {line}").expect("writing to a string cannot fail"); + } + } else { + writeln!(out, "{indent}{key}: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } +} + +fn yaml_scalar(value: &str) -> String { + if can_emit_plain_scalar(value) { + value.to_owned() + } else { + format!("'{}'", value.replace('\'', "''")) + } +} + +fn can_emit_plain_scalar(value: &str) -> bool { + !value.is_empty() + && !value.starts_with([ + ' ', '-', '?', ':', '@', '`', '&', '*', '!', '|', '>', '#', '{', '[', ',', + ]) + && !value.ends_with(' ') + && value + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, ' ' | '.' | ',' | '_' | '-' | '/')) + && !value.chars().all(|ch| ch.is_ascii_digit()) + && !matches!( + value, + "true" | "false" | "True" | "False" | "TRUE" | "FALSE" | "null" | "Null" | "NULL" + ) +} + +#[derive(Debug)] +pub enum CanonicalYamlError { + Parse(serde_yaml::Error), + StableId(InvalidStableId), + InvalidOverlayKind(String), + InvalidChangeIntent(String), + InvalidExpectedBase(String), + InvalidFieldAddition(String), + InvalidFieldFill(String), + MissingOrderedEntity { section: &'static str, id: String }, + UnorderedEntity { section: &'static str, id: String }, + Validation(ValidationReport), +} + +impl fmt::Display for CanonicalYamlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Parse(error) => write!(f, "failed to parse canonical YAML: {error}"), + Self::StableId(error) => write!(f, "{error}"), + Self::InvalidOverlayKind(kind) => write!(f, "invalid overlay kind {kind:?}"), + Self::InvalidChangeIntent(intent) => write!(f, "invalid change intent {intent:?}"), + Self::InvalidExpectedBase(expected_base) => { + write!(f, "invalid expected base {expected_base:?}") + } + Self::InvalidFieldAddition(message) => write!(f, "invalid field addition: {message}"), + Self::InvalidFieldFill(message) => write!(f, "invalid field fill: {message}"), + Self::MissingOrderedEntity { section, id } => { + write!(f, "{section} order references missing entity {id}") + } + Self::UnorderedEntity { section, id } => { + write!(f, "{section} entity {id} is missing from its order array") + } + Self::Validation(report) => write!(f, "canonical deck validation failed: {report}"), + } + } +} + +impl std::error::Error for CanonicalYamlError {} + +impl From for CanonicalYamlError { + fn from(error: InvalidStableId) -> Self { + Self::StableId(error) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct OverlayYaml { + id: String, + kind: String, + #[serde(default)] + translations: Option, + #[serde(default)] + deck: Option, + #[serde(default)] + field_additions: BTreeMap, + #[serde(default)] + field_fills: BTreeMap>, + #[serde(default)] + notes: BTreeMap, + #[serde(default)] + note_types: BTreeMap, + #[serde(default)] + media: BTreeMap, +} + +impl OverlayYaml { + fn into_overlay(self) -> Result { + let mut note_changes = self + .notes + .into_iter() + .map(|(id, change)| { + let id = sid(&id)?; + Ok((id.clone(), change.into_note_change(id)?)) + }) + .collect::, CanonicalYamlError>>()?; + let mut note_type_changes = self + .note_types + .into_iter() + .map(|(id, change)| { + let id = sid(&id)?; + Ok((id.clone(), change.into_note_type_change(id)?)) + }) + .collect::, CanonicalYamlError>>()?; + + for (note_type_id, additions) in self.field_additions { + additions.apply( + sid(¬e_type_id)?, + &mut note_type_changes, + &mut note_changes, + )?; + } + + apply_field_fills(self.field_fills, &mut note_changes)?; + + Ok(Overlay { + id: sid(&self.id)?, + kind: parse_overlay_kind(&self.kind)?, + translations: self + .translations + .map(TranslationDictionaryYaml::into_translation_dictionary), + deck_change: self + .deck + .map(DeckChangeYaml::into_deck_change) + .transpose()?, + note_changes, + note_type_changes, + media_changes: self + .media + .into_iter() + .map(|(id, change)| { + let id = sid(&id)?; + Ok((id.clone(), change.into_media_change(id)?)) + }) + .collect::>()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct FieldAdditionsYaml { + fields: BTreeMap, + #[serde(default)] + values: BTreeMap>, +} + +impl FieldAdditionsYaml { + fn apply( + self, + note_type_id: StableId, + note_type_changes: &mut BTreeMap, + note_changes: &mut BTreeMap, + ) -> Result<(), CanonicalYamlError> { + if self.fields.is_empty() { + return Err(CanonicalYamlError::InvalidFieldAddition(format!( + "field_additions.{note_type_id}.fields must not be empty" + ))); + } + + let mut declared_fields = BTreeSet::new(); + let note_type_change = note_type_changes + .entry(note_type_id.clone()) + .or_insert_with(empty_note_type_merge_change); + if note_type_change.intent != ChangeIntent::Merge { + return Err(CanonicalYamlError::InvalidFieldAddition(format!( + "field_additions.{note_type_id} can only merge into an existing note type" + ))); + } + + for (field_id, name) in self.fields { + let field_id = sid(&field_id)?; + if !declared_fields.insert(field_id.clone()) { + return Err(CanonicalYamlError::InvalidFieldAddition(format!( + "duplicate field_additions.{note_type_id}.fields.{field_id}" + ))); + } + if note_type_change.fields.contains_key(&field_id) { + return Err(CanonicalYamlError::InvalidFieldAddition(format!( + "field_additions.{note_type_id}.fields.{field_id} conflicts with another field change" + ))); + } + note_type_change.fields.insert( + field_id.clone(), + FieldDefinitionChange { + intent: ChangeIntent::Add, + field: Some(FieldDefinition { id: field_id, name }), + expected_base: None, + }, + ); + } + + for (note_id, values) in self.values { + let note_id = sid(¬e_id)?; + let note_change = note_changes + .entry(note_id.clone()) + .or_insert_with(empty_note_merge_change); + if note_change.intent != ChangeIntent::Merge { + return Err(CanonicalYamlError::InvalidFieldAddition(format!( + "field_additions.{note_type_id}.values.{note_id} can only merge into an existing note" + ))); + } + for (field_id, value) in values { + let field_id = sid(&field_id)?; + if !declared_fields.contains(&field_id) { + return Err(CanonicalYamlError::InvalidFieldAddition(format!( + "field_additions.{note_type_id}.values.{note_id}.{field_id} has no declared field" + ))); + } + if note_change.fields.contains_key(&field_id) { + return Err(CanonicalYamlError::InvalidFieldAddition(format!( + "field_additions.{note_type_id}.values.{note_id}.{field_id} conflicts with another field change" + ))); + } + note_change.fields.insert( + field_id, + FieldChange { + intent: ChangeIntent::Add, + value: Some(value), + expected_base: None, + }, + ); + } + } + + Ok(()) + } +} + +fn apply_field_fills( + field_fills: BTreeMap>, + note_changes: &mut BTreeMap, +) -> Result<(), CanonicalYamlError> { + for (note_id, fields) in field_fills { + let note_id = sid(¬e_id)?; + if fields.is_empty() { + return Err(CanonicalYamlError::InvalidFieldFill(format!( + "field_fills.{note_id} must not be empty" + ))); + } + let note_change = note_changes + .entry(note_id.clone()) + .or_insert_with(empty_note_merge_change); + if note_change.intent != ChangeIntent::Merge { + return Err(CanonicalYamlError::InvalidFieldFill(format!( + "field_fills.{note_id} can only merge into an existing note" + ))); + } + for (field_id, value) in fields { + let field_id = sid(&field_id)?; + if note_change.fields.contains_key(&field_id) { + return Err(CanonicalYamlError::InvalidFieldFill(format!( + "field_fills.{note_id}.{field_id} conflicts with another field change" + ))); + } + note_change.fields.insert( + field_id, + FieldChange { + intent: ChangeIntent::Replace, + value: Some(value), + expected_base: Some(ExpectedBase::Value(String::new())), + }, + ); + } + } + + Ok(()) +} + +fn empty_note_type_merge_change() -> NoteTypeChange { + NoteTypeChange { + intent: ChangeIntent::Merge, + note_type: None, + name: None, + variables: BTreeMap::new(), + styling: None, + fields: BTreeMap::new(), + card_templates: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + } +} + +fn empty_note_merge_change() -> NoteChange { + NoteChange { + intent: ChangeIntent::Merge, + note: None, + variables: BTreeMap::new(), + fields: BTreeMap::new(), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct TranslationDictionaryYaml { + #[serde(default)] + changes: BTreeMap, + #[serde(default)] + additions: BTreeMap, + #[serde(default)] + variables: BTreeMap>, + #[serde(default)] + adapter_ids: BTreeMap>, + #[serde(default)] + require_complete: bool, + #[serde(default)] + ignore_paths: BTreeSet, +} + +impl TranslationDictionaryYaml { + fn into_translation_dictionary(self) -> TranslationDictionary { + TranslationDictionary { + changes: self + .changes + .into_iter() + .map(|(source, change)| (source, change.into_translation_change())) + .collect(), + additions: self.additions, + variables: self.variables, + adapter_ids: self.adapter_ids, + require_complete: self.require_complete, + ignore_paths: self.ignore_paths, + } + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum TranslationChangeYaml { + Global(String), + AtPaths(BTreeMap), +} + +impl TranslationChangeYaml { + fn into_translation_change(self) -> TranslationChange { + match self { + Self::Global(value) => TranslationChange::Global(value), + Self::AtPaths(paths) => TranslationChange::AtPaths(paths), + } + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct DeckChangeYaml { + #[serde(default)] + name: Option, + #[serde(default)] + description: Option, + #[serde(default)] + variables: BTreeMap, + #[serde(default)] + adapter_ids: BTreeMap, +} + +impl DeckChangeYaml { + fn into_deck_change(self) -> Result { + Ok(DeckChange { + name: self + .name + .map(PropertyChangeYaml::into_property_change) + .transpose()?, + description: self + .description + .map(PropertyChangeYaml::into_property_change) + .transpose()?, + variables: self + .variables + .into_iter() + .map(|(key, change)| Ok((key, change.into_property_change()?))) + .collect::>()?, + adapter_ids: self + .adapter_ids + .into_iter() + .map(|(key, change)| Ok((key, change.into_adapter_id_change()?))) + .collect::>()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct NoteTypeChangeYaml { + intent: String, + #[serde(default)] + note_type: Option, + #[serde(default)] + name: Option, + #[serde(default)] + variables: BTreeMap, + #[serde(default)] + styling: Option, + #[serde(default)] + fields: BTreeMap, + #[serde(default)] + card_templates: BTreeMap, + #[serde(default)] + adapter_ids: BTreeMap, + #[serde(default)] + expected_base: Option, +} + +impl NoteTypeChangeYaml { + fn into_note_type_change(self, id: StableId) -> Result { + Ok(NoteTypeChange { + intent: parse_change_intent(&self.intent)?, + note_type: self + .note_type + .map(|note_type| note_type.into_note_type(id.clone())) + .transpose()?, + name: self + .name + .map(PropertyChangeYaml::into_property_change) + .transpose()?, + variables: self + .variables + .into_iter() + .map(|(key, change)| Ok((key, change.into_property_change()?))) + .collect::>()?, + styling: self + .styling + .map(PropertyChangeYaml::into_property_change) + .transpose()?, + fields: self + .fields + .into_iter() + .map(|(id, change)| { + let id = sid(&id)?; + Ok((id.clone(), change.into_field_definition_change(id)?)) + }) + .collect::>()?, + card_templates: self + .card_templates + .into_iter() + .map(|(id, change)| { + let id = sid(&id)?; + Ok((id.clone(), change.into_card_template_change(id)?)) + }) + .collect::>()?, + adapter_ids: self + .adapter_ids + .into_iter() + .map(|(key, change)| Ok((key, change.into_adapter_id_change()?))) + .collect::>()?, + expected_base: self + .expected_base + .map(ExpectedBaseYaml::into_expected_base) + .transpose()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct CardTemplatePayloadYaml { + name: String, + #[serde(default)] + variables: BTreeMap, + question_format: String, + answer_format: String, + #[serde(default)] + adapter_ids: BTreeMap, +} + +impl CardTemplatePayloadYaml { + fn into_card_template(self, id: StableId) -> Result { + Ok(CardTemplate { + id, + name: self.name, + variables: self.variables, + question_format: self.question_format, + answer_format: self.answer_format, + adapter_ids: adapter_ids_from_map(self.adapter_ids), + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct CardTemplateChangeYaml { + intent: String, + #[serde(default)] + template: Option, + #[serde(default)] + insert_after: Option, + #[serde(default)] + name: Option, + #[serde(default)] + variables: BTreeMap, + #[serde(default)] + question_format: Option, + #[serde(default)] + answer_format: Option, + #[serde(default)] + adapter_ids: BTreeMap, + #[serde(default)] + expected_base: Option, +} + +impl CardTemplateChangeYaml { + fn into_card_template_change( + self, + id: StableId, + ) -> Result { + Ok(CardTemplateChange { + intent: parse_change_intent(&self.intent)?, + template: self + .template + .map(|template| template.into_card_template(id)) + .transpose()?, + insert_after: self.insert_after.map(|id| sid(&id)).transpose()?, + name: self + .name + .map(PropertyChangeYaml::into_property_change) + .transpose()?, + variables: self + .variables + .into_iter() + .map(|(key, change)| Ok((key, change.into_property_change()?))) + .collect::>()?, + question_format: self + .question_format + .map(PropertyChangeYaml::into_property_change) + .transpose()?, + answer_format: self + .answer_format + .map(PropertyChangeYaml::into_property_change) + .transpose()?, + adapter_ids: self + .adapter_ids + .into_iter() + .map(|(key, change)| Ok((key, change.into_adapter_id_change()?))) + .collect::>()?, + expected_base: self + .expected_base + .map(ExpectedBaseYaml::into_expected_base) + .transpose()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct PropertyChangeYaml { + intent: String, + #[serde(default)] + value: Option, + #[serde(default)] + expected_base: Option, +} + +impl PropertyChangeYaml { + fn into_property_change(self) -> Result { + Ok(PropertyChange { + intent: parse_change_intent(&self.intent)?, + value: self.value, + expected_base: self + .expected_base + .map(ExpectedBaseYaml::into_expected_base) + .transpose()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct AdapterIdChangeYaml { + intent: String, + #[serde(default)] + value: Option, + #[serde(default)] + expected_base: Option, +} + +impl AdapterIdChangeYaml { + fn into_adapter_id_change(self) -> Result { + Ok(AdapterIdChange { + intent: parse_change_intent(&self.intent)?, + value: self.value, + expected_base: self + .expected_base + .map(ExpectedBaseYaml::into_expected_base) + .transpose()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct FieldDefinitionChangeYaml { + intent: String, + #[serde(default)] + name: Option, + #[serde(default)] + expected_base: Option, +} + +impl FieldDefinitionChangeYaml { + fn into_field_definition_change( + self, + id: StableId, + ) -> Result { + Ok(FieldDefinitionChange { + intent: parse_change_intent(&self.intent)?, + field: self.name.map(|name| FieldDefinition { id, name }), + expected_base: self + .expected_base + .map(ExpectedBaseYaml::into_expected_base) + .transpose()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct NoteChangeYaml { + intent: String, + #[serde(default)] + note: Option, + #[serde(default)] + variables: BTreeMap, + #[serde(default)] + fields: BTreeMap, + #[serde(default)] + tags: BTreeMap, + #[serde(default)] + adapter_ids: BTreeMap, + #[serde(default)] + expected_base: Option, +} + +impl NoteChangeYaml { + fn into_note_change(self, id: StableId) -> Result { + Ok(NoteChange { + intent: parse_change_intent(&self.intent)?, + note: self.note.map(|note| note.into_note(id)).transpose()?, + variables: self + .variables + .into_iter() + .map(|(key, change)| Ok((key, change.into_property_change()?))) + .collect::>()?, + fields: self + .fields + .into_iter() + .map(|(id, change)| { + let id = sid(&id)?; + Ok((id, change.into_field_change()?)) + }) + .collect::>()?, + tags: self + .tags + .into_iter() + .map(|(tag, change)| Ok((tag, change.into_tag_change()?))) + .collect::>()?, + adapter_ids: self + .adapter_ids + .into_iter() + .map(|(key, change)| Ok((key, change.into_adapter_id_change()?))) + .collect::>()?, + expected_base: self + .expected_base + .map(ExpectedBaseYaml::into_expected_base) + .transpose()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct FieldChangeYaml { + intent: String, + #[serde(default)] + value: Option, + #[serde(default)] + expected_base: Option, +} + +impl FieldChangeYaml { + fn into_field_change(self) -> Result { + Ok(FieldChange { + intent: parse_change_intent(&self.intent)?, + value: self.value, + expected_base: self + .expected_base + .map(ExpectedBaseYaml::into_expected_base) + .transpose()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct TagChangeYaml { + intent: String, + #[serde(default)] + expected_base: Option, +} + +impl TagChangeYaml { + fn into_tag_change(self) -> Result { + Ok(TagChange { + intent: parse_change_intent(&self.intent)?, + expected_base: self + .expected_base + .map(ExpectedBaseYaml::into_expected_base) + .transpose()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct MediaChangeYaml { + intent: String, + #[serde(default)] + path: Option, + #[serde(default)] + sha256: Option, + #[serde(default)] + expected_base: Option, +} + +impl MediaChangeYaml { + fn into_media_change(self, id: StableId) -> Result { + let media = match (self.path, self.sha256) { + (Some(path), Some(sha256)) => Some(MediaReference { id, path, sha256 }), + _ => None, + }; + Ok(MediaChange { + intent: parse_change_intent(&self.intent)?, + media, + expected_base: self + .expected_base + .map(ExpectedBaseYaml::into_expected_base) + .transpose()?, + }) + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum ExpectedBaseYaml { + Marker(String), + Value { value: String }, +} + +impl ExpectedBaseYaml { + fn into_expected_base(self) -> Result { + match self { + Self::Marker(marker) if marker == "entity_present" => Ok(ExpectedBase::EntityPresent), + Self::Marker(marker) => Err(CanonicalYamlError::InvalidExpectedBase(marker)), + Self::Value { value } => Ok(ExpectedBase::Value(value)), + } + } +} + +fn parse_overlay_kind(kind: &str) -> Result { + match kind { + "translation" => Ok(OverlayKind::Translation), + "extension" => Ok(OverlayKind::Extension), + "patch" => Ok(OverlayKind::Patch), + "personal" => Ok(OverlayKind::Personal), + other => Err(CanonicalYamlError::InvalidOverlayKind(other.to_owned())), + } +} + +fn parse_change_intent(intent: &str) -> Result { + match intent { + "add" => Ok(ChangeIntent::Add), + "merge" => Ok(ChangeIntent::Merge), + "replace" => Ok(ChangeIntent::Replace), + "remove" => Ok(ChangeIntent::Remove), + "override" => Ok(ChangeIntent::Override), + other => Err(CanonicalYamlError::InvalidChangeIntent(other.to_owned())), + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct CanonicalDeckYaml { + deck: DeckYaml, + note_types: BTreeMap, + notes: BTreeMap, + #[serde(default)] + media: BTreeMap, + #[serde(default)] + tombstones: Vec, +} + +impl CanonicalDeckYaml { + fn into_deck(self) -> Result { + Ok(CanonicalDeck { + id: sid(&self.deck.id)?, + name: self.deck.name, + description: self.deck.description, + variables: self.deck.variables, + note_types: self + .note_types + .into_iter() + .map(|(id, note_type)| { + let stable_id = sid(&id)?; + Ok((stable_id.clone(), note_type.into_note_type(stable_id)?)) + }) + .collect::>()?, + notes: self + .notes + .into_iter() + .map(|(id, note)| { + let stable_id = sid(&id)?; + Ok((stable_id.clone(), note.into_note(stable_id)?)) + }) + .collect::>()?, + media: self + .media + .into_iter() + .map(|(id, media)| { + let stable_id = sid(&id)?; + Ok((stable_id.clone(), media.into_media(stable_id))) + }) + .collect::>()?, + tombstones: self + .tombstones + .into_iter() + .map(|id| sid(&id)) + .collect::, _>>()?, + adapter_ids: adapter_ids_from_map(self.deck.adapter_ids), + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct DeckYaml { + id: String, + name: String, + description: String, + #[serde(default)] + variables: BTreeMap, + #[serde(default)] + adapter_ids: BTreeMap, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct NoteTypeYaml { + name: String, + #[serde(default)] + variables: BTreeMap, + field_order: Vec, + fields: BTreeMap, + card_template_order: Vec, + card_templates: BTreeMap, + styling: String, + #[serde(default)] + adapter_ids: BTreeMap, +} + +impl NoteTypeYaml { + fn into_note_type(self, id: StableId) -> Result { + let fields = ordered_values("fields", self.fields, self.field_order, |id, field| { + Ok(FieldDefinition { + id, + name: field.name, + }) + })?; + let card_templates = ordered_values( + "card_templates", + self.card_templates, + self.card_template_order, + |id, template| { + Ok(CardTemplate { + id, + name: template.name, + variables: template.variables, + question_format: template.question_format, + answer_format: template.answer_format, + adapter_ids: adapter_ids_from_map(template.adapter_ids), + }) + }, + )?; + + Ok(NoteType { + id, + name: self.name, + variables: self.variables, + fields, + card_templates, + styling: self.styling, + adapter_ids: adapter_ids_from_map(self.adapter_ids), + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct FieldYaml { + name: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct CardTemplateYaml { + name: String, + #[serde(default)] + variables: BTreeMap, + question_format: String, + answer_format: String, + #[serde(default)] + adapter_ids: BTreeMap, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct NoteYaml { + note_type_id: String, + #[serde(default)] + variables: BTreeMap, + fields: BTreeMap, + #[serde(default)] + tags: BTreeSet, + #[serde(default)] + adapter_ids: BTreeMap, +} + +impl NoteYaml { + fn into_note(self, id: StableId) -> Result { + Ok(Note { + id, + note_type_id: sid(&self.note_type_id)?, + variables: self.variables, + fields: self + .fields + .into_iter() + .map(|(id, value)| Ok((sid(&id)?, value))) + .collect::>()?, + tags: self.tags, + adapter_ids: adapter_ids_from_map(self.adapter_ids), + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct MediaYaml { + path: String, + sha256: String, +} + +impl MediaYaml { + fn into_media(self, id: StableId) -> MediaReference { + MediaReference { + id, + path: self.path, + sha256: self.sha256, + } + } +} + +fn ordered_values( + section: &'static str, + mut values: BTreeMap, + order: Vec, + convert: impl Fn(StableId, T) -> Result, +) -> Result, CanonicalYamlError> { + let mut ordered = Vec::new(); + for id in order { + let Some(value) = values.remove(&id) else { + return Err(CanonicalYamlError::MissingOrderedEntity { section, id }); + }; + ordered.push(convert(sid(&id)?, value)?); + } + + if let Some(id) = values.into_keys().next() { + return Err(CanonicalYamlError::UnorderedEntity { section, id }); + } + + Ok(ordered) +} + +fn adapter_ids_from_map(map: BTreeMap) -> AdapterIds { + let mut adapter_ids = AdapterIds::new(); + for (key, value) in map { + adapter_ids.insert(key, value); + } + adapter_ids +} + +fn sid(value: &str) -> Result { + StableId::new(value) +} diff --git a/crates/brain-brew-formats/src/crowdanki.rs b/crates/brain-brew-formats/src/crowdanki.rs new file mode 100644 index 0000000..ab337dc --- /dev/null +++ b/crates/brain-brew-formats/src/crowdanki.rs @@ -0,0 +1,1110 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +use brain_brew_core::{ + AdapterIds, CanonicalDeck, CardTemplate, FieldDefinition, MediaReference, Note, NoteType, + StableId, ValidationReport, VariableRenderReport, +}; +use serde::{Deserialize, Serialize}; + +/// Normalized CrowdAnki export artifacts and adapter report data. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CrowdAnkiExport { + pub deck_json: String, + pub omitted_tombstones: Vec, +} + +/// Export a CanonicalDeck to deterministic normalized CrowdAnki `deck.json` bytes. +pub fn export_deck(deck: &CanonicalDeck) -> Result { + deck.validate().map_err(CrowdAnkiError::Validation)?; + let rendered_deck = deck + .render_variables() + .map_err(CrowdAnkiError::VariableRender)?; + rendered_deck + .validate() + .map_err(CrowdAnkiError::Validation)?; + let deck = &rendered_deck; + + let note_models = deck + .note_types + .values() + .map(export_note_model) + .collect::, _>>()?; + + let note_type_uuids = deck + .note_types + .iter() + .map(|(id, note_type)| Ok((id.clone(), crowdanki_note_model_uuid(note_type)?))) + .collect::, CrowdAnkiError>>()?; + + let mut omitted_tombstones = Vec::new(); + let mut notes = Vec::new(); + for (id, note) in &deck.notes { + if deck.tombstones.contains(id) { + omitted_tombstones.push(id.clone()); + continue; + } + notes.push(export_note(note, deck, ¬e_type_uuids)?); + } + + let deck_config_uuid = crowdanki_deck_config_uuid(deck); + let deck_json = CrowdAnkiDeckJson { + type_: "Deck".to_owned(), + children: Vec::new(), + crowdanki_uuid: crowdanki_deck_uuid(deck), + deck_config_uuid: deck_config_uuid.clone(), + deck_configurations: vec![default_deck_config_json( + &deck_config_uuid, + &crowdanki_deck_config_name(deck), + )], + desc: deck.description.clone(), + dyn_: 0, + extend_new: 10, + extend_rev: 50, + media_files: deck + .media + .values() + .map(|media| media.path.clone()) + .collect::>(), + name: deck.name.clone(), + note_models, + notes, + }; + + let mut serialized = serde_json::to_string_pretty(&deck_json).map_err(CrowdAnkiError::Json)?; + serialized.push('\n'); + + Ok(CrowdAnkiExport { + deck_json: serialized, + omitted_tombstones, + }) +} + +/// Import normalized CrowdAnki `deck.json`, accepting generated stable IDs. +pub fn import_deck_accept_suggested_ids(input: &str) -> Result { + let deck_json: CrowdAnkiDeckJson = serde_json::from_str(input).map_err(CrowdAnkiError::Json)?; + deck_json.into_deck() +} + +/// Options for comparing generated CrowdAnki JSON with an expected oracle. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct CrowdAnkiParityOptions { + /// JSON path globs explicitly allowed to differ. + pub allowed_path_globs: BTreeSet, +} + +/// A CrowdAnki parity comparison failure report. +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct CrowdAnkiParityReport { + pub differences: Vec, +} + +/// One exact JSON difference between expected and actual CrowdAnki output. +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct CrowdAnkiParityDifference { + pub path: String, + pub kind: CrowdAnkiParityDifferenceKind, + pub expected: Option, + pub actual: Option, +} + +/// The broad shape of a CrowdAnki JSON parity difference. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum CrowdAnkiParityDifferenceKind { + MissingActual, + ExtraActual, + ValueMismatch, + LengthMismatch, +} + +/// Compare two CrowdAnki `deck.json` values exactly, with only explicit path allowlists. +pub fn compare_deck_json_values( + expected: &serde_json::Value, + actual: &serde_json::Value, + options: &CrowdAnkiParityOptions, +) -> Result<(), CrowdAnkiParityReport> { + let mut differences = Vec::new(); + compare_json_value(expected, actual, "$", options, &mut differences); + if differences.is_empty() { + Ok(()) + } else { + Err(CrowdAnkiParityReport { differences }) + } +} + +impl fmt::Display for CrowdAnkiParityReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "{} CrowdAnki JSON difference(s)", self.differences.len())?; + let grouped_paths = repeated_difference_groups(&self.differences); + if !grouped_paths.is_empty() { + writeln!(f, "Repeated differences:")?; + for group in &grouped_paths { + writeln!( + f, + "{} × {} ({:?}): expected {}, actual {}", + group.count, + group.path_pattern, + group.kind, + json_value_summary(group.expected.as_ref()), + json_value_summary(group.actual.as_ref()) + )?; + } + } + let grouped_patterns = grouped_paths + .iter() + .map(|group| group.path_pattern.as_str()) + .collect::>(); + let mut shown = 0; + for difference in &self.differences { + if grouped_patterns.contains(normalize_repeated_path(&difference.path).as_str()) { + continue; + } + if shown >= 20 { + break; + } + writeln!( + f, + "{} ({:?}): expected {}, actual {}", + difference.path, + difference.kind, + json_value_summary(difference.expected.as_ref()), + json_value_summary(difference.actual.as_ref()) + )?; + shown += 1; + } + let ungrouped_count = self + .differences + .iter() + .filter(|difference| { + !grouped_patterns.contains(normalize_repeated_path(&difference.path).as_str()) + }) + .count(); + if ungrouped_count > shown { + writeln!(f, "... {} more", ungrouped_count - shown)?; + } + Ok(()) + } +} + +struct RepeatedDifferenceGroup { + path_pattern: String, + kind: CrowdAnkiParityDifferenceKind, + expected: Option, + actual: Option, + count: usize, +} + +fn repeated_difference_groups( + differences: &[CrowdAnkiParityDifference], +) -> Vec { + let mut groups = BTreeMap::<(String, String, String, String), RepeatedDifferenceGroup>::new(); + for difference in differences { + let path_pattern = normalize_repeated_path(&difference.path); + if path_pattern == difference.path { + continue; + } + let key = ( + path_pattern.clone(), + format!("{:?}", difference.kind), + json_value_summary(difference.expected.as_ref()), + json_value_summary(difference.actual.as_ref()), + ); + groups + .entry(key) + .and_modify(|group| group.count += 1) + .or_insert_with(|| RepeatedDifferenceGroup { + path_pattern, + kind: difference.kind.clone(), + expected: difference.expected.clone(), + actual: difference.actual.clone(), + count: 1, + }); + } + + groups + .into_values() + .filter(|group| group.count > 1) + .collect() +} + +fn normalize_repeated_path(path: &str) -> String { + let mut normalized = String::new(); + let mut chars = path.chars().peekable(); + while let Some(ch) = chars.next() { + if ch != '[' { + normalized.push(ch); + continue; + } + + let mut bracket = String::from("["); + for bracket_ch in chars.by_ref() { + bracket.push(bracket_ch); + if bracket_ch == ']' { + break; + } + } + if bracket + .chars() + .skip(1) + .all(|ch| ch.is_ascii_digit() || ch == ']') + || bracket.contains('=') + { + normalized.push_str("[*]"); + } else { + normalized.push_str(&bracket); + } + } + normalized +} + +fn compare_json_value( + expected: &serde_json::Value, + actual: &serde_json::Value, + path: &str, + options: &CrowdAnkiParityOptions, + differences: &mut Vec, +) { + if expected == actual || is_allowed_parity_path(options, path) { + return; + } + + match (expected, actual) { + (serde_json::Value::Object(expected), serde_json::Value::Object(actual)) => { + let keys = expected + .keys() + .chain(actual.keys()) + .collect::>(); + for key in keys { + let child_path = json_path_key(path, key); + if is_allowed_parity_path(options, &child_path) { + continue; + } + match (expected.get(key), actual.get(key)) { + (Some(expected), Some(actual)) => { + compare_json_value(expected, actual, &child_path, options, differences); + } + (Some(expected), None) => differences.push(CrowdAnkiParityDifference { + path: child_path, + kind: CrowdAnkiParityDifferenceKind::MissingActual, + expected: Some(expected.clone()), + actual: None, + }), + (None, Some(actual)) => differences.push(CrowdAnkiParityDifference { + path: child_path, + kind: CrowdAnkiParityDifferenceKind::ExtraActual, + expected: None, + actual: Some(actual.clone()), + }), + (None, None) => {} + } + } + } + (serde_json::Value::Array(expected), serde_json::Value::Array(actual)) => { + if compare_json_array_by_identity(expected, actual, path, options, differences) { + return; + } + for index in 0..expected.len().min(actual.len()) { + let child_path = format!("{path}[{index}]"); + compare_json_value( + &expected[index], + &actual[index], + &child_path, + options, + differences, + ); + } + if expected.len() != actual.len() { + let length_path = format!("{path}.length"); + if !is_allowed_parity_path(options, &length_path) { + differences.push(CrowdAnkiParityDifference { + path: length_path, + kind: CrowdAnkiParityDifferenceKind::LengthMismatch, + expected: Some(serde_json::json!(expected.len())), + actual: Some(serde_json::json!(actual.len())), + }); + } + } + for (index, value) in expected.iter().enumerate().skip(actual.len()) { + let child_path = format!("{path}[{index}]"); + if !is_allowed_parity_path(options, &child_path) { + differences.push(CrowdAnkiParityDifference { + path: child_path, + kind: CrowdAnkiParityDifferenceKind::MissingActual, + expected: Some(value.clone()), + actual: None, + }); + } + } + for (index, value) in actual.iter().enumerate().skip(expected.len()) { + let child_path = format!("{path}[{index}]"); + if !is_allowed_parity_path(options, &child_path) { + differences.push(CrowdAnkiParityDifference { + path: child_path, + kind: CrowdAnkiParityDifferenceKind::ExtraActual, + expected: None, + actual: Some(value.clone()), + }); + } + } + } + _ => differences.push(CrowdAnkiParityDifference { + path: path.to_owned(), + kind: CrowdAnkiParityDifferenceKind::ValueMismatch, + expected: Some(expected.clone()), + actual: Some(actual.clone()), + }), + } +} + +fn compare_json_array_by_identity( + expected: &[serde_json::Value], + actual: &[serde_json::Value], + path: &str, + options: &CrowdAnkiParityOptions, + differences: &mut Vec, +) -> bool { + let Some(identity) = array_identity(path) else { + return false; + }; + + let Some(expected_by_key) = array_by_identity(expected, identity) else { + return false; + }; + let Some(actual_by_key) = array_by_identity(actual, identity) else { + return false; + }; + + let keys = expected_by_key + .keys() + .chain(actual_by_key.keys()) + .collect::>(); + for key in keys { + let child_path = format!("{path}[{}={}]", identity.name, json_path_label(key)); + if is_allowed_parity_path(options, &child_path) { + continue; + } + match (expected_by_key.get(key), actual_by_key.get(key)) { + (Some(expected), Some(actual)) => { + compare_json_value(expected, actual, &child_path, options, differences); + } + (Some(expected), None) => differences.push(CrowdAnkiParityDifference { + path: child_path, + kind: CrowdAnkiParityDifferenceKind::MissingActual, + expected: Some((*expected).clone()), + actual: None, + }), + (None, Some(actual)) => differences.push(CrowdAnkiParityDifference { + path: child_path, + kind: CrowdAnkiParityDifferenceKind::ExtraActual, + expected: None, + actual: Some((*actual).clone()), + }), + (None, None) => {} + } + } + + true +} + +#[derive(Clone, Copy)] +struct ArrayIdentity { + name: &'static str, + value: fn(&serde_json::Value) -> Option, +} + +fn array_identity(path: &str) -> Option { + match path { + "$.notes" => Some(ArrayIdentity { + name: "guid", + value: |value| value.get("guid")?.as_str().map(str::to_owned), + }), + "$.note_models" => Some(ArrayIdentity { + name: "model", + value: |value| { + value + .get("crowdanki_uuid") + .and_then(serde_json::Value::as_str) + .or_else(|| value.get("name").and_then(serde_json::Value::as_str)) + .map(str::to_owned) + }, + }), + path if path.ends_with(".flds") => Some(ArrayIdentity { + name: "name", + value: |value| value.get("name")?.as_str().map(str::to_owned), + }), + path if path.ends_with(".tmpls") => Some(ArrayIdentity { + name: "template", + value: |value| { + value + .get("ord") + .and_then(serde_json::Value::as_i64) + .map(|ord| ord.to_string()) + .or_else(|| { + value + .get("name") + .and_then(serde_json::Value::as_str) + .map(str::to_owned) + }) + }, + }), + _ => None, + } +} + +fn array_by_identity( + values: &[serde_json::Value], + identity: ArrayIdentity, +) -> Option> { + let mut by_key = BTreeMap::new(); + for value in values { + let key = (identity.value)(value)?; + if by_key.insert(key, value).is_some() { + return None; + } + } + Some(by_key) +} + +fn json_path_label(value: &str) -> String { + serde_json::to_string(value).expect("serializing a JSON path label cannot fail") +} + +fn is_allowed_parity_path(options: &CrowdAnkiParityOptions, path: &str) -> bool { + options + .allowed_path_globs + .iter() + .any(|pattern| glob_matches(pattern, path)) +} + +fn glob_matches(pattern: &str, value: &str) -> bool { + fn matches_parts(pattern: &[u8], value: &[u8]) -> bool { + match pattern.split_first() { + None => value.is_empty(), + Some((&b'*', rest)) => { + matches_parts(rest, value) + || (!value.is_empty() && matches_parts(pattern, &value[1..])) + } + Some((&expected, rest)) => value.split_first().is_some_and(|(&actual, rest_value)| { + actual == expected && matches_parts(rest, rest_value) + }), + } + } + + matches_parts(pattern.as_bytes(), value.as_bytes()) +} + +fn json_path_key(parent: &str, key: &str) -> String { + if key + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_') + { + format!("{parent}.{key}") + } else { + format!( + "{parent}[{}]", + serde_json::to_string(key).expect("serializing a JSON key cannot fail") + ) + } +} + +fn json_value_summary(value: Option<&serde_json::Value>) -> String { + let Some(value) = value else { + return "".to_owned(); + }; + let mut summary = serde_json::to_string(value).expect("serializing JSON value cannot fail"); + if summary.len() > 120 { + summary.truncate(117); + summary.push_str("..."); + } + summary +} + +fn export_note_model(note_type: &NoteType) -> Result { + Ok(CrowdAnkiNoteModelJson { + kind: "NoteModel".to_owned(), + crowdanki_uuid: crowdanki_note_model_uuid(note_type)?, + css: note_type.styling.clone(), + flds: note_type + .fields + .iter() + .enumerate() + .map(|(ord, field)| CrowdAnkiFieldJson { + font: "Arial".to_owned(), + media: Vec::new(), + name: field.name.clone(), + ord, + rtl: false, + size: 20, + sticky: false, + }) + .collect(), + latex_post: "\\end{document}".to_owned(), + latex_pre: default_latex_pre(), + latex_svg: false, + name: note_type.name.clone(), + req: Vec::new(), + sortf: 0, + tags: Vec::new(), + tmpls: note_type + .card_templates + .iter() + .enumerate() + .map(|(ord, template)| CrowdAnkiTemplateJson { + afmt: template.answer_format.clone(), + bafmt: String::new(), + bfont: Some(String::new()), + bqfmt: String::new(), + bsize: Some(0), + did: None, + name: template.name.clone(), + ord, + qfmt: template.question_format.clone(), + scratch_pad: Some(0), + }) + .collect(), + model_type: 0, + vers: Vec::new(), + }) +} + +fn export_note( + note: &Note, + deck: &CanonicalDeck, + note_type_uuids: &BTreeMap, +) -> Result { + let note_type = deck.note_types.get(¬e.note_type_id).ok_or_else(|| { + CrowdAnkiError::Unsupported(format!( + "note {} references missing note type {}", + note.id, note.note_type_id + )) + })?; + let note_model_uuid = note_type_uuids + .get(¬e.note_type_id) + .cloned() + .expect("note type uuid was precomputed"); + + let fields = note_type + .fields + .iter() + .map(|field| note.fields.get(&field.id).cloned().unwrap_or_default()) + .collect(); + + Ok(CrowdAnkiNoteJson { + type_: "Note".to_owned(), + data: String::new(), + fields, + flags: 0, + guid: crowdanki_note_guid(note), + note_model_uuid, + tags: note.tags.iter().cloned().collect(), + }) +} + +fn crowdanki_deck_uuid(deck: &CanonicalDeck) -> String { + deck.adapter_ids + .get("crowdanki:uuid") + .map(str::to_owned) + .unwrap_or_else(|| deck.id.to_string()) +} + +fn crowdanki_deck_config_uuid(deck: &CanonicalDeck) -> String { + deck.adapter_ids + .get("crowdanki:deck_config_uuid") + .map(str::to_owned) + .unwrap_or_else(|| format!("{}:deck-config", deck.id)) +} + +fn crowdanki_deck_config_name(deck: &CanonicalDeck) -> String { + deck.adapter_ids + .get("crowdanki:deck_config_name") + .map(str::to_owned) + .unwrap_or_else(|| deck.name.clone()) +} + +fn crowdanki_note_model_uuid(note_type: &NoteType) -> Result { + note_type + .adapter_ids + .get("crowdanki:uuid") + .map(str::to_owned) + .ok_or_else(|| { + CrowdAnkiError::Unsupported(format!( + "note type {} is missing crowdanki:uuid adapter id", + note_type.id + )) + }) +} + +fn crowdanki_note_guid(note: &Note) -> String { + note.adapter_ids + .get("crowdanki:guid") + .map(str::to_owned) + .unwrap_or_else(|| note.id.to_string()) +} + +fn default_latex_pre() -> String { + "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n" + .to_owned() +} + +fn default_deck_config_json(uuid: &str, name: &str) -> serde_json::Value { + serde_json::json!({ + "__type__": "DeckConfig", + "crowdanki_uuid": uuid, + "name": name, + "autoplay": false, + "dyn": false, + "lapse": { + "delays": [10], + "leechAction": 0, + "leechFails": 8, + "minInt": 1, + "mult": 0, + }, + "maxTaken": 60, + "new": { + "bury": true, + "delays": [1, 10], + "initialFactor": 2500, + "ints": [1, 4, 7], + "order": 0, + "perDay": 15, + "separate": true, + }, + "replayq": true, + "rev": { + "bury": true, + "ease4": 1.3, + "fuzz": 0.05, + "ivlFct": 1, + "maxIvl": 36500, + "minSpace": 1, + "perDay": 100, + }, + "timer": 0, + }) +} + +fn validate_supported_deck_configurations( + uuid: &str, + configurations: &[serde_json::Value], +) -> Result { + if configurations.len() != 1 { + return Err(CrowdAnkiError::Unsupported(format!( + "expected one default deck configuration, found {}", + configurations.len() + ))); + } + let Some(name) = configurations[0] + .get("name") + .and_then(serde_json::Value::as_str) + else { + return Err(CrowdAnkiError::Unsupported( + "deck configuration is missing a name".to_owned(), + )); + }; + let expected = default_deck_config_json(uuid, name); + if configurations[0] != expected { + return Err(CrowdAnkiError::Unsupported( + "non-default deck configurations are not modeled yet".to_owned(), + )); + } + Ok(name.to_owned()) +} + +#[derive(Debug)] +pub enum CrowdAnkiError { + Json(serde_json::Error), + StableId(String), + Unsupported(String), + Validation(ValidationReport), + VariableRender(VariableRenderReport), +} + +impl fmt::Display for CrowdAnkiError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Json(error) => write!(f, "CrowdAnki JSON error: {error}"), + Self::StableId(id) => write!(f, "generated invalid stable id {id:?}"), + Self::Unsupported(message) => write!(f, "unsupported CrowdAnki data: {message}"), + Self::Validation(report) => write!(f, "imported deck failed validation: {report}"), + Self::VariableRender(report) => write!(f, "deck variable rendering failed: {report}"), + } + } +} + +impl std::error::Error for CrowdAnkiError {} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct CrowdAnkiDeckJson { + #[serde(rename = "__type__")] + type_: String, + children: Vec, + crowdanki_uuid: String, + deck_config_uuid: String, + deck_configurations: Vec, + desc: String, + #[serde(rename = "dyn")] + dyn_: i64, + #[serde(rename = "extendNew")] + extend_new: i64, + #[serde(rename = "extendRev")] + extend_rev: i64, + media_files: Vec, + name: String, + note_models: Vec, + notes: Vec, +} + +impl CrowdAnkiDeckJson { + fn into_deck(self) -> Result { + if self.type_ != "Deck" { + return Err(CrowdAnkiError::Unsupported(format!( + "expected __type__ Deck, found {}", + self.type_ + ))); + } + if !self.children.is_empty() { + return Err(CrowdAnkiError::Unsupported( + "child decks are not modeled yet".to_owned(), + )); + } + if self.dyn_ != 0 || self.extend_new != 10 || self.extend_rev != 50 { + return Err(CrowdAnkiError::Unsupported(format!( + "non-default deck scheduling header is not modeled yet (dyn={}, extendNew={}, extendRev={})", + self.dyn_, self.extend_new, self.extend_rev + ))); + } + let deck_config_name = validate_supported_deck_configurations( + &self.deck_config_uuid, + &self.deck_configurations, + )?; + + let deck_id = prefixed_stable_id("deck", &self.name)?; + let mut deck_adapter_ids = AdapterIds::new(); + deck_adapter_ids.insert("crowdanki:uuid", self.crowdanki_uuid); + deck_adapter_ids.insert("crowdanki:deck_config_uuid", self.deck_config_uuid); + deck_adapter_ids.insert("crowdanki:deck_config_name", deck_config_name); + + let mut note_type_by_uuid = BTreeMap::new(); + let mut note_types = BTreeMap::new(); + for note_model in self.note_models { + let (uuid, id, note_type) = note_model.into_note_type()?; + note_type_by_uuid.insert(uuid, id.clone()); + note_types.insert(id, note_type); + } + + let notes = self + .notes + .into_iter() + .map(|note| note.into_note(¬e_types, ¬e_type_by_uuid)) + .collect::, _>>()?; + + let media = self + .media_files + .into_iter() + .map(|path| { + let id = prefixed_stable_id("media", &path)?; + Ok(( + id.clone(), + MediaReference { + id, + path, + sha256: String::new(), + }, + )) + }) + .collect::, CrowdAnkiError>>()?; + + let deck = CanonicalDeck { + id: deck_id, + name: self.name, + description: self.desc, + note_types, + notes, + media, + tombstones: BTreeSet::new(), + variables: BTreeMap::new(), + adapter_ids: deck_adapter_ids, + }; + deck.validate().map_err(CrowdAnkiError::Validation)?; + Ok(deck) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct CrowdAnkiNoteModelJson { + #[serde(rename = "__type__")] + kind: String, + crowdanki_uuid: String, + css: String, + flds: Vec, + #[serde(rename = "latexPost")] + latex_post: String, + #[serde(rename = "latexPre")] + latex_pre: String, + #[serde(rename = "latexsvg")] + latex_svg: bool, + name: String, + req: Vec, + sortf: usize, + tags: Vec, + tmpls: Vec, + #[serde(rename = "type")] + model_type: i64, + vers: Vec, +} + +impl CrowdAnkiNoteModelJson { + fn validate_supported_defaults(&self) -> Result<(), CrowdAnkiError> { + if self.latex_post != "\\end{document}" + || self.latex_pre != default_latex_pre() + || self.latex_svg + || !self.req.is_empty() + || self.sortf != 0 + || !self.tags.is_empty() + || !self.vers.is_empty() + { + return Err(CrowdAnkiError::Unsupported(format!( + "note model {} has non-default CrowdAnki options that are not modeled yet", + self.name + ))); + } + Ok(()) + } + + fn into_note_type(self) -> Result<(String, StableId, NoteType), CrowdAnkiError> { + if self.kind != "NoteModel" { + return Err(CrowdAnkiError::Unsupported(format!( + "expected note model __type__ NoteModel, found {}", + self.kind + ))); + } + if self.model_type != 0 { + return Err(CrowdAnkiError::Unsupported(format!( + "only standard note models are supported, found type {}", + self.model_type + ))); + } + self.validate_supported_defaults()?; + + let id = prefixed_stable_id("note-type", &self.name)?; + let mut adapter_ids = AdapterIds::new(); + adapter_ids.insert("crowdanki:uuid", self.crowdanki_uuid.clone()); + + let fields = self + .flds + .into_iter() + .enumerate() + .map(|(index, field)| { + field.validate_supported_defaults(index)?; + Ok(FieldDefinition { + id: prefixed_stable_id("field", &field.name)?, + name: field.name, + }) + }) + .collect::, CrowdAnkiError>>()?; + + let mut templates = self.tmpls; + templates.sort_by_key(|template| template.ord); + let card_templates = templates + .into_iter() + .map(|template| { + template.validate_supported_defaults()?; + Ok(CardTemplate { + id: prefixed_stable_id("template", &template.name)?, + name: template.name, + variables: BTreeMap::new(), + question_format: template.qfmt, + answer_format: template.afmt, + adapter_ids: AdapterIds::new(), + }) + }) + .collect::, CrowdAnkiError>>()?; + + let note_type = NoteType { + id: id.clone(), + name: self.name, + variables: BTreeMap::new(), + fields, + card_templates, + styling: self.css, + adapter_ids, + }; + + Ok((self.crowdanki_uuid, id, note_type)) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct CrowdAnkiFieldJson { + font: String, + media: Vec, + name: String, + ord: usize, + rtl: bool, + size: usize, + sticky: bool, +} + +impl CrowdAnkiFieldJson { + fn validate_supported_defaults(&self, expected_ord: usize) -> Result<(), CrowdAnkiError> { + if self.font != "Arial" + || !self.media.is_empty() + || self.ord != expected_ord + || self.rtl + || self.size != 20 + || self.sticky + { + return Err(CrowdAnkiError::Unsupported(format!( + "field {} has non-default CrowdAnki options that are not modeled yet", + self.name + ))); + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct CrowdAnkiTemplateJson { + afmt: String, + bafmt: String, + bfont: Option, + bqfmt: String, + bsize: Option, + did: Option, + name: String, + ord: usize, + qfmt: String, + #[serde(rename = "scratchPad")] + scratch_pad: Option, +} + +impl CrowdAnkiTemplateJson { + fn validate_supported_defaults(&self) -> Result<(), CrowdAnkiError> { + if self.bfont.as_deref().unwrap_or_default() != "" + || self.bsize.unwrap_or_default() != 0 + || self.scratch_pad.unwrap_or_default() != 0 + { + return Err(CrowdAnkiError::Unsupported(format!( + "card template {} has non-default browser options that are not modeled yet", + self.name + ))); + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct CrowdAnkiNoteJson { + #[serde(rename = "__type__")] + type_: String, + data: String, + fields: Vec, + flags: i64, + guid: String, + note_model_uuid: String, + tags: Vec, +} + +impl CrowdAnkiNoteJson { + fn into_note( + self, + note_types: &BTreeMap, + note_type_by_uuid: &BTreeMap, + ) -> Result<(StableId, Note), CrowdAnkiError> { + if self.type_ != "Note" { + return Err(CrowdAnkiError::Unsupported(format!( + "expected note __type__ Note, found {}", + self.type_ + ))); + } + if !self.data.is_empty() || self.flags != 0 { + return Err(CrowdAnkiError::Unsupported(format!( + "note {} has non-default data/flags that are not modeled yet", + self.guid + ))); + } + let note_type_id = note_type_by_uuid + .get(&self.note_model_uuid) + .ok_or_else(|| { + CrowdAnkiError::Unsupported(format!( + "note references missing note_model_uuid {}", + self.note_model_uuid + )) + })? + .clone(); + let note_type = note_types + .get(¬e_type_id) + .expect("note type id came from note type map"); + if self.fields.len() != note_type.fields.len() { + return Err(CrowdAnkiError::Unsupported(format!( + "note {} has {} fields but note type {} has {} fields", + self.guid, + self.fields.len(), + note_type.id, + note_type.fields.len() + ))); + } + + let first_field = self + .fields + .first() + .map(String::as_str) + .unwrap_or(&self.guid); + let id = prefixed_stable_id("note", first_field)?; + let fields = note_type + .fields + .iter() + .zip(self.fields) + .map(|(field, value)| (field.id.clone(), value)) + .collect(); + let mut adapter_ids = AdapterIds::new(); + adapter_ids.insert("crowdanki:guid", self.guid); + + Ok(( + id.clone(), + Note { + id, + note_type_id, + variables: BTreeMap::new(), + fields, + tags: self.tags.into_iter().collect(), + adapter_ids, + }, + )) + } +} + +fn prefixed_stable_id(prefix: &str, source: &str) -> Result { + let slug = slugify(source); + let id = format!("{prefix}.{slug}"); + StableId::new(&id).map_err(|_| CrowdAnkiError::StableId(id)) +} + +fn slugify(source: &str) -> String { + let mut slug = String::new(); + let mut last_was_separator = false; + for ch in source.chars().flat_map(char::to_lowercase) { + if ch.is_ascii_alphanumeric() { + slug.push(ch); + last_was_separator = false; + } else if !last_was_separator && !slug.is_empty() { + slug.push('-'); + last_was_separator = true; + } + } + while slug.ends_with('-') { + slug.pop(); + } + if slug.is_empty() { + "unnamed".to_owned() + } else { + slug + } +} diff --git a/crates/brain-brew-formats/src/lib.rs b/crates/brain-brew-formats/src/lib.rs new file mode 100644 index 0000000..0a3fe38 --- /dev/null +++ b/crates/brain-brew-formats/src/lib.rs @@ -0,0 +1,31 @@ +//! Reusable format codecs for Brain Brew. +//! +//! This crate contains strict CanonicalDeck YAML support, CrowdAnki codecs, and +//! media helpers. It depends on `brain-brew-core`, but does not own domain +//! behavior. + +pub mod canonical_yaml; +pub mod crowdanki; +pub mod lockfile; +pub mod manifest; +pub mod media; + +pub use brain_brew_core as core; + +/// Name of the formats crate. +pub const CRATE_NAME: &str = env!("CARGO_PKG_NAME"); + +#[cfg(test)] +mod tests { + use super::{CRATE_NAME, core}; + + #[test] + fn exposes_formats_crate_name() { + assert_eq!(CRATE_NAME, "brain-brew-formats"); + } + + #[test] + fn can_reach_core_crate() { + assert_eq!(core::CRATE_NAME, "brain-brew-core"); + } +} diff --git a/crates/brain-brew-formats/src/lockfile.rs b/crates/brain-brew-formats/src/lockfile.rs new file mode 100644 index 0000000..929401e --- /dev/null +++ b/crates/brain-brew-formats/src/lockfile.rs @@ -0,0 +1,238 @@ +use std::collections::BTreeMap; +use std::fmt; + +use serde::Deserialize; + +/// Reproducible source lock for a set of Federated Deck package inputs. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FederationLock { + pub version: u32, + pub packages: BTreeMap, +} + +/// One locked package input. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LockedPackage { + pub manifest: String, + pub package: LockedPackageMetadata, + pub original: Option, + pub locked: LockedSource, +} + +/// Package metadata captured at lock time. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LockedPackageMetadata { + pub version: String, +} + +/// Original or locked source reference for one package input. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LockedSource { + pub source_type: String, + pub url: Option, + pub path: Option, + pub reference: Option, + pub rev: Option, + pub nar_hash: Option, +} + +/// Parse a federation lock file from strict YAML. +pub fn from_str(input: &str) -> Result { + let yaml: FederationLockYaml = serde_yaml::from_str(input).map_err(LockfileError::Parse)?; + yaml.into_lock() +} + +/// Parse and re-emit a federation lock file using deterministic formatting. +pub fn format_str(input: &str) -> Result { + Ok(to_string(&from_str(input)?)) +} + +/// Emit a federation lock file as deterministic YAML. +pub fn to_string(lock: &FederationLock) -> String { + let mut out = String::new(); + out.push_str(&format!("version: {}\n", lock.version)); + if lock.packages.is_empty() { + out.push_str("packages: {}\n"); + return out; + } + out.push_str("packages:\n"); + for (id, package) in &lock.packages { + out.push_str(&format!(" {id}:\n")); + out.push_str(&format!( + " manifest: {}\n", + yaml_scalar(&package.manifest) + )); + out.push_str(" package:\n"); + out.push_str(&format!( + " version: {}\n", + yaml_scalar(&package.package.version) + )); + if let Some(original) = &package.original { + out.push_str(" original:\n"); + write_source(&mut out, " ", original); + } + out.push_str(" locked:\n"); + write_source(&mut out, " ", &package.locked); + } + out +} + +fn write_source(out: &mut String, indent: &str, source: &LockedSource) { + out.push_str(&format!( + "{indent}type: {}\n", + yaml_scalar(&source.source_type) + )); + if let Some(url) = &source.url { + out.push_str(&format!("{indent}url: {}\n", yaml_scalar(url))); + } + if let Some(path) = &source.path { + out.push_str(&format!("{indent}path: {}\n", yaml_scalar(path))); + } + if let Some(reference) = &source.reference { + out.push_str(&format!("{indent}ref: {}\n", yaml_scalar(reference))); + } + if let Some(rev) = &source.rev { + out.push_str(&format!("{indent}rev: {}\n", yaml_scalar(rev))); + } + if let Some(nar_hash) = &source.nar_hash { + out.push_str(&format!("{indent}nar_hash: {}\n", yaml_scalar(nar_hash))); + } +} + +fn yaml_scalar(value: &str) -> String { + if !value.is_empty() + && !value.starts_with([ + ' ', '-', '?', ':', '@', '`', '&', '*', '!', '|', '>', '#', '{', '[', ',', + ]) + && !value.ends_with(' ') + && value.chars().all(|ch| { + ch.is_ascii_alphanumeric() || matches!(ch, ' ' | '.' | ',' | '_' | '-' | '/' | ':') + }) + && !value.chars().all(|ch| ch.is_ascii_digit()) + && !matches!( + value, + "true" | "false" | "True" | "False" | "TRUE" | "FALSE" | "null" | "Null" | "NULL" + ) + { + value.to_owned() + } else { + format!("'{}'", value.replace('\'', "''")) + } +} + +#[derive(Debug)] +pub enum LockfileError { + Parse(serde_yaml::Error), + UnsupportedVersion(u32), + MissingLockedSource(String), +} + +impl fmt::Display for LockfileError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Parse(error) => write!(f, "failed to parse lock YAML: {error}"), + Self::UnsupportedVersion(version) => { + write!(f, "unsupported federation lock version {version}") + } + Self::MissingLockedSource(package) => { + write!(f, "locked package {package} must include a locked source") + } + } + } +} + +impl std::error::Error for LockfileError {} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct FederationLockYaml { + version: u32, + #[serde(default)] + packages: BTreeMap, +} + +impl FederationLockYaml { + fn into_lock(self) -> Result { + if self.version != 1 { + return Err(LockfileError::UnsupportedVersion(self.version)); + } + let packages = self + .packages + .into_iter() + .map(|(id, package)| Ok((id.clone(), package.into_locked_package(id)?))) + .collect::>()?; + Ok(FederationLock { + version: self.version, + packages, + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct LockedPackageYaml { + manifest: String, + package: LockedPackageMetadataYaml, + #[serde(default)] + original: Option, + #[serde(default)] + locked: Option, +} + +impl LockedPackageYaml { + fn into_locked_package(self, id: String) -> Result { + let Some(locked) = self.locked else { + return Err(LockfileError::MissingLockedSource(id)); + }; + Ok(LockedPackage { + manifest: self.manifest, + package: self.package.into_metadata(), + original: self.original.map(LockedSourceYaml::into_source), + locked: locked.into_source(), + }) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct LockedPackageMetadataYaml { + version: String, +} + +impl LockedPackageMetadataYaml { + fn into_metadata(self) -> LockedPackageMetadata { + LockedPackageMetadata { + version: self.version, + } + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct LockedSourceYaml { + #[serde(rename = "type")] + source_type: String, + #[serde(default)] + url: Option, + #[serde(default)] + path: Option, + #[serde(default, rename = "ref")] + reference: Option, + #[serde(default)] + rev: Option, + #[serde(default)] + nar_hash: Option, +} + +impl LockedSourceYaml { + fn into_source(self) -> LockedSource { + LockedSource { + source_type: self.source_type, + url: self.url, + path: self.path, + reference: self.reference, + rev: self.rev, + nar_hash: self.nar_hash, + } + } +} diff --git a/crates/brain-brew-formats/src/manifest.rs b/crates/brain-brew-formats/src/manifest.rs new file mode 100644 index 0000000..3aa8af4 --- /dev/null +++ b/crates/brain-brew-formats/src/manifest.rs @@ -0,0 +1,405 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +use serde::Deserialize; + +/// Public manifest for a Federated Deck workspace. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct FederatedDeckManifest { + pub package: Option, + pub base: String, + pub overlays: BTreeMap, + pub targets: BTreeMap, +} + +impl FederatedDeckManifest { + /// Expand one target into the deterministic overlay stack implied by its dependencies. + pub fn expand_target(&self, target: &str) -> Result { + let target_entry = self + .targets + .get(target) + .ok_or_else(|| ManifestError::MissingTarget(target.to_owned()))?; + let mut visited = BTreeSet::new(); + let mut stack = Vec::new(); + let mut overlays = Vec::new(); + + for overlay in &target_entry.overlays { + self.visit_overlay(overlay, &mut visited, &mut stack, &mut overlays)?; + } + + Ok(ExpandedTarget { + name: target.to_owned(), + base: self.base.clone(), + extends: target_entry.extends.clone(), + overlays, + }) + } + + fn visit_overlay( + &self, + overlay: &str, + visited: &mut BTreeSet, + stack: &mut Vec, + expanded: &mut Vec, + ) -> Result<(), ManifestError> { + if visited.contains(overlay) { + return Ok(()); + } + if stack.iter().any(|candidate| candidate == overlay) { + let mut cycle = stack.clone(); + cycle.push(overlay.to_owned()); + return Err(ManifestError::DependencyCycle(cycle)); + } + + let entry = self + .overlays + .get(overlay) + .ok_or_else(|| ManifestError::MissingOverlay(overlay.to_owned()))?; + stack.push(overlay.to_owned()); + for dependency in &entry.depends_on { + self.visit_overlay(dependency, visited, stack, expanded)?; + } + stack.pop(); + + visited.insert(overlay.to_owned()); + expanded.push(ExpandedOverlay { + id: overlay.to_owned(), + file: entry.file.clone(), + }); + Ok(()) + } +} + +/// Package identity and dependency metadata for a Federated Deck workspace. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PackageMetadata { + pub id: String, + pub version: String, + pub compatible_base_versions: Vec, + pub depends_on: Vec, +} + +/// One overlay available in a Federated Deck manifest. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OverlayManifestEntry { + pub file: String, + pub kind: Option, + pub depends_on: Vec, +} + +/// One named composition goal in a Federated Deck manifest. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BuildTarget { + pub extends: Option, + pub overlays: Vec, + pub exports: TargetExports, +} + +/// Optional reproducibility checks and default outputs for one target. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TargetExports { + pub crowdanki: Option, +} + +impl TargetExports { + pub fn is_empty(&self) -> bool { + self.crowdanki.is_none() + } +} + +/// CrowdAnki export defaults and golden-file verification for one target. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CrowdAnkiTargetExport { + pub out: Option, + pub golden: Option, + pub golden_allowlist: Vec, +} + +/// Expanded target ready for filesystem loading and composition. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExpandedTarget { + pub name: String, + pub base: String, + pub extends: Option, + pub overlays: Vec, +} + +/// One overlay in an expanded deterministic overlay stack. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExpandedOverlay { + pub id: String, + pub file: String, +} + +/// Parse a Federated Deck manifest from strict YAML. +pub fn from_str(input: &str) -> Result { + let yaml: ManifestYaml = serde_yaml::from_str(input).map_err(ManifestError::Parse)?; + Ok(yaml.into_manifest()) +} + +/// Parse and re-emit a Federated Deck manifest using deterministic formatting. +pub fn format_str(input: &str) -> Result { + let manifest = from_str(input)?; + Ok(to_string(&manifest)) +} + +/// Emit a Federated Deck manifest as deterministic YAML. +pub fn to_string(manifest: &FederatedDeckManifest) -> String { + let mut out = String::new(); + if let Some(package) = &manifest.package { + out.push_str("package:\n"); + out.push_str(&format!(" id: {}\n", yaml_scalar(&package.id))); + out.push_str(&format!(" version: {}\n", yaml_scalar(&package.version))); + if !package.compatible_base_versions.is_empty() { + out.push_str(" compatible_base_versions:\n"); + for version in &package.compatible_base_versions { + out.push_str(&format!(" - {}\n", yaml_scalar(version))); + } + } + if !package.depends_on.is_empty() { + out.push_str(" depends_on:\n"); + for dependency in &package.depends_on { + out.push_str(&format!(" - {}\n", yaml_scalar(dependency))); + } + } + } + out.push_str(&format!("base: {}\n", yaml_scalar(&manifest.base))); + + if manifest.overlays.is_empty() { + out.push_str("overlays: {}\n"); + } else { + out.push_str("overlays:\n"); + for (id, overlay) in &manifest.overlays { + out.push_str(&format!(" {id}:\n")); + out.push_str(&format!(" file: {}\n", yaml_scalar(&overlay.file))); + if let Some(kind) = &overlay.kind { + out.push_str(&format!(" kind: {}\n", yaml_scalar(kind))); + } + if !overlay.depends_on.is_empty() { + out.push_str(" depends_on:\n"); + for dependency in &overlay.depends_on { + out.push_str(&format!(" - {}\n", yaml_scalar(dependency))); + } + } + } + } + + if manifest.targets.is_empty() { + out.push_str("targets: {}\n"); + } else { + out.push_str("targets:\n"); + for (id, target) in &manifest.targets { + out.push_str(&format!(" {id}:\n")); + if let Some(extends) = &target.extends { + out.push_str(&format!(" extends: {}\n", yaml_scalar(extends))); + } + if target.overlays.is_empty() { + out.push_str(" overlays: []\n"); + } else { + out.push_str(" overlays:\n"); + for overlay in &target.overlays { + out.push_str(&format!(" - {}\n", yaml_scalar(overlay))); + } + } + if !target.exports.is_empty() { + out.push_str(" exports:\n"); + if let Some(export) = &target.exports.crowdanki { + out.push_str(" crowdanki:\n"); + if let Some(path) = &export.out { + out.push_str(&format!(" out: {}\n", yaml_scalar(path))); + } + if let Some(path) = &export.golden { + out.push_str(&format!(" golden: {}\n", yaml_scalar(path))); + } + if !export.golden_allowlist.is_empty() { + out.push_str(" golden_allowlist:\n"); + for path in &export.golden_allowlist { + out.push_str(&format!(" - {}\n", yaml_scalar(path))); + } + } + } + } + } + } + out +} + +fn yaml_scalar(value: &str) -> String { + if !value.is_empty() + && !value.starts_with([ + ' ', '-', '?', ':', '@', '`', '&', '*', '!', '|', '>', '#', '{', '[', ',', + ]) + && !value.ends_with(' ') + && value.chars().all(|ch| { + ch.is_ascii_alphanumeric() || matches!(ch, ' ' | '.' | ',' | '_' | '-' | '/' | ':') + }) + && !value.chars().all(|ch| ch.is_ascii_digit()) + && !matches!( + value, + "true" | "false" | "True" | "False" | "TRUE" | "FALSE" | "null" | "Null" | "NULL" + ) + { + value.to_owned() + } else { + format!("'{}'", value.replace('\'', "''")) + } +} + +#[derive(Debug)] +pub enum ManifestError { + Parse(serde_yaml::Error), + MissingTarget(String), + MissingOverlay(String), + DependencyCycle(Vec), +} + +impl fmt::Display for ManifestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Parse(error) => write!(f, "failed to parse manifest YAML: {error}"), + Self::MissingTarget(target) => write!(f, "manifest target {target:?} does not exist"), + Self::MissingOverlay(overlay) => { + write!(f, "manifest overlay {overlay:?} does not exist") + } + Self::DependencyCycle(cycle) => { + write!( + f, + "manifest overlay dependency cycle: {}", + cycle.join(" -> ") + ) + } + } + } +} + +impl std::error::Error for ManifestError {} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ManifestYaml { + #[serde(default)] + package: Option, + base: String, + #[serde(default)] + overlays: BTreeMap, + #[serde(default)] + targets: BTreeMap, +} + +impl ManifestYaml { + fn into_manifest(self) -> FederatedDeckManifest { + FederatedDeckManifest { + package: self.package.map(PackageMetadataYaml::into_metadata), + base: self.base, + overlays: self + .overlays + .into_iter() + .map(|(id, overlay)| (id, overlay.into_entry())) + .collect(), + targets: self + .targets + .into_iter() + .map(|(id, target)| (id, target.into_target())) + .collect(), + } + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct PackageMetadataYaml { + id: String, + version: String, + #[serde(default)] + compatible_base_versions: Vec, + #[serde(default)] + depends_on: Vec, +} + +impl PackageMetadataYaml { + fn into_metadata(self) -> PackageMetadata { + PackageMetadata { + id: self.id, + version: self.version, + compatible_base_versions: self.compatible_base_versions, + depends_on: self.depends_on, + } + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct OverlayManifestEntryYaml { + file: String, + #[serde(default)] + kind: Option, + #[serde(default)] + depends_on: Vec, +} + +impl OverlayManifestEntryYaml { + fn into_entry(self) -> OverlayManifestEntry { + OverlayManifestEntry { + file: self.file, + kind: self.kind, + depends_on: self.depends_on, + } + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct BuildTargetYaml { + #[serde(default)] + extends: Option, + #[serde(default)] + overlays: Vec, + #[serde(default)] + exports: TargetExportsYaml, +} + +impl BuildTargetYaml { + fn into_target(self) -> BuildTarget { + BuildTarget { + extends: self.extends, + overlays: self.overlays, + exports: self.exports.into_exports(), + } + } +} + +#[derive(Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct TargetExportsYaml { + #[serde(default)] + crowdanki: Option, +} + +impl TargetExportsYaml { + fn into_exports(self) -> TargetExports { + TargetExports { + crowdanki: self.crowdanki.map(CrowdAnkiTargetExportYaml::into_export), + } + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct CrowdAnkiTargetExportYaml { + #[serde(default)] + out: Option, + #[serde(default)] + golden: Option, + #[serde(default)] + golden_allowlist: Vec, +} + +impl CrowdAnkiTargetExportYaml { + fn into_export(self) -> CrowdAnkiTargetExport { + CrowdAnkiTargetExport { + out: self.out, + golden: self.golden, + golden_allowlist: self.golden_allowlist, + } + } +} diff --git a/crates/brain-brew-formats/src/media.rs b/crates/brain-brew-formats/src/media.rs new file mode 100644 index 0000000..c9d5111 --- /dev/null +++ b/crates/brain-brew-formats/src/media.rs @@ -0,0 +1,235 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +use brain_brew_core::CanonicalDeck; +use sha2::{Digest, Sha256}; + +/// Extract Anki-compatible media paths used by note fields and card templates. +pub fn referenced_paths(deck: &CanonicalDeck) -> BTreeSet { + let mut paths = BTreeSet::new(); + + for note in deck.notes.values() { + for value in note.fields.values() { + extract_from_text(value, &mut paths); + } + } + + for note_type in deck.note_types.values() { + extract_from_text(¬e_type.styling, &mut paths); + for template in ¬e_type.card_templates { + extract_from_text(&template.question_format, &mut paths); + extract_from_text(&template.answer_format, &mut paths); + } + } + + paths +} + +/// Compute a lowercase SHA-256 hex digest. +pub fn sha256_hex(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + digest.iter().map(|byte| format!("{byte:02x}")).collect() +} + +/// Validate content hashes using caller-supplied media asset bytes. +pub fn validate_hashes( + deck: &CanonicalDeck, + assets: &BTreeMap>, +) -> Result<(), MediaValidationReport> { + let mut errors = Vec::new(); + + for media in deck.media.values() { + if media.sha256.is_empty() { + continue; + } + let Some(bytes) = assets.get(&media.path) else { + errors.push(MediaValidationError { + kind: MediaValidationErrorKind::MissingAsset, + path: media.path.clone(), + message: format!( + "media asset {} was not supplied for hash validation", + media.path + ), + }); + continue; + }; + let actual = sha256_hex(bytes); + if actual != media.sha256 { + errors.push(MediaValidationError { + kind: MediaValidationErrorKind::HashMismatch, + path: media.path.clone(), + message: format!( + "media asset {} has sha256 {}, expected {}", + media.path, actual, media.sha256 + ), + }); + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(MediaValidationReport { errors }) + } +} + +/// Validate that every used media path is declared and every declaration is used. +pub fn validate_references(deck: &CanonicalDeck) -> Result<(), MediaValidationReport> { + let used = referenced_paths(deck); + let declared = deck + .media + .values() + .map(|media| media.path.clone()) + .collect::>(); + let mut errors = Vec::new(); + + for path in used.difference(&declared) { + errors.push(MediaValidationError { + kind: MediaValidationErrorKind::MissingReference, + path: path.clone(), + message: format!("media path {path} is used but not declared"), + }); + } + + for path in declared.difference(&used) { + errors.push(MediaValidationError { + kind: MediaValidationErrorKind::UnusedReference, + path: path.clone(), + message: format!("media path {path} is declared but not used"), + }); + } + + if errors.is_empty() { + Ok(()) + } else { + Err(MediaValidationReport { errors }) + } +} + +fn extract_from_text(text: &str, paths: &mut BTreeSet) { + extract_sound_references(text, paths); + extract_attribute_references(text, "src=", paths); + extract_attribute_references(text, "href=", paths); + extract_css_url_references(text, paths); +} + +fn extract_sound_references(text: &str, paths: &mut BTreeSet) { + let mut rest = text; + while let Some(start) = rest.find("[sound:") { + rest = &rest[start + "[sound:".len()..]; + let Some(end) = rest.find(']') else { + break; + }; + let path = rest[..end].trim(); + if !path.is_empty() { + paths.insert(path.to_owned()); + } + rest = &rest[end + 1..]; + } +} + +fn extract_attribute_references(text: &str, attribute: &str, paths: &mut BTreeSet) { + let mut rest = text; + while let Some(start) = rest.find(attribute) { + rest = &rest[start + attribute.len()..]; + let Some((path, consumed)) = consume_media_path(rest, |ch| ch.is_whitespace() || ch == '>') + else { + break; + }; + insert_media_path(path, paths); + rest = &rest[consumed..]; + } +} + +fn extract_css_url_references(text: &str, paths: &mut BTreeSet) { + let mut rest = text; + while let Some(start) = rest.find("url(") { + rest = &rest[start + "url(".len()..]; + let Some((path, consumed)) = consume_media_path(rest, |ch| ch == ')') else { + break; + }; + insert_media_path(path, paths); + rest = &rest[consumed..]; + } +} + +fn consume_media_path(rest: &str, unquoted_end: impl Fn(char) -> bool) -> Option<(&str, usize)> { + let trimmed = rest.trim_start(); + let consumed_whitespace = rest.len() - trimmed.len(); + let first = trimmed.chars().next()?; + let (path, consumed) = if first == '"' || first == '\'' { + let quote_len = first.len_utf8(); + let after_quote = &trimmed[quote_len..]; + let end = after_quote.find(first)?; + (&after_quote[..end], quote_len + end + quote_len) + } else { + let end = trimmed.find(unquoted_end).unwrap_or(trimmed.len()); + (&trimmed[..end], end) + }; + Some((path.trim(), consumed_whitespace + consumed)) +} + +fn insert_media_path(path: &str, paths: &mut BTreeSet) { + if !path.is_empty() && !path.starts_with("#") && !has_uri_scheme(path) { + paths.insert(path.to_owned()); + } +} + +fn has_uri_scheme(path: &str) -> bool { + let Some(colon) = path.find(':') else { + return false; + }; + let scheme = &path[..colon]; + !scheme.is_empty() + && scheme + .chars() + .next() + .is_some_and(|ch| ch.is_ascii_alphabetic()) + && scheme + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '+' | '-' | '.')) +} + +/// A media reference validation report. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MediaValidationReport { + pub errors: Vec, +} + +impl MediaValidationReport { + /// Returns true when the report contains at least one error of the given kind. + pub fn has_kind(&self, kind: MediaValidationErrorKind) -> bool { + self.errors.iter().any(|error| error.kind == kind) + } +} + +impl fmt::Display for MediaValidationReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (index, error) in self.errors.iter().enumerate() { + if index > 0 { + writeln!(f)?; + } + write!(f, "{}: {}", error.path, error.message)?; + } + Ok(()) + } +} + +impl std::error::Error for MediaValidationReport {} + +/// One media validation error. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MediaValidationError { + pub kind: MediaValidationErrorKind, + pub path: String, + pub message: String, +} + +/// Machine-readable media validation error kind. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MediaValidationErrorKind { + MissingReference, + UnusedReference, + MissingAsset, + HashMismatch, +} diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..31869a7 --- /dev/null +++ b/devbox.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.2/.schema/devbox.schema.json", + "packages": [ + "rustc@latest", + "cargo@latest", + "rustfmt@latest", + "clippy@latest", + "nodejs@20" + ], + "shell": { + "init_hook": [ + "echo 'Brain Brew dev environment ready' > /dev/null" + ], + "scripts": { + "fmt": [ + "cargo fmt --all" + ], + "fmt:check": [ + "cargo fmt --all -- --check" + ], + "check": [ + "cargo check --workspace --all-targets" + ], + "test": [ + "cargo test --workspace --all-targets" + ], + "clippy": [ + "cargo clippy --workspace --all-targets -- -D warnings" + ], + "docs:install": [ + "npm --prefix documentation install" + ], + "docs:start": [ + "npm --prefix documentation run start" + ], + "docs:build": [ + "npm --prefix documentation run build" + ], + "ci": [ + "cargo fmt --all -- --check", + "cargo test --workspace --all-targets", + "cargo clippy --workspace --all-targets -- -D warnings" + ] + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..81378ea --- /dev/null +++ b/devbox.lock @@ -0,0 +1,295 @@ +{ + "lockfile_version": "1", + "packages": { + "cargo@latest": { + "last_modified": "2026-04-23T13:07:47Z", + "resolved": "github:NixOS/nixpkgs/01fbdeef22b76df85ea168fbfe1bfd9e63681b30#cargo", + "source": "devbox-search", + "version": "1.94.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/mlrgd5zljyrxl15ryn31w15w48f6yii5-cargo-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/mlrgd5zljyrxl15ryn31w15w48f6yii5-cargo-1.94.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/fvmmpmsik6551c334wgvx8smifj2fibf-cargo-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/fvmmpmsik6551c334wgvx8smifj2fibf-cargo-1.94.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/10bhh3gsrw2xfg3fr53pip6rdzcz2sfp-cargo-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/10bhh3gsrw2xfg3fr53pip6rdzcz2sfp-cargo-1.94.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/z382dzkk7snk51ka6n4f3b953dcdm8fc-cargo-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/z382dzkk7snk51ka6n4f3b953dcdm8fc-cargo-1.94.1" + } + } + }, + "clippy@latest": { + "last_modified": "2026-04-23T13:07:47Z", + "resolved": "github:NixOS/nixpkgs/01fbdeef22b76df85ea168fbfe1bfd9e63681b30#clippy", + "source": "devbox-search", + "version": "1.94.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/z9jrilf49s8prkiicxlfapbi6hj9dz03-clippy-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/z9jrilf49s8prkiicxlfapbi6hj9dz03-clippy-1.94.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/0ag0s1wxh9l5fldh6j0318qpmzy7sn5r-clippy-1.94.1", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/svpaxsn34mgdzhgyrgdvw68fl14g1s8x-clippy-1.94.1-debug" + } + ], + "store_path": "/nix/store/0ag0s1wxh9l5fldh6j0318qpmzy7sn5r-clippy-1.94.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/874dlfzia6nhc8f9iwqnh8cg79ga3rw4-clippy-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/874dlfzia6nhc8f9iwqnh8cg79ga3rw4-clippy-1.94.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/wavkh1sj2fb7cclydhj1q15k7dcha91x-clippy-1.94.1", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/2970l2zp3wwb361mabagg9gfgnwkn9s2-clippy-1.94.1-debug" + } + ], + "store_path": "/nix/store/wavkh1sj2fb7cclydhj1q15k7dcha91x-clippy-1.94.1" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2026-05-15T18:21:44Z", + "resolved": "github:NixOS/nixpkgs/d233902339c02a9c334e7e593de68855ad26c4cb?lastModified=1778869304&narHash=sha256-30sZNZoA1cqF5JNO9fVX%2BwgiQYjB7HJqqJ4ztCDeBZE%3D" + }, + "nodejs@20": { + "last_modified": "2026-04-23T13:07:47Z", + "plugin_version": "0.0.2", + "resolved": "github:NixOS/nixpkgs/01fbdeef22b76df85ea168fbfe1bfd9e63681b30#nodejs_20", + "source": "devbox-search", + "version": "20.20.2", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/gvr230jrjzznrhlmfymjj37x0dx3srvv-nodejs-20.20.2", + "default": true + } + ], + "store_path": "/nix/store/gvr230jrjzznrhlmfymjj37x0dx3srvv-nodejs-20.20.2" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jx5whkya850cy6a1fjf74p5dfqlcp1cq-nodejs-20.20.2", + "default": true + } + ], + "store_path": "/nix/store/jx5whkya850cy6a1fjf74p5dfqlcp1cq-nodejs-20.20.2" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ggqdy8b4b62gr1ak2v41dka5s1dmflzk-nodejs-20.20.2", + "default": true + } + ], + "store_path": "/nix/store/ggqdy8b4b62gr1ak2v41dka5s1dmflzk-nodejs-20.20.2" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/nr0dnrnibq8114xa0i7zr7qg6n05q03g-nodejs-20.20.2", + "default": true + } + ], + "store_path": "/nix/store/nr0dnrnibq8114xa0i7zr7qg6n05q03g-nodejs-20.20.2" + } + } + }, + "rustc@latest": { + "last_modified": "2026-04-23T13:07:47Z", + "plugin_version": "0.0.1", + "resolved": "github:NixOS/nixpkgs/01fbdeef22b76df85ea168fbfe1bfd9e63681b30#rustc", + "source": "devbox-search", + "version": "1.94.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ldg6i5q2wwb0j4nnd1939cn7hdlsf8xl-rustc-wrapper-1.94.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/gljrw9zcrgnbmwgdxpw75klxf26i81sa-rustc-wrapper-1.94.1-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/d0h4z9rx30ipgxb0r8pp2iy01fqmbvdn-rustc-wrapper-1.94.1-doc" + } + ], + "store_path": "/nix/store/ldg6i5q2wwb0j4nnd1939cn7hdlsf8xl-rustc-wrapper-1.94.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ixn1awaxpvayfp5na0b2gr0kianim6g8-rustc-wrapper-1.94.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/fvzjq79fryqp523s5dyqfwyfj2wi8afb-rustc-wrapper-1.94.1-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/impmc1i7ifkbd4wvlxcybi0jhrafwxgb-rustc-wrapper-1.94.1-doc" + } + ], + "store_path": "/nix/store/ixn1awaxpvayfp5na0b2gr0kianim6g8-rustc-wrapper-1.94.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/szlbg4mqziywqn196x89q6n7xqdbx7dz-rustc-wrapper-1.94.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/wzrjq7km3gicbw2ag873hd9yf8qk2rn8-rustc-wrapper-1.94.1-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/f2iqkj8xgpjqsh9idpywrg7bgh16jw2i-rustc-wrapper-1.94.1-doc" + } + ], + "store_path": "/nix/store/szlbg4mqziywqn196x89q6n7xqdbx7dz-rustc-wrapper-1.94.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ph8jb0mw89p4lfshpp36z70l6r4kg3vh-rustc-wrapper-1.94.1", + "default": true + }, + { + "name": "man", + "path": "/nix/store/h68fmikviqzaspyx3ccghx6cvr69cds3-rustc-wrapper-1.94.1-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/kbnirgis5p5wlqb3wk11vgxa0c966lck-rustc-wrapper-1.94.1-doc" + } + ], + "store_path": "/nix/store/ph8jb0mw89p4lfshpp36z70l6r4kg3vh-rustc-wrapper-1.94.1" + } + } + }, + "rustfmt@latest": { + "last_modified": "2026-04-23T13:07:47Z", + "resolved": "github:NixOS/nixpkgs/01fbdeef22b76df85ea168fbfe1bfd9e63681b30#rustfmt", + "source": "devbox-search", + "version": "1.94.1", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/371msib7kcxjsjhck6562pfa0q9nn8mw-rustfmt-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/371msib7kcxjsjhck6562pfa0q9nn8mw-rustfmt-1.94.1" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/y466x0p4r8qjz5z4qpcjd16y8vmlzhl3-rustfmt-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/y466x0p4r8qjz5z4qpcjd16y8vmlzhl3-rustfmt-1.94.1" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/xz887wb672i5sb1p5yj8mjm1190np784-rustfmt-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/xz887wb672i5sb1p5yj8mjm1190np784-rustfmt-1.94.1" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/rsshbz486a0ka567awpbkbawaqdr40pd-rustfmt-1.94.1", + "default": true + } + ], + "store_path": "/nix/store/rsshbz486a0ka567awpbkbawaqdr40pd-rustfmt-1.94.1" + } + } + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ad7e9d6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1779508470, + "narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "29916453413845e54a65b8a1cf996842300cd299", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c0d929c --- /dev/null +++ b/flake.nix @@ -0,0 +1,88 @@ +{ + description = "Brain Brew local-first deck federation CLI"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system (import nixpkgs { inherit system; })); + workspace = builtins.fromTOML (builtins.readFile ./Cargo.toml); + version = workspace.workspace.package.version; + in + { + packages = forAllSystems ( + system: pkgs: + let + brainbrew = pkgs.rustPlatform.buildRustPackage { + pname = "brainbrew"; + inherit version; + + src = pkgs.lib.cleanSource ./.; + cargoLock.lockFile = ./Cargo.lock; + + cargoBuildFlags = [ + "-p" + "brain-brew-cli" + "--bin" + "brainbrew" + ]; + cargoTestFlags = [ + "--workspace" + "--all-targets" + ]; + + meta = { + description = "Local-first deck federation and round-trip CLI for Anki-compatible decks"; + homepage = "https://github.com/jeprecated/brain-brew"; + license = pkgs.lib.licenses.unlicense; + mainProgram = "brainbrew"; + }; + }; + in + { + inherit brainbrew; + default = brainbrew; + } + ); + + apps = forAllSystems ( + system: _pkgs: + let + brainbrew = self.packages.${system}.brainbrew; + in + { + brainbrew = { + type = "app"; + program = "${brainbrew}/bin/brainbrew"; + meta.description = "Run the Brain Brew CLI"; + }; + default = self.apps.${system}.brainbrew; + } + ); + + checks = forAllSystems (system: _pkgs: { + brainbrew = self.packages.${system}.brainbrew; + default = self.checks.${system}.brainbrew; + }); + + devShells = forAllSystems (system: pkgs: { + default = pkgs.mkShell { + packages = [ + pkgs.cargo + pkgs.clippy + pkgs.rustc + pkgs.rustfmt + ]; + }; + }); + }; +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f43d64e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +edition = "2024" +newline_style = "Unix" +max_width = 100 From 56b5e3feb0fe597fedb52d37687ed426b7b6990d Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 25 May 2026 09:50:56 +0200 Subject: [PATCH 003/142] test: add federation fixtures and regression coverage --- crates/brain-brew-cli/tests/cli.rs | 1691 ++++ .../brain-brew-cli/tests/ug_style_fixture.rs | 158 + .../tests/canonical_deck_validation.rs | 192 + .../brain-brew-core/tests/overlay_compose.rs | 963 +++ crates/brain-brew-core/tests/semantic_diff.rs | 147 + .../tests/canonical_yaml.rs | 319 + crates/brain-brew-formats/tests/crowdanki.rs | 433 + .../brain-brew-formats/tests/lockfile_yaml.rs | 65 + .../brain-brew-formats/tests/manifest_yaml.rs | 289 + .../tests/media_references.rs | 222 + .../brain-brew-formats/tests/overlay_yaml.rs | 736 ++ .../tests/ultimate_geography_fixture.rs | 1453 ++++ fixtures/ug-style/brainbrew.yaml | 29 + fixtures/ug-style/deck.yaml | 659 ++ fixtures/ug-style/extension-population.yaml | 57 + .../ug-style/patch-netherlands-capital.yaml | 11 + fixtures/ug-style/personal-finland-hint.yaml | 9 + fixtures/ug-style/tombstone-australia.yaml | 6 + fixtures/ug-style/translation-es.yaml | 228 + fixtures/ug-style/translation-sv.yaml | 228 + fixtures/ultimate-geography/brainbrew.yaml | 581 ++ fixtures/ultimate-geography/deck.yaml | 7036 +++++++++++++++++ .../overlays/extensions/hardcore.yaml | 665 ++ .../extensions/hardcore/field-fills.yaml | 84 + .../extensions/hardcore/field-fills/de.yaml | 40 + .../extensions/hardcore/field-fills/es.yaml | 88 + .../extensions/hardcore/field-fills/fr.yaml | 64 + .../extensions/hardcore/field-fills/nb.yaml | 48 + .../extensions/hardcore/translations/cs.yaml | 22 + .../extensions/hardcore/translations/de.yaml | 79 + .../extensions/hardcore/translations/es.yaml | 81 + .../extensions/hardcore/translations/fr.yaml | 83 + .../extensions/hardcore/translations/it.yaml | 22 + .../extensions/hardcore/translations/nb.yaml | 79 + .../extensions/hardcore/translations/nl.yaml | 22 + .../extensions/hardcore/translations/pl.yaml | 22 + .../extensions/hardcore/translations/pt.yaml | 22 + .../extensions/hardcore/translations/ru.yaml | 22 + .../extensions/hardcore/translations/sv.yaml | 22 + .../extensions/hardcore/translations/zh.yaml | 22 + .../overlays/languages/cs.yaml | 811 ++ .../overlays/languages/da.yaml | 703 ++ .../overlays/languages/de.yaml | 747 ++ .../overlays/languages/es.yaml | 832 ++ .../overlays/languages/fr.yaml | 768 ++ .../overlays/languages/it.yaml | 711 ++ .../overlays/languages/nb.yaml | 705 ++ .../overlays/languages/nl.yaml | 721 ++ .../overlays/languages/pl.yaml | 798 ++ .../overlays/languages/pt.yaml | 872 ++ .../overlays/languages/ru.yaml | 1005 +++ .../overlays/languages/sv.yaml | 772 ++ .../overlays/languages/zh-tw.yaml | 1032 +++ .../overlays/languages/zh.yaml | 1032 +++ .../overlays/variants/experimental.yaml | 745 ++ .../overlays/variants/experimental/cs.yaml | 11 + .../overlays/variants/experimental/da.yaml | 11 + .../overlays/variants/experimental/de.yaml | 11 + .../overlays/variants/experimental/en.yaml | 11 + .../overlays/variants/experimental/es.yaml | 11 + .../overlays/variants/experimental/fr.yaml | 11 + .../overlays/variants/experimental/it.yaml | 11 + .../overlays/variants/experimental/nb.yaml | 11 + .../overlays/variants/experimental/nl.yaml | 11 + .../overlays/variants/experimental/pl.yaml | 11 + .../overlays/variants/experimental/pt.yaml | 11 + .../overlays/variants/experimental/ru.yaml | 11 + .../overlays/variants/experimental/sv.yaml | 11 + .../overlays/variants/experimental/zh-tw.yaml | 11 + .../overlays/variants/experimental/zh.yaml | 11 + .../overlays/variants/extended.yaml | 80 + .../overlays/variants/extended/cs.yaml | 11 + .../overlays/variants/extended/da.yaml | 11 + .../overlays/variants/extended/de.yaml | 11 + .../overlays/variants/extended/en.yaml | 11 + .../overlays/variants/extended/es.yaml | 11 + .../overlays/variants/extended/fr.yaml | 11 + .../overlays/variants/extended/it.yaml | 11 + .../overlays/variants/extended/nb.yaml | 11 + .../overlays/variants/extended/nl.yaml | 11 + .../overlays/variants/extended/pl.yaml | 11 + .../overlays/variants/extended/pt.yaml | 11 + .../overlays/variants/extended/ru.yaml | 11 + .../overlays/variants/extended/sv.yaml | 60 + .../overlays/variants/extended/zh-tw.yaml | 11 + .../overlays/variants/extended/zh.yaml | 11 + scripts/fetch_ug_release_oracle.py | 158 + 87 files changed, 29870 insertions(+) create mode 100644 crates/brain-brew-cli/tests/cli.rs create mode 100644 crates/brain-brew-cli/tests/ug_style_fixture.rs create mode 100644 crates/brain-brew-core/tests/canonical_deck_validation.rs create mode 100644 crates/brain-brew-core/tests/overlay_compose.rs create mode 100644 crates/brain-brew-core/tests/semantic_diff.rs create mode 100644 crates/brain-brew-formats/tests/canonical_yaml.rs create mode 100644 crates/brain-brew-formats/tests/crowdanki.rs create mode 100644 crates/brain-brew-formats/tests/lockfile_yaml.rs create mode 100644 crates/brain-brew-formats/tests/manifest_yaml.rs create mode 100644 crates/brain-brew-formats/tests/media_references.rs create mode 100644 crates/brain-brew-formats/tests/overlay_yaml.rs create mode 100644 crates/brain-brew-formats/tests/ultimate_geography_fixture.rs create mode 100644 fixtures/ug-style/brainbrew.yaml create mode 100644 fixtures/ug-style/deck.yaml create mode 100644 fixtures/ug-style/extension-population.yaml create mode 100644 fixtures/ug-style/patch-netherlands-capital.yaml create mode 100644 fixtures/ug-style/personal-finland-hint.yaml create mode 100644 fixtures/ug-style/tombstone-australia.yaml create mode 100644 fixtures/ug-style/translation-es.yaml create mode 100644 fixtures/ug-style/translation-sv.yaml create mode 100644 fixtures/ultimate-geography/brainbrew.yaml create mode 100644 fixtures/ultimate-geography/deck.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/de.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/es.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/fr.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/nb.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/cs.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/it.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nl.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/pl.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/pt.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/ru.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/sv.yaml create mode 100644 fixtures/ultimate-geography/overlays/extensions/hardcore/translations/zh.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/cs.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/da.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/de.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/es.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/fr.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/it.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/nb.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/nl.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/pl.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/pt.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/ru.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/sv.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/zh-tw.yaml create mode 100644 fixtures/ultimate-geography/overlays/languages/zh.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/cs.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/da.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/de.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/en.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/es.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/fr.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/it.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/nb.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/nl.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/pl.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/pt.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/ru.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/sv.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/zh-tw.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/experimental/zh.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/cs.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/da.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/de.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/en.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/es.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/fr.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/it.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/nb.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/nl.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/pl.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/pt.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/ru.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/sv.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/zh-tw.yaml create mode 100644 fixtures/ultimate-geography/overlays/variants/extended/zh.yaml create mode 100755 scripts/fetch_ug_release_oracle.py diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs new file mode 100644 index 0000000..3d9218b --- /dev/null +++ b/crates/brain-brew-cli/tests/cli.rs @@ -0,0 +1,1691 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use flate2::Compression; +use flate2::write::GzEncoder; +use tar::Builder; + +#[test] +fn top_level_help_includes_examples() { + let output = run(["--help"]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("Usage:")); + assert!(out.contains("Examples:")); + assert!(out.contains("brainbrew targets --manifest brainbrew.yaml")); + assert!( + out.contains("brainbrew export crowdanki --manifest brainbrew.yaml --target de-extended") + ); +} + +#[test] +fn command_help_includes_focused_examples() { + let output = run(["compose", "--help"]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("Usage:")); + assert!(out.contains( + "brainbrew compose --manifest brainbrew.yaml --target da-standard --out build/da.yaml" + )); +} + +#[test] +fn validate_without_args_shows_usage_examples() { + let output = run(["validate"]); + + assert!(!output.status.success()); + let err = stderr(&output); + assert!(err.contains("Usage:")); + assert!(err.contains("Examples:")); + assert!(err.contains("brainbrew validate deck.yaml")); +} + +#[test] +fn validate_reports_valid_deck_human_readably() { + let dir = temp_dir("validate-valid"); + let deck_path = dir.join("deck.yaml"); + fs::write(&deck_path, SAMPLE_CANONICAL_YAML).unwrap(); + + let output = run(["validate", deck_path.to_str().unwrap()]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("✓")); + assert!(out.contains("valid deck")); + assert!(out.contains(deck_path.to_str().unwrap())); + assert!(out.contains("notes: 1")); +} + +#[test] +fn validate_reports_invalid_deck_path() { + let dir = temp_dir("validate-invalid"); + let deck_path = dir.join("deck.yaml"); + fs::write( + &deck_path, + SAMPLE_CANONICAL_YAML.replace( + "note_type_id: note-type.country", + "note_type_id: note-type.missing", + ), + ) + .unwrap(); + + let output = run(["validate", deck_path.to_str().unwrap()]); + + assert!(!output.status.success()); + assert!(stderr(&output).contains("notes.note.finland.note_type_id")); +} + +#[test] +fn fmt_rewrites_canonical_yaml_in_place() { + let dir = temp_dir("fmt"); + let deck_path = dir.join("deck.yaml"); + fs::write(&deck_path, MESSY_CANONICAL_YAML).unwrap(); + + let output = run(["fmt", deck_path.to_str().unwrap()]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert_eq!( + fs::read_to_string(deck_path).unwrap(), + SAMPLE_CANONICAL_YAML + ); +} + +#[test] +fn fmt_rewrites_overlay_yaml_in_place() { + let dir = temp_dir("fmt-overlay"); + let overlay_path = dir.join("overlay.yaml"); + fs::write(&overlay_path, MESSY_OVERLAY_YAML).unwrap(); + + let output = run(["fmt", overlay_path.to_str().unwrap()]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert_eq!( + fs::read_to_string(overlay_path).unwrap(), + CAPITAL_OVERLAY_YAML + ); +} + +#[test] +fn fmt_rewrites_manifest_yaml_in_place() { + let dir = temp_dir("fmt-manifest"); + let manifest_path = dir.join("brainbrew.yaml"); + fs::write(&manifest_path, MESSY_MANIFEST_YAML).unwrap(); + + let output = run(["fmt", manifest_path.to_str().unwrap()]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert_eq!(fs::read_to_string(manifest_path).unwrap(), MANIFEST_YAML); +} + +#[test] +fn fmt_rewrites_federation_lock_yaml_in_place() { + let dir = temp_dir("fmt-lock"); + let lock_path = dir.join("brainbrew.lock"); + fs::write(&lock_path, MESSY_LOCK_YAML).unwrap(); + + let output = run(["fmt", lock_path.to_str().unwrap()]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert_eq!(fs::read_to_string(lock_path).unwrap(), LOCK_YAML); +} + +#[test] +fn compose_applies_overlay_files_in_order() { + let dir = temp_dir("compose-overlay"); + let deck_path = dir.join("deck.yaml"); + let overlay_path = dir.join("overlay.yaml"); + let resolved_path = dir.join("resolved.yaml"); + fs::write(&deck_path, SAMPLE_CANONICAL_YAML).unwrap(); + fs::write(&overlay_path, CAPITAL_OVERLAY_YAML).unwrap(); + + let output = run([ + "compose", + deck_path.to_str().unwrap(), + "--overlay", + overlay_path.to_str().unwrap(), + "--out", + resolved_path.to_str().unwrap(), + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("✓")); + assert!(out.contains("composed deck")); + assert!(out.contains(resolved_path.to_str().unwrap())); + assert!( + fs::read_to_string(resolved_path) + .unwrap() + .contains("field.capital: Helsingfors") + ); +} + +#[test] +fn targets_lists_manifest_targets() { + let dir = temp_dir("targets-manifest"); + write_manifest_workspace(&dir); + + let output = run([ + "targets", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert_eq!(stdout(&output), "patched-via-dependency\n"); +} + +#[test] +fn targets_can_discover_multiple_package_manifests() { + let first = temp_dir("targets-package-first"); + let second = temp_dir("targets-package-second"); + write_manifest_workspace(&first); + write_manifest_workspace(&second); + fs::write(first.join("brainbrew.yaml"), MANIFEST_WITH_PACKAGE_YAML).unwrap(); + fs::write( + second.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML + .replace("anki-geo.ultimate-geography", "anki-geo.rivers") + .replace("patched-via-dependency", "rivers"), + ) + .unwrap(); + + let output = run([ + "targets", + "--manifest", + first.join("brainbrew.yaml").to_str().unwrap(), + "--include", + second.join("brainbrew.yaml").to_str().unwrap(), + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("anki-geo.ultimate-geography:patched-via-dependency")); + assert!(out.contains("anki-geo.rivers:rivers")); +} + +#[test] +fn targets_discovers_package_root_and_validates_dependencies() { + let root = temp_dir("targets-package-root"); + let ug = root.join("ultimate-geography"); + let rivers = root.join("rivers"); + fs::create_dir_all(&ug).unwrap(); + fs::create_dir_all(&rivers).unwrap(); + write_manifest_workspace(&ug); + write_manifest_workspace(&rivers); + fs::write( + ug.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML.replace(" depends_on:\n - anki-geo.shared-geography\n", ""), + ) + .unwrap(); + fs::write( + rivers.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML + .replace("anki-geo.ultimate-geography", "anki-geo.rivers") + .replace( + "depends_on:\n - anki-geo.shared-geography", + "depends_on:\n - anki-geo.ultimate-geography@0.1.0", + ) + .replace("patched-via-dependency", "rivers"), + ) + .unwrap(); + + let output = run(["targets", "--package-root", root.to_str().unwrap()]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("anki-geo.ultimate-geography:patched-via-dependency")); + assert!(out.contains("anki-geo.rivers:rivers")); +} + +#[test] +fn compose_can_resolve_extended_targets_from_brainbrew_lock() { + let root = temp_dir("compose-federated-lock"); + let ug = root.join("ultimate-geography"); + let america = root.join("america"); + fs::create_dir_all(&ug).unwrap(); + fs::create_dir_all(&america).unwrap(); + write_manifest_workspace(&ug); + fs::write( + ug.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML.replace(" depends_on:\n - anki-geo.shared-geography\n", ""), + ) + .unwrap(); + fs::write(america.join("deck.yaml"), SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + america.join("america.yaml"), + r#"id: overlay.extension.america +kind: extension +notes: + note.finland: + intent: merge + tags: + America::Imported: + intent: add +"#, + ) + .unwrap(); + fs::write( + america.join("brainbrew.yaml"), + r#"package: + id: anki-geo.america + version: 0.1.0 + depends_on: + - anki-geo.ultimate-geography@0.1.0 +base: deck.yaml +overlays: + overlay.extension.america: + file: america.yaml + kind: extension +targets: + en-america: + extends: anki-geo.ultimate-geography:patched-via-dependency + overlays: + - overlay.extension.america +"#, + ) + .unwrap(); + fs::write( + america.join("brainbrew.lock"), + format!( + r#"version: 1 +packages: + anki-geo.ultimate-geography: + manifest: brainbrew.yaml + package: + version: 0.1.0 + locked: + type: path + path: '{}' +"#, + ug.canonicalize().unwrap().display() + ), + ) + .unwrap(); + let resolved = root.join("resolved.yaml"); + let cache = root.join("cache"); + + let output = run_with_cache( + [ + "compose", + "--manifest", + america.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "en-america", + "--out", + resolved.to_str().unwrap(), + ], + &cache, + ); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let resolved_source = fs::read_to_string(resolved).unwrap(); + assert!(resolved_source.contains("field.capital: Helsingfors")); + assert!(resolved_source.contains("America::Imported")); +} + +#[test] +fn lock_update_and_verify_path_package_without_nix() { + let root = temp_dir("lock-update-path"); + let ug = root.join("ultimate-geography"); + let america = root.join("america"); + fs::create_dir_all(&ug).unwrap(); + fs::create_dir_all(&america).unwrap(); + write_manifest_workspace(&ug); + fs::write( + ug.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML.replace(" depends_on:\n - anki-geo.shared-geography\n", ""), + ) + .unwrap(); + let lock_path = america.join("brainbrew.lock"); + + let cache = root.join("cache"); + let update = run_with_cache( + [ + "lock", + "update", + "--lock", + lock_path.to_str().unwrap(), + "--package", + "anki-geo.ultimate-geography", + "--path", + ug.to_str().unwrap(), + ], + &cache, + ); + + assert!(update.status.success(), "stderr: {}", stderr(&update)); + let lock_source = fs::read_to_string(&lock_path).unwrap(); + assert!(lock_source.contains("original:\n type: path")); + assert!(lock_source.contains("locked:\n type: path")); + assert!(lock_source.contains(&format!("path: {}", ug.canonicalize().unwrap().display()))); + assert!(!lock_source.contains("/nix/store/")); + assert!(lock_source.contains("nar_hash: 'sha256-")); + + let verify = run_with_cache( + ["lock", "verify", "--lock", lock_path.to_str().unwrap()], + &cache, + ); + + assert!(verify.status.success(), "stderr: {}", stderr(&verify)); + assert!(stdout(&verify).contains("verified 1 locked package")); + + fs::write( + &lock_path, + fs::read_to_string(&lock_path) + .unwrap() + .replace("sha256-", "sha256-bad"), + ) + .unwrap(); + let mismatch = run_with_cache( + ["lock", "verify", "--lock", lock_path.to_str().unwrap()], + &cache, + ); + + assert!(!mismatch.status.success()); + assert!(stderr(&mismatch).contains("nar_hash mismatch")); +} + +#[test] +fn lock_update_and_verify_tarball_package_without_nix() { + let root = temp_dir("lock-update-tarball"); + let ug = root.join("ultimate-geography"); + let america = root.join("america"); + fs::create_dir_all(&ug).unwrap(); + fs::create_dir_all(&america).unwrap(); + write_manifest_workspace(&ug); + fs::write( + ug.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML.replace(" depends_on:\n - anki-geo.shared-geography\n", ""), + ) + .unwrap(); + let archive_path = root.join("ultimate-geography.tar.gz"); + write_tar_gz(&archive_path, "ultimate-geography", &ug); + let lock_path = america.join("brainbrew.lock"); + let cache = root.join("cache"); + + let update = run_with_cache( + [ + "lock", + "update", + "--lock", + lock_path.to_str().unwrap(), + "--package", + "anki-geo.ultimate-geography", + "--tarball", + &format!("file://{}", archive_path.display()), + ], + &cache, + ); + + assert!(update.status.success(), "stderr: {}", stderr(&update)); + let lock_source = fs::read_to_string(&lock_path).unwrap(); + assert!(lock_source.contains("original:\n type: tarball")); + assert!(lock_source.contains("locked:\n type: tarball")); + assert!(lock_source.contains("nar_hash: 'sha256-")); + + let verify = run_with_cache( + ["lock", "verify", "--lock", lock_path.to_str().unwrap()], + &cache, + ); + + assert!(verify.status.success(), "stderr: {}", stderr(&verify)); + assert!(stdout(&verify).contains("verified 1 locked package")); +} + +#[test] +fn compose_can_extend_targets_and_mix_overlays_from_included_package_manifests() { + let root = temp_dir("compose-federated-package"); + let ug = root.join("ultimate-geography"); + let america = root.join("america"); + fs::create_dir_all(&ug).unwrap(); + fs::create_dir_all(&america).unwrap(); + write_manifest_workspace(&ug); + fs::write( + ug.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML.replace(" depends_on:\n - anki-geo.shared-geography\n", ""), + ) + .unwrap(); + fs::write(america.join("deck.yaml"), SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + america.join("america.yaml"), + r#"id: overlay.extension.america +kind: extension +notes: + note.finland: + intent: merge + tags: + America::Imported: + intent: add +"#, + ) + .unwrap(); + fs::write( + america.join("brainbrew.yaml"), + r#"package: + id: anki-geo.america + version: 0.1.0 + depends_on: + - anki-geo.ultimate-geography@0.1.0 +base: deck.yaml +overlays: + overlay.extension.america: + file: america.yaml + kind: extension +targets: + en-america: + extends: anki-geo.ultimate-geography:patched-via-dependency + overlays: + - overlay.extension.america +"#, + ) + .unwrap(); + let america_manifest = america.join("brainbrew.yaml"); + let ug_manifest = ug.join("brainbrew.yaml"); + let resolved = root.join("resolved.yaml"); + + let output = run([ + "compose", + "--manifest", + america_manifest.to_str().unwrap(), + "--include", + ug_manifest.to_str().unwrap(), + "--target", + "en-america", + "--out", + resolved.to_str().unwrap(), + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let resolved_source = fs::read_to_string(resolved).unwrap(); + assert!(resolved_source.contains("field.capital: Helsingfors")); + assert!(resolved_source.contains("America::Imported")); + + let mixer = root.join("mixer"); + fs::create_dir_all(&mixer).unwrap(); + fs::write(mixer.join("deck.yaml"), SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + mixer.join("brainbrew.yaml"), + r#"package: + id: example.mix + version: 0.1.0 + depends_on: + - anki-geo.ultimate-geography@0.1.0 + - anki-geo.america@0.1.0 +base: deck.yaml +overlays: {} +targets: + en-mixed: + extends: anki-geo.ultimate-geography:patched-via-dependency + overlays: + - anki-geo.america:overlay.extension.america +"#, + ) + .unwrap(); + let mixer_manifest = mixer.join("brainbrew.yaml"); + let mixed_resolved = root.join("mixed-resolved.yaml"); + + let output = run([ + "compose", + "--manifest", + mixer_manifest.to_str().unwrap(), + "--include", + ug_manifest.to_str().unwrap(), + "--include", + america_manifest.to_str().unwrap(), + "--target", + "en-mixed", + "--out", + mixed_resolved.to_str().unwrap(), + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let mixed_source = fs::read_to_string(mixed_resolved).unwrap(); + assert!(mixed_source.contains("field.capital: Helsingfors")); + assert!(mixed_source.contains("America::Imported")); +} + +#[test] +fn targets_reports_missing_package_dependencies() { + let root = temp_dir("targets-missing-package-dep"); + let rivers = root.join("rivers"); + fs::create_dir_all(&rivers).unwrap(); + write_manifest_workspace(&rivers); + fs::write( + rivers.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML + .replace("anki-geo.ultimate-geography", "anki-geo.rivers") + .replace( + "depends_on:\n - anki-geo.shared-geography", + "depends_on:\n - anki-geo.ultimate-geography@0.1.0", + ) + .replace("patched-via-dependency", "rivers"), + ) + .unwrap(); + + let output = run(["targets", "--package-root", root.to_str().unwrap()]); + + assert!(!output.status.success()); + assert!(stderr(&output).contains("package dependency anki-geo.ultimate-geography")); +} + +#[test] +fn targets_reports_package_dependency_version_mismatches() { + let root = temp_dir("targets-package-version-mismatch"); + let ug = root.join("ultimate-geography"); + let rivers = root.join("rivers"); + fs::create_dir_all(&ug).unwrap(); + fs::create_dir_all(&rivers).unwrap(); + write_manifest_workspace(&ug); + write_manifest_workspace(&rivers); + fs::write( + ug.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML.replace(" depends_on:\n - anki-geo.shared-geography\n", ""), + ) + .unwrap(); + fs::write( + rivers.join("brainbrew.yaml"), + MANIFEST_WITH_PACKAGE_YAML + .replace("anki-geo.ultimate-geography", "anki-geo.rivers") + .replace( + "depends_on:\n - anki-geo.shared-geography", + "depends_on:\n - anki-geo.ultimate-geography@9.9.9", + ) + .replace("patched-via-dependency", "rivers"), + ) + .unwrap(); + + let output = run(["targets", "--package-root", root.to_str().unwrap()]); + + assert!(!output.status.success()); + assert!(stderr(&output).contains("resolved to version 0.1.0")); +} + +#[test] +fn targets_json_includes_package_metadata() { + let dir = temp_dir("targets-package-json"); + write_manifest_workspace(&dir); + fs::write(dir.join("brainbrew.yaml"), MANIFEST_WITH_PACKAGE_YAML).unwrap(); + + let output = run([ + "targets", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--json", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let json: serde_json::Value = serde_json::from_str(&stdout(&output)).unwrap(); + assert_eq!(json["package"]["id"], "anki-geo.ultimate-geography"); + assert_eq!(json["package"]["version"], "0.1.0"); +} + +#[test] +fn targets_can_report_json_with_expanded_overlays() { + let dir = temp_dir("targets-json"); + write_manifest_workspace(&dir); + + let output = run([ + "targets", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--json", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let json: serde_json::Value = serde_json::from_str(&stdout(&output)).unwrap(); + assert_eq!(json["targets"][0]["name"], "patched-via-dependency"); + assert_eq!(json["targets"][0]["overlays"][0]["id"], "patch.capital"); + assert_eq!( + json["targets"][0]["overlays"][1]["id"], + "noop.after-capital" + ); +} + +#[test] +fn validate_uses_manifest_target() { + let dir = temp_dir("validate-manifest"); + write_manifest_workspace(&dir); + + let output = run([ + "validate", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("✓")); + assert!(out.contains("valid target")); + assert!(out.contains("patched-via-dependency")); +} + +#[test] +fn manifest_target_errors_list_available_targets() { + let dir = temp_dir("missing-target"); + write_manifest_workspace(&dir); + + let output = run([ + "compose", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "missing", + ]); + + assert!(!output.status.success()); + assert!(stderr(&output).contains("available targets: patched-via-dependency")); +} + +#[test] +fn compose_uses_manifest_target_dependency_expansion() { + let dir = temp_dir("compose-manifest"); + write_manifest_workspace(&dir); + let resolved_path = dir.join("resolved.yaml"); + + let output = run([ + "compose", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + "--out", + resolved_path.to_str().unwrap(), + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("✓")); + assert!(out.contains("composed target")); + assert!(out.contains("patched-via-dependency")); + assert!(out.contains(resolved_path.to_str().unwrap())); + assert!( + fs::read_to_string(resolved_path) + .unwrap() + .contains("field.capital: Helsingfors") + ); +} + +#[test] +fn export_crowdanki_uses_manifest_target_configured_out() { + let dir = temp_dir("export-manifest-configured-out"); + write_manifest_workspace(&dir); + fs::write(dir.join("brainbrew.yaml"), MANIFEST_WITH_EXPORTS_YAML).unwrap(); + + let output = run([ + "export", + "crowdanki", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert!(dir.join("configured-crowdanki/deck.json").exists()); +} + +#[test] +fn export_crowdanki_defaults_manifest_target_out_to_build_crowdanki_target() { + let dir = temp_dir("export-manifest-default-out"); + write_manifest_workspace(&dir); + + let output = run([ + "export", + "crowdanki", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert!( + dir.join("build/crowdanki/patched-via-dependency/deck.json") + .exists() + ); +} + +#[test] +fn export_crowdanki_uses_manifest_target() { + let dir = temp_dir("export-manifest"); + write_manifest_workspace(&dir); + let export_dir = dir.join("crowdanki"); + + let output = run([ + "export", + "crowdanki", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + "--out", + export_dir.to_str().unwrap(), + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert!( + fs::read_to_string(export_dir.join("deck.json")) + .unwrap() + .contains("Helsingfors") + ); +} + +#[test] +fn verify_compares_configured_crowdanki_golden() { + let dir = temp_dir("verify-golden"); + write_manifest_workspace(&dir); + fs::write(dir.join("brainbrew.yaml"), MANIFEST_WITH_EXPORTS_YAML).unwrap(); + let golden_dir = dir.join("goldens/patched"); + + let export_output = run([ + "export", + "crowdanki", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + "--out", + golden_dir.to_str().unwrap(), + ]); + assert!( + export_output.status.success(), + "stderr: {}", + stderr(&export_output) + ); + + let verify_output = run([ + "verify", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--all-targets", + ]); + assert!( + verify_output.status.success(), + "stderr: {}", + stderr(&verify_output) + ); + + let golden_path = golden_dir.join("deck.json"); + fs::write( + &golden_path, + fs::read_to_string(&golden_path) + .unwrap() + .replace("Helsingfors", "Helsinki"), + ) + .unwrap(); + let mismatch_output = run([ + "verify", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + ]); + assert!(!mismatch_output.status.success()); + assert!(stderr(&mismatch_output).contains("CrowdAnki golden mismatch")); +} + +#[test] +fn verify_allows_configured_crowdanki_golden_paths() { + let dir = temp_dir("verify-golden-allowlist"); + write_manifest_workspace(&dir); + fs::write( + dir.join("brainbrew.yaml"), + MANIFEST_WITH_EXPORTS_YAML.replace( + " golden: goldens/patched/deck.json\n", + " golden: goldens/patched/deck.json\n golden_allowlist:\n - '$.name'\n", + ), + ) + .unwrap(); + let golden_dir = dir.join("goldens/patched"); + + let export_output = run([ + "export", + "crowdanki", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + "--out", + golden_dir.to_str().unwrap(), + ]); + assert!( + export_output.status.success(), + "stderr: {}", + stderr(&export_output) + ); + + let golden_path = golden_dir.join("deck.json"); + let mut golden_json: serde_json::Value = + serde_json::from_str(&fs::read_to_string(&golden_path).unwrap()).unwrap(); + golden_json["name"] = serde_json::json!("Allowed Legacy Name"); + fs::write( + &golden_path, + serde_json::to_string_pretty(&golden_json).unwrap(), + ) + .unwrap(); + + let verify_output = run([ + "verify", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + ]); + assert!( + verify_output.status.success(), + "stderr: {}", + stderr(&verify_output) + ); +} + +#[test] +fn verify_checks_all_manifest_targets() { + let dir = temp_dir("verify-manifest"); + write_manifest_workspace(&dir); + + let output = run([ + "verify", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--all-targets", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("✓")); + assert!(out.contains("verified 1 target")); +} + +#[test] +fn export_crowdanki_copies_media_from_media_root_and_checks_hashes() { + let dir = temp_dir("export-media"); + let deck_path = dir.join("deck.yaml"); + let media_root = dir.join("media"); + let export_dir = dir.join("crowdanki"); + fs::write(&deck_path, MEDIA_CANONICAL_YAML).unwrap(); + fs::create_dir_all(media_root.join("flags")).unwrap(); + fs::write(media_root.join("flags/fi.png"), b"flag-bytes").unwrap(); + + let output = run([ + "export", + "crowdanki", + deck_path.to_str().unwrap(), + "--media-root", + media_root.to_str().unwrap(), + "--out", + export_dir.to_str().unwrap(), + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert_eq!( + fs::read(export_dir.join("media/flags/fi.png")).unwrap(), + b"flag-bytes" + ); +} + +#[test] +fn verify_checks_media_root_hashes() { + let dir = temp_dir("verify-media"); + fs::write(dir.join("deck.yaml"), MEDIA_CANONICAL_YAML).unwrap(); + fs::write(dir.join("brainbrew.yaml"), SIMPLE_MEDIA_MANIFEST_YAML).unwrap(); + fs::create_dir_all(dir.join("media/flags")).unwrap(); + fs::write(dir.join("media/flags/fi.png"), b"wrong-bytes").unwrap(); + + let output = run([ + "verify", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--all-targets", + "--media-root", + dir.join("media").to_str().unwrap(), + ]); + + assert!(!output.status.success()); + assert!(stderr(&output).contains("sha256")); +} + +#[test] +fn explain_reports_expanded_stack_and_diff() { + let dir = temp_dir("explain"); + write_manifest_workspace(&dir); + + let output = run([ + "explain", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("target: patched-via-dependency")); + assert!(out.contains("1. patch.capital (capital.yaml)")); + assert!(out.contains("modified notes.note.finland.fields.field.capital")); +} + +#[test] +fn explain_reports_json_for_ui_consumers() { + let dir = temp_dir("explain-json"); + write_manifest_workspace(&dir); + fs::write(dir.join("brainbrew.yaml"), MANIFEST_WITH_PACKAGE_YAML).unwrap(); + + let output = run([ + "explain", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "patched-via-dependency", + "--json", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let json: serde_json::Value = serde_json::from_str(&stdout(&output)).unwrap(); + assert_eq!(json["package"]["id"], "anki-geo.ultimate-geography"); + assert_eq!(json["target"], "patched-via-dependency"); + assert_eq!(json["overlay_stack"][0]["id"], "patch.capital"); + assert_eq!( + json["changes"][0]["path"], + "notes.note.finland.fields.field.capital" + ); +} + +#[test] +fn explain_reports_json_conflicts_for_ui_consumers() { + let dir = temp_dir("explain-conflict-json"); + write_manifest_workspace(&dir); + fs::write(dir.join("second.yaml"), SECOND_CAPITAL_OVERLAY_YAML).unwrap(); + fs::write(dir.join("brainbrew.yaml"), CONFLICT_MANIFEST_YAML).unwrap(); + + let output = run([ + "explain", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "conflict", + "--json", + ]); + + assert!(!output.status.success()); + let json: serde_json::Value = serde_json::from_str(&stdout(&output)).unwrap(); + assert_eq!(json["target"], "conflict"); + assert_eq!(json["overlay_stack"][1]["id"], "patch.capital.second"); + assert_eq!(json["errors"][0]["kind"], "Conflict"); + assert_eq!( + json["errors"][0]["path"], + "notes.note.finland.fields.field.capital" + ); +} + +#[test] +fn explain_reports_overlay_conflicts_with_stack_context() { + let dir = temp_dir("explain-conflict"); + write_manifest_workspace(&dir); + fs::write(dir.join("second.yaml"), SECOND_CAPITAL_OVERLAY_YAML).unwrap(); + fs::write(dir.join("brainbrew.yaml"), CONFLICT_MANIFEST_YAML).unwrap(); + + let output = run([ + "explain", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "conflict", + ]); + + assert!(!output.status.success()); + assert!(stdout(&output).contains("2. patch.capital.second (second.yaml)")); + assert!(stderr(&output).contains("conflicts with earlier overlay")); +} + +#[test] +fn diff_can_emit_note_field_changes_as_overlay() { + let dir = temp_dir("diff-as-overlay"); + let left = dir.join("left.yaml"); + let right = dir.join("right.yaml"); + fs::write(&left, SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + &right, + SAMPLE_CANONICAL_YAML.replace("field.capital: Helsinki", "field.capital: Helsingfors"), + ) + .unwrap(); + + let output = run([ + "diff", + left.to_str().unwrap(), + right.to_str().unwrap(), + "--as-overlay", + "--id", + "overlay.patch.capital", + "--kind", + "patch", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("id: overlay.patch.capital")); + assert!(out.contains("kind: patch")); + assert!(out.contains("field.capital:")); + assert!(out.contains("value: Helsingfors")); + assert!(out.contains("expected_base:\n value: Helsinki")); +} + +#[test] +fn diff_as_overlay_emits_tag_and_adapter_id_changes() { + let dir = temp_dir("diff-as-overlay-tags"); + let left = dir.join("left.yaml"); + let right = dir.join("right.yaml"); + fs::write(&left, SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + &right, + SAMPLE_CANONICAL_YAML + .replace(" - Nordic", " - Baltic") + .replace("ug-finland-guid", "ug-finland-guid-v2"), + ) + .unwrap(); + + let output = run([ + "diff", + left.to_str().unwrap(), + right.to_str().unwrap(), + "--as-overlay", + "--id", + "overlay.patch.tags", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains(" Baltic:\n intent: add")); + assert!( + out.contains( + " Nordic:\n intent: remove\n expected_base: entity_present" + ) + ); + assert!(out.contains(" crowdanki:guid:\n intent: replace")); + assert!(out.contains(" value: ug-finland-guid-v2")); +} + +#[test] +fn diff_as_overlay_emits_media_reference_changes() { + let dir = temp_dir("diff-as-overlay-media"); + let left = dir.join("left.yaml"); + let right = dir.join("right.yaml"); + fs::write(&left, SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + &right, + SAMPLE_CANONICAL_YAML.replace( + "tombstones: []", + " media.flags-se-png:\n path: flags/se.png\n sha256: ''\ntombstones: []", + ), + ) + .unwrap(); + + let output = run([ + "diff", + left.to_str().unwrap(), + right.to_str().unwrap(), + "--as-overlay", + "--id", + "overlay.patch.media", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("media:\n media.flags-se-png:\n intent: add")); + assert!(out.contains(" path: flags/se.png")); +} + +#[test] +fn diff_as_overlay_emits_note_additions_and_removals() { + let dir = temp_dir("diff-as-overlay-notes"); + let left = dir.join("left.yaml"); + let added = dir.join("added.yaml"); + let removed = dir.join("removed.yaml"); + fs::write(&left, SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + &added, + SAMPLE_CANONICAL_YAML.replace( + "media:\n media.flags-fi-png:", + " note.sweden:\n note_type_id: note-type.country\n fields:\n field.capital: Stockholm\n field.country: Sweden\n field.flag: ''\n tags:\n - Europe\n - Nordic\n adapter_ids:\n crowdanki:guid: ug-sweden-guid\nmedia:\n media.flags-fi-png:", + ), + ) + .unwrap(); + fs::write(&removed, SAMPLE_WITHOUT_NOTES_CANONICAL_YAML).unwrap(); + + let add_output = run([ + "diff", + left.to_str().unwrap(), + added.to_str().unwrap(), + "--as-overlay", + "--id", + "overlay.patch.note-add", + ]); + assert!( + add_output.status.success(), + "stderr: {}", + stderr(&add_output) + ); + assert!(stdout(&add_output).contains(" note.sweden:\n intent: add")); + assert!(stdout(&add_output).contains(" note:\n note_type_id: note-type.country")); + + let remove_output = run([ + "diff", + left.to_str().unwrap(), + removed.to_str().unwrap(), + "--as-overlay", + "--id", + "overlay.patch.note-remove", + ]); + assert!( + remove_output.status.success(), + "stderr: {}", + stderr(&remove_output) + ); + assert!( + stdout(&remove_output) + .contains(" note.finland:\n intent: remove\n expected_base: entity_present") + ); +} + +#[test] +fn diff_reports_json_changes_by_stable_path() { + let dir = temp_dir("diff-json"); + let left = dir.join("left.yaml"); + let right = dir.join("right.yaml"); + fs::write(&left, SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + &right, + SAMPLE_CANONICAL_YAML.replace("field.capital: Helsinki", "field.capital: Helsingfors"), + ) + .unwrap(); + + let output = run([ + "diff", + left.to_str().unwrap(), + right.to_str().unwrap(), + "--json", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + assert!(stdout(&output).contains("\"path\": \"notes.note.finland.fields.field.capital\"")); + assert!(stdout(&output).contains("\"after\": \"Helsingfors\"")); +} + +#[test] +fn diff_reports_human_readable_before_and_after_values() { + let dir = temp_dir("diff-human"); + let left = dir.join("left.yaml"); + let right = dir.join("right.yaml"); + fs::write(&left, SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + &right, + SAMPLE_CANONICAL_YAML + .replace("field.capital: Helsinki", "field.capital: Helsingfors") + .replace("field.country: Finland", "field.country: Suomi"), + ) + .unwrap(); + + let output = run(["diff", left.to_str().unwrap(), right.to_str().unwrap()]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("2 semantic changes")); + assert!(out.contains("~ notes.note.finland.fields.field.capital")); + assert!(out.contains("- Helsinki")); + assert!(out.contains("+ Helsingfors")); + assert!(out.contains("~ notes.note.finland.fields.field.country")); + assert!(out.contains("- Finland")); + assert!(out.contains("+ Suomi")); +} + +#[test] +fn export_and_import_crowdanki_deck_folder() { + let dir = temp_dir("crowdanki-roundtrip"); + let deck_path = dir.join("deck.yaml"); + let export_dir = dir.join("crowdanki"); + let imported_path = dir.join("imported.yaml"); + fs::write(&deck_path, SAMPLE_CANONICAL_YAML).unwrap(); + + let export_output = run([ + "export", + "crowdanki", + deck_path.to_str().unwrap(), + "--out", + export_dir.to_str().unwrap(), + ]); + assert!( + export_output.status.success(), + "stderr: {}", + stderr(&export_output) + ); + assert!( + fs::read_to_string(export_dir.join("deck.json")) + .unwrap() + .contains("ug-finland-guid") + ); + + let import_output = run([ + "import", + "crowdanki", + export_dir.to_str().unwrap(), + "--accept-suggested-ids", + "--out", + imported_path.to_str().unwrap(), + ]); + + assert!( + import_output.status.success(), + "stderr: {}", + stderr(&import_output) + ); + assert!( + fs::read_to_string(imported_path) + .unwrap() + .contains("id: deck.ultimate-geography") + ); +} + +fn run(args: [&str; N]) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_brainbrew")) + .args(args) + .output() + .expect("command runs") +} + +fn run_with_cache(args: [&str; N], cache: &Path) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_brainbrew")) + .args(args) + .env("BRAINBREW_CACHE_DIR", cache) + .output() + .expect("command runs") +} + +fn write_manifest_workspace(dir: &Path) { + fs::write(dir.join("deck.yaml"), SAMPLE_CANONICAL_YAML).unwrap(); + fs::write(dir.join("capital.yaml"), CAPITAL_OVERLAY_YAML).unwrap(); + fs::write(dir.join("noop.yaml"), NOOP_OVERLAY_YAML).unwrap(); + fs::write(dir.join("brainbrew.yaml"), MANIFEST_YAML).unwrap(); +} + +fn write_tar_gz(path: &Path, root_name: &str, source_dir: &Path) { + let file = fs::File::create(path).unwrap(); + let encoder = GzEncoder::new(file, Compression::default()); + let mut archive = Builder::new(encoder); + archive.append_dir_all(root_name, source_dir).unwrap(); + let encoder = archive.into_inner().unwrap(); + encoder.finish().unwrap(); +} + +fn stdout(output: &std::process::Output) -> String { + String::from_utf8_lossy(&output.stdout).into_owned() +} + +fn stderr(output: &std::process::Output) -> String { + String::from_utf8_lossy(&output.stderr).into_owned() +} + +fn temp_dir(name: &str) -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("{name}-{unique}")); + fs::create_dir_all(&path).unwrap(); + path +} + +const CAPITAL_OVERLAY_YAML: &str = r#"id: overlay.patch.capital +kind: patch +notes: + note.finland: + intent: merge + fields: + field.capital: + intent: replace + value: Helsingfors + expected_base: + value: Helsinki +"#; + +const MESSY_OVERLAY_YAML: &str = r#"kind: patch +id: overlay.patch.capital +notes: + note.finland: + fields: + field.capital: + expected_base: + value: Helsinki + value: Helsingfors + intent: replace + intent: merge +"#; + +const NOOP_OVERLAY_YAML: &str = r#"id: overlay.noop +kind: patch +"#; + +const SECOND_CAPITAL_OVERLAY_YAML: &str = r#"id: overlay.patch.capital.second +kind: patch +notes: + note.finland: + intent: merge + fields: + field.capital: + intent: replace + value: Helsinki City + expected_base: + value: Helsinki +"#; + +const MANIFEST_YAML: &str = r#"base: deck.yaml +overlays: + noop.after-capital: + file: noop.yaml + kind: patch + depends_on: + - patch.capital + patch.capital: + file: capital.yaml + kind: patch +targets: + patched-via-dependency: + overlays: + - noop.after-capital +"#; + +const MANIFEST_WITH_EXPORTS_YAML: &str = r#"base: deck.yaml +overlays: + noop.after-capital: + file: noop.yaml + kind: patch + depends_on: + - patch.capital + patch.capital: + file: capital.yaml + kind: patch +targets: + patched-via-dependency: + overlays: + - noop.after-capital + exports: + crowdanki: + out: configured-crowdanki + golden: goldens/patched/deck.json +"#; + +const MANIFEST_WITH_PACKAGE_YAML: &str = r#"package: + id: anki-geo.ultimate-geography + version: 0.1.0 + compatible_base_versions: + - '>=0.1,<0.2' + depends_on: + - anki-geo.shared-geography +base: deck.yaml +overlays: + noop.after-capital: + file: noop.yaml + kind: patch + depends_on: + - patch.capital + patch.capital: + file: capital.yaml + kind: patch +targets: + patched-via-dependency: + overlays: + - noop.after-capital +"#; + +const CONFLICT_MANIFEST_YAML: &str = r#"base: deck.yaml +overlays: + patch.capital: + file: capital.yaml + kind: patch + patch.capital.second: + file: second.yaml + kind: patch +targets: + conflict: + overlays: + - patch.capital + - patch.capital.second +"#; + +const SIMPLE_MEDIA_MANIFEST_YAML: &str = r#"base: deck.yaml +overlays: {} +targets: + base: + overlays: [] +"#; + +const MESSY_MANIFEST_YAML: &str = r#"targets: + patched-via-dependency: + overlays: [noop.after-capital] +overlays: + patch.capital: + kind: patch + file: capital.yaml + noop.after-capital: + depends_on: [patch.capital] + kind: patch + file: noop.yaml +base: deck.yaml +"#; + +const LOCK_YAML: &str = r#"version: 1 +packages: + anki-geo.ultimate-geography: + manifest: brainbrew.yaml + package: + version: 0.1.0 + original: + type: git + url: https://github.com/anki-geo/ultimate-geography.git + ref: main + locked: + type: git + url: https://github.com/anki-geo/ultimate-geography.git + rev: ccf150a1b21e + nar_hash: sha256-example +"#; + +const MESSY_LOCK_YAML: &str = r#"packages: + anki-geo.ultimate-geography: + locked: + nar_hash: sha256-example + rev: ccf150a1b21e + url: https://github.com/anki-geo/ultimate-geography.git + type: git + original: + ref: main + url: https://github.com/anki-geo/ultimate-geography.git + type: git + package: + version: 0.1.0 + manifest: brainbrew.yaml +version: 1 +"#; + +const MESSY_CANONICAL_YAML: &str = r#"deck: + description: A geography deck fixture. + id: deck.ultimate-geography + name: Ultimate Geography + adapter_ids: + crowdanki:uuid: 43c5ba66-9a65-11e8-90c9-a0481cc15658 +note_types: + note-type.country: + adapter_ids: + crowdanki:uuid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa + name: Country + styling: | + .card { font-family: sans-serif; } + field_order: [field.country, field.capital, field.flag] + fields: + field.flag: { name: Flag } + field.capital: { name: Capital } + field.country: { name: Country } + card_template_order: [template.country-capital] + card_templates: + template.country-capital: + adapter_ids: {} + name: Country - Capital + question_format: '{{Country}}' + answer_format: '{{FrontSide}}
{{Capital}}' +notes: + note.finland: + adapter_ids: + crowdanki:guid: ug-finland-guid + fields: + field.flag: '' + field.capital: Helsinki + field.country: Finland + note_type_id: note-type.country + tags: [Europe, Nordic] +media: + media.flags-fi-png: + path: flags/fi.png + sha256: '' +tombstones: [] +"#; + +const SAMPLE_WITHOUT_NOTES_CANONICAL_YAML: &str = r#"deck: + id: deck.ultimate-geography + name: Ultimate Geography + description: A geography deck fixture. + adapter_ids: + crowdanki:uuid: 43c5ba66-9a65-11e8-90c9-a0481cc15658 +note_types: + note-type.country: + name: Country + field_order: + - field.country + - field.capital + - field.flag + fields: + field.capital: + name: Capital + field.country: + name: Country + field.flag: + name: Flag + card_template_order: + - template.country-capital + card_templates: + template.country-capital: + name: Country - Capital + question_format: '{{Country}}' + answer_format: '{{FrontSide}}
{{Capital}}' + adapter_ids: {} + styling: | + .card { font-family: sans-serif; } + adapter_ids: + crowdanki:uuid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa +notes: {} +media: + media.flags-fi-png: + path: flags/fi.png + sha256: '' +tombstones: [] +"#; + +const MEDIA_CANONICAL_YAML: &str = r#"deck: + id: deck.media-fixture + name: Media Fixture + description: A deck with media. + adapter_ids: + crowdanki:uuid: media-deck-uuid +note_types: + note-type.country: + name: Country + field_order: + - field.country + - field.flag + fields: + field.country: + name: Country + field.flag: + name: Flag + card_template_order: + - template.country-flag + card_templates: + template.country-flag: + name: Country - Flag + question_format: '{{Country}}' + answer_format: '{{FrontSide}}
{{Flag}}' + adapter_ids: {} + styling: | + .card { font-family: sans-serif; } + adapter_ids: + crowdanki:uuid: media-note-type-uuid +notes: + note.finland: + note_type_id: note-type.country + fields: + field.country: Finland + field.flag: '' + tags: + - Media + adapter_ids: + crowdanki:guid: media-fi-guid +media: + media.flags-fi-png: + path: flags/fi.png + sha256: 14873f4faae48052921f9272d948a369f775b2406e57a9b8d55fb94452b73948 +tombstones: [] +"#; + +const SAMPLE_CANONICAL_YAML: &str = r#"deck: + id: deck.ultimate-geography + name: Ultimate Geography + description: A geography deck fixture. + adapter_ids: + crowdanki:uuid: 43c5ba66-9a65-11e8-90c9-a0481cc15658 +note_types: + note-type.country: + name: Country + field_order: + - field.country + - field.capital + - field.flag + fields: + field.capital: + name: Capital + field.country: + name: Country + field.flag: + name: Flag + card_template_order: + - template.country-capital + card_templates: + template.country-capital: + name: Country - Capital + question_format: '{{Country}}' + answer_format: '{{FrontSide}}
{{Capital}}' + adapter_ids: {} + styling: | + .card { font-family: sans-serif; } + adapter_ids: + crowdanki:uuid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa +notes: + note.finland: + note_type_id: note-type.country + fields: + field.capital: Helsinki + field.country: Finland + field.flag: '' + tags: + - Europe + - Nordic + adapter_ids: + crowdanki:guid: ug-finland-guid +media: + media.flags-fi-png: + path: flags/fi.png + sha256: '' +tombstones: [] +"#; diff --git a/crates/brain-brew-cli/tests/ug_style_fixture.rs b/crates/brain-brew-cli/tests/ug_style_fixture.rs new file mode 100644 index 0000000..d1d6244 --- /dev/null +++ b/crates/brain-brew-cli/tests/ug_style_fixture.rs @@ -0,0 +1,158 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +use brain_brew_core::StableId; +use brain_brew_formats::{canonical_yaml, media}; + +#[test] +fn ug_style_fixture_composes_exports_imports_and_diffs_semantically() { + let fixture = fixture_dir(); + let dir = temp_dir("ug-style-fixture"); + let resolved_path = dir.join("resolved.yaml"); + let export_dir = dir.join("crowdanki"); + let imported_path = dir.join("imported.yaml"); + + let targets_output = run([ + "targets", + "--manifest", + fixture.join("brainbrew.yaml").to_str().unwrap(), + ]); + assert!( + targets_output.status.success(), + "stderr: {}", + stderr(&targets_output) + ); + assert_eq!(stdout(&targets_output), "full-demo\n"); + + let verify_output = run([ + "verify", + "--manifest", + fixture.join("brainbrew.yaml").to_str().unwrap(), + "--all-targets", + ]); + assert!( + verify_output.status.success(), + "stderr: {}", + stderr(&verify_output) + ); + + let compose_output = run([ + "compose", + "--manifest", + fixture.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "full-demo", + "--out", + resolved_path.to_str().unwrap(), + ]); + assert!( + compose_output.status.success(), + "stderr: {}", + stderr(&compose_output) + ); + + let validate_output = run(["validate", resolved_path.to_str().unwrap()]); + assert!( + validate_output.status.success(), + "stderr: {}", + stderr(&validate_output) + ); + + let export_output = run([ + "export", + "crowdanki", + resolved_path.to_str().unwrap(), + "--out", + export_dir.to_str().unwrap(), + ]); + assert!( + export_output.status.success(), + "stderr: {}", + stderr(&export_output) + ); + assert!(stdout(&export_output).contains("omitted tombstones: note.australia")); + + let deck_json = fs::read_to_string(export_dir.join("deck.json")).unwrap(); + let crowdanki: serde_json::Value = serde_json::from_str(&deck_json).unwrap(); + assert_eq!(crowdanki["notes"].as_array().unwrap().len(), 24); + assert_eq!(crowdanki["media_files"].as_array().unwrap().len(), 50); + assert_eq!( + crowdanki["note_models"][0]["flds"] + .as_array() + .unwrap() + .len(), + 10 + ); + assert_eq!( + crowdanki["note_models"][0]["tmpls"] + .as_array() + .unwrap() + .len(), + 6 + ); + assert!(deck_json.contains("Amsterdam (constitutional capital)")); + assert!(deck_json.contains("Starts with H")); + assert!(!deck_json.contains("ug-australia-guid")); + + let import_output = run([ + "import", + "crowdanki", + export_dir.to_str().unwrap(), + "--accept-suggested-ids", + "--out", + imported_path.to_str().unwrap(), + ]); + assert!( + import_output.status.success(), + "stderr: {}", + stderr(&import_output) + ); + + let mut expected_export_projection = + canonical_yaml::from_str(&fs::read_to_string(resolved_path).unwrap()).unwrap(); + media::validate_references(&expected_export_projection) + .expect("fixture media references validate"); + expected_export_projection + .notes + .remove(&sid("note.australia")); + expected_export_projection.tombstones.clear(); + let imported = canonical_yaml::from_str(&fs::read_to_string(imported_path).unwrap()).unwrap(); + + let diff = expected_export_projection.semantic_diff(&imported); + assert!(diff.is_empty(), "unexpected semantic diff: {diff:#?}"); +} + +fn run(args: [&str; N]) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_brainbrew")) + .args(args) + .output() + .expect("command runs") +} + +fn stdout(output: &std::process::Output) -> String { + String::from_utf8_lossy(&output.stdout).into_owned() +} + +fn stderr(output: &std::process::Output) -> String { + String::from_utf8_lossy(&output.stderr).into_owned() +} + +fn temp_dir(name: &str) -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("{name}-{unique}")); + fs::create_dir_all(&path).unwrap(); + path +} + +fn fixture_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/ug-style") +} + +fn sid(value: &str) -> StableId { + StableId::new(value).expect("test stable id is valid") +} diff --git a/crates/brain-brew-core/tests/canonical_deck_validation.rs b/crates/brain-brew-core/tests/canonical_deck_validation.rs new file mode 100644 index 0000000..3fc6b91 --- /dev/null +++ b/crates/brain-brew-core/tests/canonical_deck_validation.rs @@ -0,0 +1,192 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use brain_brew_core::{ + AdapterIds, CanonicalDeck, CardTemplate, FieldDefinition, MediaReference, Note, NoteType, + StableId, ValidationErrorKind, +}; + +#[test] +fn complete_deck_preserves_anki_compatible_structure_and_adapter_identities() { + let deck = ug_style_deck(); + + assert!(deck.validate().is_ok()); + + let note_type = deck + .note_types + .get(&sid("note-type.country")) + .expect("note type exists"); + assert_eq!(note_type.fields[0].id, sid("field.country")); + assert_eq!(note_type.fields[1].id, sid("field.capital")); + assert_eq!( + note_type.card_templates[0].id, + sid("template.country-to-capital") + ); + assert_eq!(note_type.card_templates[0].question_format, "{{Country}}"); + assert_eq!( + note_type.card_templates[0].answer_format, + "{{FrontSide}}
{{Capital}}" + ); + assert_eq!(note_type.styling, ".card { font-family: sans-serif; }\n"); + assert_eq!( + note_type.adapter_ids.get("crowdanki:model_id"), + Some("1548959259107") + ); + + let note = deck.notes.get(&sid("note.finland")).expect("note exists"); + assert_eq!(note.note_type_id, sid("note-type.country")); + assert_eq!( + note.fields.get(&sid("field.country")), + Some(&"Finland".to_owned()) + ); + assert!(note.tags.contains("Europe")); + assert_eq!( + note.adapter_ids.get("crowdanki:guid"), + Some("ug-finland-guid") + ); + + let flag = deck + .media + .get(&sid("media.flag.finland")) + .expect("media exists"); + assert_eq!(flag.path, "flags/fi.png"); + assert_eq!(flag.sha256, "0123456789abcdef"); +} + +#[test] +fn validation_rejects_note_with_missing_note_type() { + let mut deck = ug_style_deck(); + deck.notes + .get_mut(&sid("note.finland")) + .unwrap() + .note_type_id = sid("note-type.missing"); + + let report = deck + .validate() + .expect_err("missing note type must fail validation"); + + assert!(report.has_kind(ValidationErrorKind::MissingNoteType)); + assert!( + report + .errors + .iter() + .any(|error| error.path == "notes.note.finland.note_type_id") + ); +} + +#[test] +fn validation_rejects_note_fields_not_defined_by_its_note_type() { + let mut deck = ug_style_deck(); + deck.notes + .get_mut(&sid("note.finland")) + .unwrap() + .fields + .insert(sid("field.population"), "5.6 million".to_owned()); + + let report = deck + .validate() + .expect_err("unknown field must fail validation"); + + assert!(report.has_kind(ValidationErrorKind::UnknownNoteField)); + assert!( + report + .errors + .iter() + .any(|error| error.path == "notes.note.finland.fields.field.population") + ); +} + +#[test] +fn validation_rejects_note_missing_required_field() { + let mut deck = ug_style_deck(); + deck.notes + .get_mut(&sid("note.finland")) + .unwrap() + .fields + .remove(&sid("field.capital")); + + let report = deck + .validate() + .expect_err("missing field must fail validation"); + + assert!(report.has_kind(ValidationErrorKind::MissingNoteField)); + assert!( + report + .errors + .iter() + .any(|error| error.path == "notes.note.finland.fields.field.capital") + ); +} + +fn ug_style_deck() -> CanonicalDeck { + let mut note_type_adapter_ids = AdapterIds::new(); + note_type_adapter_ids.insert("crowdanki:model_id", "1548959259107"); + + let note_type = NoteType { + id: sid("note-type.country"), + name: "Ultimate Geography Country".to_owned(), + variables: BTreeMap::new(), + fields: vec![ + FieldDefinition { + id: sid("field.country"), + name: "Country".to_owned(), + }, + FieldDefinition { + id: sid("field.capital"), + name: "Capital".to_owned(), + }, + FieldDefinition { + id: sid("field.flag"), + name: "Flag".to_owned(), + }, + ], + card_templates: vec![CardTemplate { + id: sid("template.country-to-capital"), + name: "Country - Capital".to_owned(), + variables: BTreeMap::new(), + question_format: "{{Country}}".to_owned(), + answer_format: "{{FrontSide}}
{{Capital}}".to_owned(), + adapter_ids: AdapterIds::new(), + }], + styling: ".card { font-family: sans-serif; }\n".to_owned(), + adapter_ids: note_type_adapter_ids, + }; + + let mut note_adapter_ids = AdapterIds::new(); + note_adapter_ids.insert("crowdanki:guid", "ug-finland-guid"); + + let note = Note { + id: sid("note.finland"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Finland".to_owned()), + (sid("field.capital"), "Helsinki".to_owned()), + (sid("field.flag"), "".to_owned()), + ]), + tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), + adapter_ids: note_adapter_ids, + }; + + CanonicalDeck { + id: sid("deck.ultimate-geography"), + name: "Ultimate Geography".to_owned(), + description: "A geography deck fixture.".to_owned(), + variables: BTreeMap::new(), + note_types: BTreeMap::from([(note_type.id.clone(), note_type)]), + notes: BTreeMap::from([(note.id.clone(), note)]), + media: BTreeMap::from([( + sid("media.flag.finland"), + MediaReference { + id: sid("media.flag.finland"), + path: "flags/fi.png".to_owned(), + sha256: "0123456789abcdef".to_owned(), + }, + )]), + tombstones: BTreeSet::new(), + adapter_ids: AdapterIds::new(), + } +} + +fn sid(value: &str) -> StableId { + StableId::new(value).expect("test stable id is valid") +} diff --git a/crates/brain-brew-core/tests/overlay_compose.rs b/crates/brain-brew-core/tests/overlay_compose.rs new file mode 100644 index 0000000..64ea7f5 --- /dev/null +++ b/crates/brain-brew-core/tests/overlay_compose.rs @@ -0,0 +1,963 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use brain_brew_core::{ + AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplate, CardTemplateChange, ChangeIntent, + ComposeErrorKind, DeckChange, ExpectedBase, FieldChange, FieldDefinition, + FieldDefinitionChange, MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, + Overlay, OverlayKind, PropertyChange, StableId, TagChange, TranslationChange, + TranslationDictionary, +}; + +#[test] +fn add_overlay_adds_a_new_note_to_the_resolved_deck() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.extension.nordics"), + kind: OverlayKind::Extension, + translations: None, + deck_change: None, + note_changes: BTreeMap::from([( + sid("note.sweden"), + NoteChange { + intent: ChangeIntent::Add, + note: Some(sweden_note()), + variables: BTreeMap::new(), + fields: BTreeMap::new(), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("overlay composes"); + + assert!(resolved.notes.contains_key(&sid("note.finland"))); + assert_eq!( + resolved + .notes + .get(&sid("note.sweden")) + .unwrap() + .fields + .get(&sid("field.capital")), + Some(&"Stockholm".to_owned()) + ); +} + +#[test] +fn extension_overlay_can_add_a_note_type_and_notes_using_it() { + let base = ug_style_deck(); + let region_note_type = NoteType { + id: sid("note-type.region"), + name: "Geography Region".to_owned(), + variables: BTreeMap::from([("label.region".to_owned(), "Region".to_owned())]), + fields: vec![ + FieldDefinition { + id: sid("field.region"), + name: "Region".to_owned(), + }, + FieldDefinition { + id: sid("field.map"), + name: "Map".to_owned(), + }, + ], + card_templates: vec![CardTemplate { + id: sid("template.region-map"), + name: "Region - Map".to_owned(), + variables: BTreeMap::new(), + question_format: "{{Region}}".to_owned(), + answer_format: "{{Map}}".to_owned(), + adapter_ids: AdapterIds::new(), + }], + styling: ".card { font-family: sans-serif; }\n".to_owned(), + adapter_ids: AdapterIds::new(), + }; + let overlay = Overlay { + id: sid("overlay.extension.regions"), + kind: OverlayKind::Extension, + translations: None, + deck_change: None, + note_type_changes: BTreeMap::from([( + sid("note-type.region"), + NoteTypeChange { + intent: ChangeIntent::Add, + note_type: Some(region_note_type), + name: None, + variables: BTreeMap::new(), + styling: None, + fields: BTreeMap::new(), + card_templates: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + note_changes: BTreeMap::from([( + sid("note.europe"), + NoteChange { + intent: ChangeIntent::Add, + note: Some(Note { + id: sid("note.europe"), + note_type_id: sid("note-type.region"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.region"), "Europe".to_owned()), + (sid("field.map"), "".to_owned()), + ]), + tags: BTreeSet::from(["UG::Europe".to_owned()]), + adapter_ids: AdapterIds::new(), + }), + variables: BTreeMap::new(), + fields: BTreeMap::new(), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("extension composes"); + + assert!(resolved.note_types.contains_key(&sid("note-type.region"))); + assert_eq!( + resolved.notes[&sid("note.europe")].note_type_id, + sid("note-type.region") + ); +} + +#[test] +fn replace_field_requires_expected_base() { + let base = ug_style_deck(); + let overlay = overlay_replacing_capital(ChangeIntent::Replace, None, "Helsingfors"); + + let report = base + .compose(&[overlay]) + .expect_err("replace without expected base must fail"); + + assert!(report.has_kind(ComposeErrorKind::MissingExpectedBase)); + assert!( + report + .errors + .iter() + .any(|error| error.path == "notes.note.finland.fields.field.capital") + ); +} + +#[test] +fn replace_field_succeeds_when_expected_base_matches_current_value() { + let base = ug_style_deck(); + let overlay = overlay_replacing_capital( + ChangeIntent::Replace, + Some(ExpectedBase::Value("Helsinki".to_owned())), + "Helsingfors", + ); + + let resolved = base.compose(&[overlay]).expect("expected base matches"); + + assert_eq!( + resolved + .notes + .get(&sid("note.finland")) + .unwrap() + .fields + .get(&sid("field.capital")), + Some(&"Helsingfors".to_owned()) + ); +} + +#[test] +fn ordered_overlay_stack_reports_conflicts_without_explicit_override() { + let base = ug_style_deck(); + let first = overlay_replacing_capital(ChangeIntent::Merge, None, "Helsingfors"); + let second = overlay_replacing_capital(ChangeIntent::Merge, None, "Helsinki, Helsingfors"); + + let report = base + .compose(&[first, second]) + .expect_err("two non-override changes to the same path conflict"); + + assert!(report.has_kind(ComposeErrorKind::Conflict)); + assert!( + report + .errors + .iter() + .any(|error| error.path == "notes.note.finland.fields.field.capital") + ); +} + +#[test] +fn later_override_can_intentionally_replace_an_earlier_overlay_change() { + let base = ug_style_deck(); + let first = overlay_replacing_capital(ChangeIntent::Merge, None, "Helsingfors"); + let second = overlay_replacing_capital( + ChangeIntent::Override, + Some(ExpectedBase::Value("Helsingfors".to_owned())), + "Helsinki / Helsingfors", + ); + + let resolved = base + .compose(&[first, second]) + .expect("override resolves conflict explicitly"); + + assert_eq!( + resolved + .notes + .get(&sid("note.finland")) + .unwrap() + .fields + .get(&sid("field.capital")), + Some(&"Helsinki / Helsingfors".to_owned()) + ); +} + +#[test] +fn translation_dictionary_reports_stale_entries() { + let deck = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + changes: BTreeMap::from([( + "Missing source".to_owned(), + TranslationChange::Global("Mangler".to_owned()), + )]), + additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::new(), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = deck.compose(&[overlay]).expect_err("stale entry fails"); + + assert!(report.has_kind(ComposeErrorKind::StaleTranslationEntry)); + assert!( + report.errors[0] + .message + .contains("translation source \"Missing source\" did not match") + ); +} + +#[test] +fn translation_dictionary_can_scope_ambiguous_changes_by_path_and_add_blank_values() { + let mut base = ug_style_deck(); + base.notes + .get_mut(&sid("note.finland")) + .unwrap() + .fields + .insert(sid("field.country"), "Shared source".to_owned()); + let mut sweden = sweden_note(); + sweden + .fields + .insert(sid("field.country"), "Shared source".to_owned()); + sweden.fields.insert(sid("field.capital"), String::new()); + base.notes.insert(sid("note.sweden"), sweden); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + changes: BTreeMap::from([( + "Shared source".to_owned(), + TranslationChange::AtPaths(BTreeMap::from([ + ( + "notes.note.finland.fields.field.country".to_owned(), + "Finsk kontekst".to_owned(), + ), + ( + "notes.note.sweden.fields.field.country".to_owned(), + "Svensk kontekst".to_owned(), + ), + ])), + )]), + additions: BTreeMap::from([( + "notes.note.sweden.fields.field.capital".to_owned(), + "Stockholm".to_owned(), + )]), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::new(), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("translations compose"); + + assert_eq!( + resolved.notes[&sid("note.finland")].fields[&sid("field.country")], + "Finsk kontekst" + ); + assert_eq!( + resolved.notes[&sid("note.sweden")].fields[&sid("field.country")], + "Svensk kontekst" + ); + assert_eq!( + resolved.notes[&sid("note.sweden")].fields[&sid("field.capital")], + "Stockholm" + ); +} + +#[test] +fn translation_dictionary_can_translate_tags() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + changes: BTreeMap::from([ + ( + "Nordic".to_owned(), + TranslationChange::Global("UG::Nordic".to_owned()), + ), + ( + "Europe".to_owned(), + TranslationChange::AtPaths(BTreeMap::from([( + "notes.note.finland.tags.Europe".to_owned(), + "UG::Europe".to_owned(), + )])), + ), + ]), + additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::new(), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("tag translations compose"); + let note = &resolved.notes[&sid("note.finland")]; + + assert!(note.tags.contains("UG::Europe")); + assert!(note.tags.contains("UG::Nordic")); + assert!(!note.tags.contains("Europe")); + assert!(!note.tags.contains("Nordic")); +} + +#[test] +fn translation_dictionary_addition_fails_when_base_is_not_blank() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + changes: BTreeMap::new(), + additions: BTreeMap::from([( + "notes.note.finland.fields.field.capital".to_owned(), + "Helsinki translated".to_owned(), + )]), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::new(), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = base + .compose(&[overlay]) + .expect_err("addition expects blank base"); + + assert!(report.has_kind(ComposeErrorKind::ExpectedBaseMismatch)); + assert!(report.errors.iter().any(|error| { + error.path == "notes.note.finland.fields.field.capital" + && error.message.contains("expected blank value") + })); +} + +#[test] +fn extension_overlay_can_add_a_note_type_field_and_backfill_note_values() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.extension.population"), + kind: OverlayKind::Extension, + translations: None, + deck_change: None, + note_type_changes: BTreeMap::from([( + sid("note-type.country"), + NoteTypeChange { + intent: ChangeIntent::Merge, + note_type: None, + name: None, + variables: BTreeMap::new(), + styling: None, + fields: BTreeMap::from([( + sid("field.population"), + FieldDefinitionChange { + intent: ChangeIntent::Add, + field: Some(FieldDefinition { + id: sid("field.population"), + name: "Population".to_owned(), + }), + expected_base: None, + }, + )]), + card_templates: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + note_changes: BTreeMap::from([( + sid("note.finland"), + NoteChange { + intent: ChangeIntent::Merge, + note: None, + variables: BTreeMap::new(), + fields: BTreeMap::from([( + sid("field.population"), + FieldChange { + intent: ChangeIntent::Add, + value: Some("5.6 million".to_owned()), + expected_base: None, + }, + )]), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("extension composes"); + + let note_type = resolved.note_types.get(&sid("note-type.country")).unwrap(); + assert_eq!(note_type.fields.last().unwrap().id, sid("field.population")); + assert_eq!( + resolved + .notes + .get(&sid("note.finland")) + .unwrap() + .fields + .get(&sid("field.population")), + Some(&"5.6 million".to_owned()) + ); +} + +#[test] +fn extension_overlay_adds_blank_values_for_new_fields_without_explicit_values() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.extension.region-code"), + kind: OverlayKind::Extension, + translations: None, + deck_change: None, + note_type_changes: BTreeMap::from([( + sid("note-type.country"), + NoteTypeChange { + intent: ChangeIntent::Merge, + note_type: None, + name: None, + variables: BTreeMap::new(), + styling: None, + fields: BTreeMap::from([( + sid("field.region-code"), + FieldDefinitionChange { + intent: ChangeIntent::Add, + field: Some(FieldDefinition { + id: sid("field.region-code"), + name: "Region code".to_owned(), + }), + expected_base: None, + }, + )]), + card_templates: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + note_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("extension composes"); + + assert_eq!( + resolved.notes[&sid("note.finland")] + .fields + .get(&sid("field.region-code")), + Some(&String::new()) + ); + resolved + .validate() + .expect("new fields default to blank values on existing notes"); +} + +#[test] +fn metadata_overlay_can_replace_names_and_adapter_identities() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.translation.de"), + kind: OverlayKind::Translation, + translations: None, + deck_change: Some(DeckChange { + name: Some(PropertyChange { + intent: ChangeIntent::Replace, + value: Some("Ultimate Geography [DE]".to_owned()), + expected_base: Some(ExpectedBase::Value("Ultimate Geography".to_owned())), + }), + description: None, + variables: BTreeMap::new(), + adapter_ids: BTreeMap::from([( + "crowdanki:uuid".to_owned(), + AdapterIdChange { + intent: ChangeIntent::Replace, + value: Some("de-deck-uuid".to_owned()), + expected_base: Some(ExpectedBase::Value( + "43c5ba66-9a65-11e8-90c9-a0481cc15658".to_owned(), + )), + }, + )]), + }), + note_type_changes: BTreeMap::from([( + sid("note-type.country"), + NoteTypeChange { + intent: ChangeIntent::Merge, + note_type: None, + name: Some(PropertyChange { + intent: ChangeIntent::Replace, + value: Some("Ultimate Geography [DE]".to_owned()), + expected_base: Some(ExpectedBase::Value( + "Ultimate Geography Country".to_owned(), + )), + }), + variables: BTreeMap::new(), + styling: None, + fields: BTreeMap::new(), + card_templates: BTreeMap::from([( + sid("template.country-to-capital"), + CardTemplateChange { + intent: ChangeIntent::Merge, + template: None, + insert_after: None, + name: None, + variables: BTreeMap::new(), + question_format: None, + answer_format: None, + adapter_ids: BTreeMap::from([( + "crowdanki:ord".to_owned(), + AdapterIdChange { + intent: ChangeIntent::Add, + value: Some("0".to_owned()), + expected_base: None, + }, + )]), + expected_base: None, + }, + )]), + adapter_ids: BTreeMap::from([( + "crowdanki:model_id".to_owned(), + AdapterIdChange { + intent: ChangeIntent::Replace, + value: Some("de-model-id".to_owned()), + expected_base: Some(ExpectedBase::Value("1548959259107".to_owned())), + }, + )]), + expected_base: None, + }, + )]), + note_changes: BTreeMap::from([( + sid("note.finland"), + NoteChange { + intent: ChangeIntent::Merge, + note: None, + variables: BTreeMap::new(), + fields: BTreeMap::new(), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::from([( + "crowdanki:guid".to_owned(), + AdapterIdChange { + intent: ChangeIntent::Replace, + value: Some("ug-finland-de-guid".to_owned()), + expected_base: Some(ExpectedBase::Value("ug-finland-guid".to_owned())), + }, + )]), + expected_base: None, + }, + )]), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("metadata overlay composes"); + + assert_eq!(resolved.name, "Ultimate Geography [DE]"); + assert_eq!( + resolved.adapter_ids.get("crowdanki:uuid"), + Some("de-deck-uuid") + ); + let note_type = resolved.note_types.get(&sid("note-type.country")).unwrap(); + assert_eq!(note_type.name, "Ultimate Geography [DE]"); + assert_eq!( + note_type.adapter_ids.get("crowdanki:model_id"), + Some("de-model-id") + ); + assert_eq!( + note_type.card_templates[0].adapter_ids.get("crowdanki:ord"), + Some("0") + ); + assert_eq!( + resolved + .notes + .get(&sid("note.finland")) + .unwrap() + .adapter_ids + .get("crowdanki:guid"), + Some("ug-finland-de-guid") + ); +} + +#[test] +fn overlay_can_add_card_templates_in_order_and_replace_styling() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.extension.extended"), + kind: OverlayKind::Extension, + translations: None, + deck_change: None, + note_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + note_type_changes: BTreeMap::from([( + sid("note-type.country"), + NoteTypeChange { + intent: ChangeIntent::Merge, + note_type: None, + name: None, + variables: BTreeMap::new(), + styling: Some(PropertyChange { + intent: ChangeIntent::Replace, + value: Some(".card { font-family: serif; }\n".to_owned()), + expected_base: Some(ExpectedBase::Value( + ".card { font-family: sans-serif; }\n".to_owned(), + )), + }), + fields: BTreeMap::new(), + card_templates: BTreeMap::from([( + sid("template.country-to-flag"), + CardTemplateChange { + intent: ChangeIntent::Add, + template: Some(CardTemplate { + id: sid("template.country-to-flag"), + name: "Country - Flag".to_owned(), + variables: BTreeMap::new(), + question_format: "{{Country}}".to_owned(), + answer_format: "{{Flag}}".to_owned(), + adapter_ids: AdapterIds::new(), + }), + insert_after: Some(sid("template.country-to-capital")), + name: None, + variables: BTreeMap::new(), + question_format: None, + answer_format: None, + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + }; + + let resolved = base.compose(&[overlay]).expect("template overlay composes"); + let note_type = resolved.note_types.get(&sid("note-type.country")).unwrap(); + assert_eq!(note_type.styling, ".card { font-family: serif; }\n"); + assert_eq!( + note_type + .card_templates + .iter() + .map(|template| template.id.clone()) + .collect::>(), + vec![ + sid("template.country-to-capital"), + sid("template.country-to-flag") + ] + ); + assert_eq!(note_type.card_templates[1].answer_format, "{{Flag}}"); +} + +#[test] +fn overlay_can_change_note_tags_and_media_references() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.extension.media-tags"), + kind: OverlayKind::Extension, + translations: None, + deck_change: None, + note_type_changes: BTreeMap::new(), + note_changes: BTreeMap::from([( + sid("note.finland"), + NoteChange { + intent: ChangeIntent::Merge, + note: None, + variables: BTreeMap::new(), + fields: BTreeMap::new(), + tags: BTreeMap::from([ + ( + "UG::Nordic".to_owned(), + TagChange { + intent: ChangeIntent::Add, + expected_base: None, + }, + ), + ( + "Nordic".to_owned(), + TagChange { + intent: ChangeIntent::Remove, + expected_base: Some(ExpectedBase::EntityPresent), + }, + ), + ]), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + media_changes: BTreeMap::from([( + sid("media.flag.sweden"), + MediaChange { + intent: ChangeIntent::Add, + media: Some(MediaReference { + id: sid("media.flag.sweden"), + path: "flags/se.png".to_owned(), + sha256: "abcdef".to_owned(), + }), + expected_base: None, + }, + )]), + }; + + let resolved = base + .compose(&[overlay]) + .expect("tag/media overlay composes"); + let note = resolved.notes.get(&sid("note.finland")).unwrap(); + assert!(note.tags.contains("UG::Nordic")); + assert!(!note.tags.contains("Nordic")); + assert_eq!( + resolved.media.get(&sid("media.flag.sweden")).unwrap().path, + "flags/se.png" + ); +} + +#[test] +fn remove_overlay_can_tombstone_an_unused_note_type() { + let mut base = ug_style_deck(); + base.note_types.insert( + sid("note-type.region"), + NoteType { + id: sid("note-type.region"), + name: "Region".to_owned(), + variables: BTreeMap::new(), + fields: vec![FieldDefinition { + id: sid("field.region"), + name: "Region".to_owned(), + }], + card_templates: vec![CardTemplate { + id: sid("template.region"), + name: "Region".to_owned(), + variables: BTreeMap::new(), + question_format: "{{Region}}".to_owned(), + answer_format: "{{Region}}".to_owned(), + adapter_ids: AdapterIds::new(), + }], + styling: ".card {}\n".to_owned(), + adapter_ids: AdapterIds::new(), + }, + ); + let overlay = Overlay { + id: sid("overlay.patch.remove-region-type"), + kind: OverlayKind::Patch, + translations: None, + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::from([( + sid("note-type.region"), + NoteTypeChange { + intent: ChangeIntent::Remove, + note_type: None, + name: None, + variables: BTreeMap::new(), + styling: None, + fields: BTreeMap::new(), + card_templates: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: Some(ExpectedBase::EntityPresent), + }, + )]), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("remove composes"); + + assert!(!resolved.note_types.contains_key(&sid("note-type.region"))); + assert!(resolved.tombstones.contains(&sid("note-type.region"))); +} + +#[test] +fn remove_overlay_records_a_tombstone_without_erasing_the_entity_from_resolved_deck() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.patch.remove-finland"), + kind: OverlayKind::Patch, + translations: None, + deck_change: None, + note_changes: BTreeMap::from([( + sid("note.finland"), + NoteChange { + intent: ChangeIntent::Remove, + note: None, + variables: BTreeMap::new(), + fields: BTreeMap::new(), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: Some(ExpectedBase::EntityPresent), + }, + )]), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("remove composes"); + + assert!(resolved.notes.contains_key(&sid("note.finland"))); + assert!(resolved.tombstones.contains(&sid("note.finland"))); +} + +fn overlay_replacing_capital( + intent: ChangeIntent, + expected_base: Option, + value: &str, +) -> Overlay { + Overlay { + id: sid("overlay.patch.capital"), + kind: OverlayKind::Patch, + translations: None, + deck_change: None, + note_changes: BTreeMap::from([( + sid("note.finland"), + NoteChange { + intent: ChangeIntent::Merge, + note: None, + variables: BTreeMap::new(), + fields: BTreeMap::from([( + sid("field.capital"), + FieldChange { + intent, + value: Some(value.to_owned()), + expected_base, + }, + )]), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + }, + )]), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + } +} + +fn ug_style_deck() -> CanonicalDeck { + let mut note_type_adapter_ids = AdapterIds::new(); + note_type_adapter_ids.insert("crowdanki:model_id", "1548959259107"); + + let note_type = NoteType { + id: sid("note-type.country"), + name: "Ultimate Geography Country".to_owned(), + variables: BTreeMap::new(), + fields: vec![ + FieldDefinition { + id: sid("field.country"), + name: "Country".to_owned(), + }, + FieldDefinition { + id: sid("field.capital"), + name: "Capital".to_owned(), + }, + FieldDefinition { + id: sid("field.flag"), + name: "Flag".to_owned(), + }, + ], + card_templates: vec![CardTemplate { + id: sid("template.country-to-capital"), + name: "Country - Capital".to_owned(), + variables: BTreeMap::new(), + question_format: "{{Country}}".to_owned(), + answer_format: "{{FrontSide}}
{{Capital}}".to_owned(), + adapter_ids: AdapterIds::new(), + }], + styling: ".card { font-family: sans-serif; }\n".to_owned(), + adapter_ids: note_type_adapter_ids, + }; + + let mut note_adapter_ids = AdapterIds::new(); + note_adapter_ids.insert("crowdanki:guid", "ug-finland-guid"); + let note = Note { + id: sid("note.finland"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Finland".to_owned()), + (sid("field.capital"), "Helsinki".to_owned()), + (sid("field.flag"), "".to_owned()), + ]), + tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), + adapter_ids: note_adapter_ids, + }; + + let mut deck_adapter_ids = AdapterIds::new(); + deck_adapter_ids.insert("crowdanki:uuid", "43c5ba66-9a65-11e8-90c9-a0481cc15658"); + + CanonicalDeck { + id: sid("deck.ultimate-geography"), + name: "Ultimate Geography".to_owned(), + description: "A geography deck fixture.".to_owned(), + variables: BTreeMap::new(), + note_types: BTreeMap::from([(note_type.id.clone(), note_type)]), + notes: BTreeMap::from([(note.id.clone(), note)]), + media: BTreeMap::from([( + sid("media.flag.finland"), + MediaReference { + id: sid("media.flag.finland"), + path: "flags/fi.png".to_owned(), + sha256: "0123456789abcdef".to_owned(), + }, + )]), + tombstones: BTreeSet::new(), + adapter_ids: deck_adapter_ids, + } +} + +fn sweden_note() -> Note { + Note { + id: sid("note.sweden"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Sweden".to_owned()), + (sid("field.capital"), "Stockholm".to_owned()), + (sid("field.flag"), "".to_owned()), + ]), + tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), + adapter_ids: AdapterIds::new(), + } +} + +fn sid(value: &str) -> StableId { + StableId::new(value).expect("test stable id is valid") +} diff --git a/crates/brain-brew-core/tests/semantic_diff.rs b/crates/brain-brew-core/tests/semantic_diff.rs new file mode 100644 index 0000000..e47b9fc --- /dev/null +++ b/crates/brain-brew-core/tests/semantic_diff.rs @@ -0,0 +1,147 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use brain_brew_core::{ + AdapterIds, CanonicalDeck, CardTemplate, FieldDefinition, MediaReference, Note, NoteType, + SemanticChangeKind, StableId, +}; + +#[test] +fn unchanged_decks_have_empty_semantic_diff() { + let left = ug_style_deck(); + let right = ug_style_deck(); + + let diff = left.semantic_diff(&right); + + assert!(diff.is_empty()); +} + +#[test] +fn note_field_changes_are_reported_by_stable_id_path() { + let left = ug_style_deck(); + let mut right = ug_style_deck(); + right + .notes + .get_mut(&sid("note.finland")) + .unwrap() + .fields + .insert(sid("field.capital"), "Helsingfors".to_owned()); + + let diff = left.semantic_diff(&right); + + assert_eq!(diff.changes.len(), 1); + let change = &diff.changes[0]; + assert_eq!(change.kind, SemanticChangeKind::Modified); + assert_eq!(change.path, "notes.note.finland.fields.field.capital"); + assert_eq!(change.before.as_deref(), Some("Helsinki")); + assert_eq!(change.after.as_deref(), Some("Helsingfors")); +} + +#[test] +fn added_and_removed_notes_are_reported_by_stable_id_not_position() { + let left = ug_style_deck(); + let mut right = ug_style_deck(); + right.notes.remove(&sid("note.finland")); + right.notes.insert(sid("note.sweden"), sweden_note()); + + let diff = left.semantic_diff(&right); + + assert!(diff.has_change(SemanticChangeKind::Added, "notes.note.sweden")); + assert!(diff.has_change(SemanticChangeKind::Removed, "notes.note.finland")); +} + +#[test] +fn tombstones_are_distinct_semantic_changes() { + let left = ug_style_deck(); + let mut right = ug_style_deck(); + right.tombstones.insert(sid("note.finland")); + + let diff = left.semantic_diff(&right); + + assert!(diff.has_change(SemanticChangeKind::Tombstoned, "tombstones.note.finland")); +} + +fn ug_style_deck() -> CanonicalDeck { + let mut note_type_adapter_ids = AdapterIds::new(); + note_type_adapter_ids.insert("crowdanki:model_id", "1548959259107"); + + let note_type = NoteType { + id: sid("note-type.country"), + name: "Ultimate Geography Country".to_owned(), + variables: BTreeMap::new(), + fields: vec![ + FieldDefinition { + id: sid("field.country"), + name: "Country".to_owned(), + }, + FieldDefinition { + id: sid("field.capital"), + name: "Capital".to_owned(), + }, + FieldDefinition { + id: sid("field.flag"), + name: "Flag".to_owned(), + }, + ], + card_templates: vec![CardTemplate { + id: sid("template.country-to-capital"), + name: "Country - Capital".to_owned(), + variables: BTreeMap::new(), + question_format: "{{Country}}".to_owned(), + answer_format: "{{FrontSide}}
{{Capital}}".to_owned(), + adapter_ids: AdapterIds::new(), + }], + styling: ".card { font-family: sans-serif; }\n".to_owned(), + adapter_ids: note_type_adapter_ids, + }; + + let note = Note { + id: sid("note.finland"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Finland".to_owned()), + (sid("field.capital"), "Helsinki".to_owned()), + (sid("field.flag"), "".to_owned()), + ]), + tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), + adapter_ids: AdapterIds::new(), + }; + + CanonicalDeck { + id: sid("deck.ultimate-geography"), + name: "Ultimate Geography".to_owned(), + description: "A geography deck fixture.".to_owned(), + variables: BTreeMap::new(), + note_types: BTreeMap::from([(note_type.id.clone(), note_type)]), + notes: BTreeMap::from([(note.id.clone(), note)]), + media: BTreeMap::from([( + sid("media.flag.finland"), + MediaReference { + id: sid("media.flag.finland"), + path: "flags/fi.png".to_owned(), + sha256: "0123456789abcdef".to_owned(), + }, + )]), + tombstones: BTreeSet::new(), + adapter_ids: AdapterIds::new(), + } +} + +fn sweden_note() -> Note { + Note { + id: sid("note.sweden"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Sweden".to_owned()), + (sid("field.capital"), "Stockholm".to_owned()), + (sid("field.flag"), "".to_owned()), + ]), + tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), + adapter_ids: AdapterIds::new(), + } +} + +fn sid(value: &str) -> StableId { + StableId::new(value).expect("test stable id is valid") +} diff --git a/crates/brain-brew-formats/tests/canonical_yaml.rs b/crates/brain-brew-formats/tests/canonical_yaml.rs new file mode 100644 index 0000000..13ffbb8 --- /dev/null +++ b/crates/brain-brew-formats/tests/canonical_yaml.rs @@ -0,0 +1,319 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use brain_brew_core::{ + AdapterIds, CanonicalDeck, CardTemplate, FieldDefinition, MediaReference, Note, NoteType, + StableId, +}; +use brain_brew_formats::{canonical_yaml, crowdanki}; + +#[test] +fn emits_canonical_deck_yaml_with_explicit_order_arrays() { + let yaml = canonical_yaml::to_string(&ug_style_deck()).expect("deck emits"); + + assert_eq!(yaml, EXPECTED_CANONICAL_YAML); +} + +#[test] +fn parses_emitted_yaml_back_to_semantically_equal_deck() { + let original = ug_style_deck(); + let yaml = canonical_yaml::to_string(&original).expect("deck emits"); + + let parsed = canonical_yaml::from_str(&yaml).expect("emitted yaml parses"); + + assert_eq!(parsed, original); + assert!(parsed.semantic_diff(&original).is_empty()); +} + +#[test] +fn formatter_canonicalizes_valid_yaml_bytes() { + let messy_yaml = r#"deck: + description: A geography deck fixture. + id: deck.ultimate-geography + name: Ultimate Geography + adapter_ids: {} +note_types: + note-type.country: + adapter_ids: + crowdanki:model_id: '1548959259107' + name: Ultimate Geography Country + styling: | + .card { font-family: sans-serif; } + field_order: [field.country, field.capital, field.flag] + fields: + field.flag: { name: Flag } + field.capital: { name: Capital } + field.country: { name: Country } + card_template_order: + - template.country-to-capital + card_templates: + template.country-to-capital: + adapter_ids: {} + name: Country - Capital + question_format: '{{Country}}' + answer_format: '{{FrontSide}}
{{Capital}}' +notes: + note.finland: + adapter_ids: + crowdanki:guid: ug-finland-guid + fields: + field.flag: '' + field.capital: Helsinki + field.country: Finland + note_type_id: note-type.country + tags: [Europe, Nordic] +media: + media.flag.finland: + path: flags/fi.png + sha256: 0123456789abcdef +tombstones: [] +"#; + + let formatted = canonical_yaml::format_str(messy_yaml).expect("valid yaml formats"); + + assert_eq!(formatted, EXPECTED_CANONICAL_YAML); +} + +#[test] +fn translation_dictionary_overlay_translates_variables_fields_and_adapter_ids() { + let deck = canonical_yaml::from_str( + r#"deck: + id: deck.translation-fixture + name: Ultimate Geography + description: A geography deck fixture. + variables: + label.deck: Ultimate Geography + adapter_ids: + crowdanki:uuid: deck-en +note_types: + note-type.country: + name: Ultimate Geography + variables: + label.capital: Capital + field_order: + - field.country + - field.capital + fields: + field.capital: + name: Capital + field.country: + name: Country + card_template_order: + - template.country-capital + card_templates: + template.country-capital: + name: Country - Capital + question_format: '
${label.capital}
{{Country}}' + answer_format: '
${label.capital}: {{Capital}}
' + adapter_ids: {} + styling: '' + adapter_ids: + crowdanki:uuid: model-en +notes: + note.denmark: + note_type_id: note-type.country + fields: + field.capital: Copenhagen + field.country: Denmark + tags: [] + adapter_ids: + crowdanki:guid: note-guid-en +media: {} +tombstones: [] +"#, + ) + .expect("deck parses"); + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.translation.da +kind: translation +translations: + changes: + Copenhagen: København + Denmark: Danmark + Ultimate Geography: 'Ultimate Geography [DA]' + variables: + label.capital: + Capital: Hovedstad + adapter_ids: + crowdanki:guid: + note-guid-en: note-guid-da + crowdanki:uuid: + deck-en: deck-da + model-en: model-da +"#, + ) + .expect("translation overlay parses"); + + let translated = deck.compose(&[overlay]).expect("translation composes"); + assert_eq!(translated.name, "Ultimate Geography [DA]"); + let note_type = &translated.note_types[&sid("note-type.country")]; + assert_eq!(note_type.variables["label.capital"], "Hovedstad"); + assert_eq!( + note_type.adapter_ids.get("crowdanki:uuid"), + Some("model-da") + ); + let note = &translated.notes[&sid("note.denmark")]; + assert_eq!(note.fields[&sid("field.country")], "Danmark"); + assert_eq!(note.fields[&sid("field.capital")], "København"); + assert_eq!(note.adapter_ids.get("crowdanki:guid"), Some("note-guid-da")); + + let exported = crowdanki::export_deck(&translated).expect("translated deck exports"); + let json: serde_json::Value = serde_json::from_str(&exported.deck_json).unwrap(); + assert_eq!(json["crowdanki_uuid"], "deck-da"); + assert_eq!( + json["note_models"][0]["tmpls"][0]["qfmt"], + "
Hovedstad
{{Country}}" + ); + assert_eq!(json["notes"][0]["guid"], "note-guid-da"); +} + +#[test] +fn translation_dictionary_rejects_legacy_text_key() { + let error = canonical_yaml::overlay_from_str( + r#"id: overlay.translation.da +kind: translation +translations: + text: + Denmark: Danmark +"#, + ) + .expect_err("legacy text key is not accepted"); + + assert!(error.to_string().contains("text")); +} + +#[test] +fn parser_rejects_unknown_fields_in_canonical_yaml() { + let yaml = EXPECTED_CANONICAL_YAML.replace( + "name: Ultimate Geography\n", + "name: Ultimate Geography\n unsupported: true\n", + ); + + let error = canonical_yaml::from_str(&yaml).expect_err("unknown fields must fail"); + + assert!(error.to_string().contains("unsupported")); +} + +const EXPECTED_CANONICAL_YAML: &str = r#"deck: + id: deck.ultimate-geography + name: Ultimate Geography + description: A geography deck fixture. + adapter_ids: {} +note_types: + note-type.country: + name: Ultimate Geography Country + field_order: + - field.country + - field.capital + - field.flag + fields: + field.capital: + name: Capital + field.country: + name: Country + field.flag: + name: Flag + card_template_order: + - template.country-to-capital + card_templates: + template.country-to-capital: + name: Country - Capital + question_format: '{{Country}}' + answer_format: '{{FrontSide}}
{{Capital}}' + adapter_ids: {} + styling: | + .card { font-family: sans-serif; } + adapter_ids: + crowdanki:model_id: '1548959259107' +notes: + note.finland: + note_type_id: note-type.country + fields: + field.capital: Helsinki + field.country: Finland + field.flag: '' + tags: + - Europe + - Nordic + adapter_ids: + crowdanki:guid: ug-finland-guid +media: + media.flag.finland: + path: flags/fi.png + sha256: 0123456789abcdef +tombstones: [] +"#; + +fn ug_style_deck() -> CanonicalDeck { + let deck_adapter_ids = AdapterIds::new(); + let mut note_type_adapter_ids = AdapterIds::new(); + note_type_adapter_ids.insert("crowdanki:model_id", "1548959259107"); + + let note_type = NoteType { + id: sid("note-type.country"), + name: "Ultimate Geography Country".to_owned(), + variables: BTreeMap::new(), + fields: vec![ + FieldDefinition { + id: sid("field.country"), + name: "Country".to_owned(), + }, + FieldDefinition { + id: sid("field.capital"), + name: "Capital".to_owned(), + }, + FieldDefinition { + id: sid("field.flag"), + name: "Flag".to_owned(), + }, + ], + card_templates: vec![CardTemplate { + id: sid("template.country-to-capital"), + name: "Country - Capital".to_owned(), + variables: BTreeMap::new(), + question_format: "{{Country}}".to_owned(), + answer_format: "{{FrontSide}}
{{Capital}}".to_owned(), + adapter_ids: AdapterIds::new(), + }], + styling: ".card { font-family: sans-serif; }\n".to_owned(), + adapter_ids: note_type_adapter_ids, + }; + + let mut note_adapter_ids = AdapterIds::new(); + note_adapter_ids.insert("crowdanki:guid", "ug-finland-guid"); + + let note = Note { + id: sid("note.finland"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Finland".to_owned()), + (sid("field.capital"), "Helsinki".to_owned()), + (sid("field.flag"), "".to_owned()), + ]), + tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), + adapter_ids: note_adapter_ids, + }; + + CanonicalDeck { + id: sid("deck.ultimate-geography"), + name: "Ultimate Geography".to_owned(), + description: "A geography deck fixture.".to_owned(), + variables: BTreeMap::new(), + note_types: BTreeMap::from([(note_type.id.clone(), note_type)]), + notes: BTreeMap::from([(note.id.clone(), note)]), + media: BTreeMap::from([( + sid("media.flag.finland"), + MediaReference { + id: sid("media.flag.finland"), + path: "flags/fi.png".to_owned(), + sha256: "0123456789abcdef".to_owned(), + }, + )]), + tombstones: BTreeSet::new(), + adapter_ids: deck_adapter_ids, + } +} + +fn sid(value: &str) -> StableId { + StableId::new(value).expect("test stable id is valid") +} diff --git a/crates/brain-brew-formats/tests/crowdanki.rs b/crates/brain-brew-formats/tests/crowdanki.rs new file mode 100644 index 0000000..a10058f --- /dev/null +++ b/crates/brain-brew-formats/tests/crowdanki.rs @@ -0,0 +1,433 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use brain_brew_core::{ + AdapterIds, CanonicalDeck, CardTemplate, FieldDefinition, MediaReference, Note, NoteType, + StableId, +}; +use brain_brew_formats::crowdanki; + +#[test] +fn exports_deterministic_crowdanki_json_preserving_adapter_identities() { + let export = crowdanki::export_deck(&ug_style_deck()).expect("deck exports"); + + assert_eq!(export.deck_json, EXPECTED_CROWDANKI_JSON); + assert!(export.omitted_tombstones.is_empty()); +} + +#[test] +fn import_export_round_trip_is_semantically_equal_when_suggested_ids_match_source() { + let original = ug_style_deck(); + let export = crowdanki::export_deck(&original).expect("deck exports"); + + let imported = crowdanki::import_deck_accept_suggested_ids(&export.deck_json) + .expect("exported CrowdAnki imports"); + + assert!(original.semantic_diff(&imported).is_empty()); +} + +#[test] +fn import_preserves_crowdanki_adapter_identities() { + let imported = crowdanki::import_deck_accept_suggested_ids(EXPECTED_CROWDANKI_JSON) + .expect("CrowdAnki imports"); + + assert_eq!( + imported.adapter_ids.get("crowdanki:uuid"), + Some("43c5ba66-9a65-11e8-90c9-a0481cc15658") + ); + assert_eq!( + imported.adapter_ids.get("crowdanki:deck_config_uuid"), + Some("deck.ultimate-geography:deck-config") + ); + assert_eq!( + imported.adapter_ids.get("crowdanki:deck_config_name"), + Some("Ultimate Geography") + ); + assert_eq!( + imported + .note_types + .get(&sid("note-type.country")) + .unwrap() + .adapter_ids + .get("crowdanki:uuid"), + Some("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") + ); + assert_eq!( + imported + .notes + .get(&sid("note.finland")) + .unwrap() + .adapter_ids + .get("crowdanki:guid"), + Some("ug-finland-guid") + ); +} + +#[test] +fn crowdanki_parity_comparator_accepts_exact_match() { + let expected: serde_json::Value = serde_json::json!({ + "name": "Deck", + "notes": [{"guid": "abc", "fields": ["A"]}] + }); + let actual = expected.clone(); + + crowdanki::compare_deck_json_values( + &expected, + &actual, + &crowdanki::CrowdAnkiParityOptions::default(), + ) + .expect("exact JSON matches"); +} + +#[test] +fn crowdanki_parity_comparator_reports_json_paths() { + let expected: serde_json::Value = serde_json::json!({ + "desc": "Expected description", + "name": "Deck", + "notes": [{"guid": "abc", "fields": ["A"]}] + }); + let actual: serde_json::Value = serde_json::json!({ + "name": "Deck", + "notes": [{"guid": "abc", "fields": ["B"]}], + "deck_config_uuid": "legacy-default" + }); + + let report = crowdanki::compare_deck_json_values( + &expected, + &actual, + &crowdanki::CrowdAnkiParityOptions::default(), + ) + .expect_err("differences are reported"); + + assert!( + report + .differences + .iter() + .any(|difference| difference.path == "$.deck_config_uuid") + ); + assert!( + report + .differences + .iter() + .any(|difference| difference.path == "$.desc") + ); + assert!( + report + .differences + .iter() + .any(|difference| difference.path == "$.notes[guid=\"abc\"].fields[0]") + ); +} + +#[test] +fn crowdanki_parity_comparator_accepts_allowlisted_paths() { + let expected: serde_json::Value = serde_json::json!({ + "name": "Deck", + "notes": [{"guid": "abc", "fields": ["A"]}] + }); + let actual: serde_json::Value = serde_json::json!({ + "name": "Deck", + "notes": [{"guid": "abc", "fields": ["A"], "flags": 0}], + "deck_config_uuid": "legacy-default" + }); + let options = crowdanki::CrowdAnkiParityOptions { + allowed_path_globs: BTreeSet::from([ + "$.deck_config_uuid".to_owned(), + "$.notes[*].flags".to_owned(), + ]), + }; + + crowdanki::compare_deck_json_values(&expected, &actual, &options) + .expect("allowlisted JSON paths may differ"); +} + +#[test] +fn crowdanki_parity_comparator_matches_reordered_identity_arrays() { + let expected: serde_json::Value = serde_json::json!({ + "notes": [ + {"guid": "a", "fields": ["A"]}, + {"guid": "b", "fields": ["B"]} + ], + "note_models": [{ + "crowdanki_uuid": "model-1", + "flds": [{"name": "Country"}, {"name": "Capital"}], + "tmpls": [{"ord": 0, "name": "A"}, {"ord": 1, "name": "B"}] + }] + }); + let actual: serde_json::Value = serde_json::json!({ + "note_models": [{ + "crowdanki_uuid": "model-1", + "tmpls": [{"ord": 1, "name": "B"}, {"ord": 0, "name": "A"}], + "flds": [{"name": "Capital"}, {"name": "Country"}] + }], + "notes": [ + {"guid": "b", "fields": ["B"]}, + {"guid": "a", "fields": ["A"]} + ] + }); + + crowdanki::compare_deck_json_values( + &expected, + &actual, + &crowdanki::CrowdAnkiParityOptions::default(), + ) + .expect("identity-keyed arrays may reorder without a parity difference"); +} + +#[test] +fn crowdanki_parity_report_summarizes_repeated_defaults_and_serializes_to_json() { + let expected: serde_json::Value = serde_json::json!({ + "notes": [ + {"guid": "a", "fields": ["A"]}, + {"guid": "b", "fields": ["B"]}, + {"guid": "c", "fields": ["C"]} + ] + }); + let actual: serde_json::Value = serde_json::json!({ + "notes": [ + {"guid": "a", "fields": ["A"], "flags": 0}, + {"guid": "b", "fields": ["B"], "flags": 0}, + {"guid": "c", "fields": ["C"], "flags": 0} + ] + }); + + let report = crowdanki::compare_deck_json_values( + &expected, + &actual, + &crowdanki::CrowdAnkiParityOptions::default(), + ) + .expect_err("extra defaults are reported"); + let human = report.to_string(); + assert!(human.contains("Repeated differences")); + assert!(human.contains("3 × $.notes[*].flags")); + + let json = serde_json::to_value(&report).expect("report serializes"); + assert_eq!(json["differences"][0]["kind"], "extra_actual"); + assert_eq!(json["differences"].as_array().unwrap().len(), 3); +} + +#[test] +fn export_omits_tombstoned_notes_and_reports_their_stable_ids() { + let mut deck = ug_style_deck(); + deck.tombstones.insert(sid("note.finland")); + + let export = crowdanki::export_deck(&deck).expect("deck exports"); + + assert_eq!(export.omitted_tombstones, vec![sid("note.finland")]); + assert!(!export.deck_json.contains("ug-finland-guid")); +} + +const EXPECTED_CROWDANKI_JSON: &str = r#"{ + "__type__": "Deck", + "children": [], + "crowdanki_uuid": "43c5ba66-9a65-11e8-90c9-a0481cc15658", + "deck_config_uuid": "deck.ultimate-geography:deck-config", + "deck_configurations": [ + { + "__type__": "DeckConfig", + "autoplay": false, + "crowdanki_uuid": "deck.ultimate-geography:deck-config", + "dyn": false, + "lapse": { + "delays": [ + 10 + ], + "leechAction": 0, + "leechFails": 8, + "minInt": 1, + "mult": 0 + }, + "maxTaken": 60, + "name": "Ultimate Geography", + "new": { + "bury": true, + "delays": [ + 1, + 10 + ], + "initialFactor": 2500, + "ints": [ + 1, + 4, + 7 + ], + "order": 0, + "perDay": 15, + "separate": true + }, + "replayq": true, + "rev": { + "bury": true, + "ease4": 1.3, + "fuzz": 0.05, + "ivlFct": 1, + "maxIvl": 36500, + "minSpace": 1, + "perDay": 100 + }, + "timer": 0 + } + ], + "desc": "A geography deck fixture.", + "dyn": 0, + "extendNew": 10, + "extendRev": 50, + "media_files": [ + "flags/fi.png" + ], + "name": "Ultimate Geography", + "note_models": [ + { + "__type__": "NoteModel", + "crowdanki_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "css": ".card { font-family: sans-serif; }\n", + "flds": [ + { + "font": "Arial", + "media": [], + "name": "Country", + "ord": 0, + "rtl": false, + "size": 20, + "sticky": false + }, + { + "font": "Arial", + "media": [], + "name": "Capital", + "ord": 1, + "rtl": false, + "size": 20, + "sticky": false + }, + { + "font": "Arial", + "media": [], + "name": "Flag", + "ord": 2, + "rtl": false, + "size": 20, + "sticky": false + } + ], + "latexPost": "\\end{document}", + "latexPre": "\\documentclass[12pt]{article}\n\\special{papersize=3in,5in}\n\\usepackage{amssymb,amsmath}\n\\pagestyle{empty}\n\\setlength{\\parindent}{0in}\n\\begin{document}\n", + "latexsvg": false, + "name": "Country", + "req": [], + "sortf": 0, + "tags": [], + "tmpls": [ + { + "afmt": "{{FrontSide}}
{{Capital}}", + "bafmt": "", + "bfont": "", + "bqfmt": "", + "bsize": 0, + "did": null, + "name": "Country - Capital", + "ord": 0, + "qfmt": "{{Country}}", + "scratchPad": 0 + } + ], + "type": 0, + "vers": [] + } + ], + "notes": [ + { + "__type__": "Note", + "data": "", + "fields": [ + "Finland", + "Helsinki", + "" + ], + "flags": 0, + "guid": "ug-finland-guid", + "note_model_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "tags": [ + "Europe", + "Nordic" + ] + } + ] +} +"#; + +fn ug_style_deck() -> CanonicalDeck { + let mut deck_adapter_ids = AdapterIds::new(); + deck_adapter_ids.insert("crowdanki:uuid", "43c5ba66-9a65-11e8-90c9-a0481cc15658"); + + let mut note_type_adapter_ids = AdapterIds::new(); + note_type_adapter_ids.insert("crowdanki:uuid", "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + + let note_type = NoteType { + id: sid("note-type.country"), + name: "Country".to_owned(), + variables: BTreeMap::new(), + fields: vec![ + FieldDefinition { + id: sid("field.country"), + name: "Country".to_owned(), + }, + FieldDefinition { + id: sid("field.capital"), + name: "Capital".to_owned(), + }, + FieldDefinition { + id: sid("field.flag"), + name: "Flag".to_owned(), + }, + ], + card_templates: vec![CardTemplate { + id: sid("template.country-capital"), + name: "Country - Capital".to_owned(), + variables: BTreeMap::new(), + question_format: "{{Country}}".to_owned(), + answer_format: "{{FrontSide}}
{{Capital}}".to_owned(), + adapter_ids: AdapterIds::new(), + }], + styling: ".card { font-family: sans-serif; }\n".to_owned(), + adapter_ids: note_type_adapter_ids, + }; + + let mut note_adapter_ids = AdapterIds::new(); + note_adapter_ids.insert("crowdanki:guid", "ug-finland-guid"); + + let note = Note { + id: sid("note.finland"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Finland".to_owned()), + (sid("field.capital"), "Helsinki".to_owned()), + (sid("field.flag"), "".to_owned()), + ]), + tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), + adapter_ids: note_adapter_ids, + }; + + CanonicalDeck { + id: sid("deck.ultimate-geography"), + name: "Ultimate Geography".to_owned(), + description: "A geography deck fixture.".to_owned(), + variables: BTreeMap::new(), + note_types: BTreeMap::from([(note_type.id.clone(), note_type)]), + notes: BTreeMap::from([(note.id.clone(), note)]), + media: BTreeMap::from([( + sid("media.flags-fi-png"), + MediaReference { + id: sid("media.flags-fi-png"), + path: "flags/fi.png".to_owned(), + sha256: String::new(), + }, + )]), + tombstones: BTreeSet::new(), + adapter_ids: deck_adapter_ids, + } +} + +fn sid(value: &str) -> StableId { + StableId::new(value).expect("test stable id is valid") +} diff --git a/crates/brain-brew-formats/tests/lockfile_yaml.rs b/crates/brain-brew-formats/tests/lockfile_yaml.rs new file mode 100644 index 0000000..4c907e5 --- /dev/null +++ b/crates/brain-brew-formats/tests/lockfile_yaml.rs @@ -0,0 +1,65 @@ +use brain_brew_formats::lockfile; + +#[test] +fn parses_and_formats_federation_lock_with_git_nar_hash() { + let formatted = lockfile::format_str( + r#" +packages: + anki-geo.ultimate-geography: + locked: + nar_hash: sha256-example + rev: ccf150a1b21e + url: https://github.com/anki-geo/ultimate-geography.git + type: git + original: + ref: main + url: https://github.com/anki-geo/ultimate-geography.git + type: git + package: + version: 0.1.0 + manifest: brainbrew.yaml +version: 1 +"#, + ) + .expect("lock formats"); + + assert_eq!( + formatted, + r#"version: 1 +packages: + anki-geo.ultimate-geography: + manifest: brainbrew.yaml + package: + version: 0.1.0 + original: + type: git + url: https://github.com/anki-geo/ultimate-geography.git + ref: main + locked: + type: git + url: https://github.com/anki-geo/ultimate-geography.git + rev: ccf150a1b21e + nar_hash: sha256-example +"# + ); + + let lock = lockfile::from_str(&formatted).expect("formatted lock parses"); + let package = &lock.packages["anki-geo.ultimate-geography"]; + assert_eq!(package.package.version, "0.1.0"); + assert_eq!(package.locked.source_type, "git"); + assert_eq!(package.locked.rev.as_deref(), Some("ccf150a1b21e")); + assert_eq!(package.locked.nar_hash.as_deref(), Some("sha256-example")); +} + +#[test] +fn rejects_unknown_lock_fields() { + let error = lockfile::from_str( + r#" +version: 1 +unknown: true +"#, + ) + .expect_err("unknown fields are rejected"); + + assert!(error.to_string().contains("unknown field `unknown`")); +} diff --git a/crates/brain-brew-formats/tests/manifest_yaml.rs b/crates/brain-brew-formats/tests/manifest_yaml.rs new file mode 100644 index 0000000..bdaa96b --- /dev/null +++ b/crates/brain-brew-formats/tests/manifest_yaml.rs @@ -0,0 +1,289 @@ +use brain_brew_formats::manifest; + +#[test] +fn expands_manifest_target_dependencies_in_deterministic_order() { + let manifest = manifest::from_str( + r#" +base: deck.yaml +overlays: + lang.de: + file: overlays/languages/de.yaml + kind: translation + variant.extended.de: + file: overlays/variants/extended.de.yaml + kind: extension + depends_on: + - lang.de + patch.capital: + file: overlays/patches/capital.yaml + kind: patch +targets: + de-extended-patched: + overlays: + - variant.extended.de + - patch.capital +"#, + ) + .expect("manifest parses"); + + let target = manifest + .expand_target("de-extended-patched") + .expect("target expands"); + + assert_eq!(target.base, "deck.yaml"); + assert_eq!(target.extends, None); + assert_eq!( + target + .overlays + .iter() + .map(|overlay| overlay.id.as_str()) + .collect::>(), + vec!["lang.de", "variant.extended.de", "patch.capital"] + ); + assert_eq!( + target + .overlays + .iter() + .map(|overlay| overlay.file.as_str()) + .collect::>(), + vec![ + "overlays/languages/de.yaml", + "overlays/variants/extended.de.yaml", + "overlays/patches/capital.yaml" + ] + ); +} + +#[test] +fn parses_package_metadata_and_target_export_checks() { + let manifest = manifest::from_str( + r#" +package: + id: anki-geo.ultimate-geography + version: 0.1.0 + compatible_base_versions: + - '>=0.1,<0.2' + depends_on: + - anki-geo.shared-geography@0.1.0 +base: deck.yaml +targets: + en-standard: + overlays: [] + exports: + crowdanki: + out: build/en-standard + golden: goldens/en-standard/deck.json + golden_allowlist: + - $.deck_config_uuid +"#, + ) + .expect("manifest parses"); + + let package = manifest.package.expect("package metadata parsed"); + assert_eq!(package.id, "anki-geo.ultimate-geography"); + assert_eq!(package.version, "0.1.0"); + assert_eq!(package.compatible_base_versions, vec![">=0.1,<0.2"]); + assert_eq!(package.depends_on, vec!["anki-geo.shared-geography@0.1.0"]); + + let export = manifest.targets["en-standard"] + .exports + .crowdanki + .as_ref() + .expect("crowdanki export config parsed"); + assert_eq!(export.out.as_deref(), Some("build/en-standard")); + assert_eq!( + export.golden.as_deref(), + Some("goldens/en-standard/deck.json") + ); + assert_eq!(export.golden_allowlist, vec!["$.deck_config_uuid"]); +} + +#[test] +fn formatter_canonicalizes_manifest_yaml() { + let formatted = manifest::format_str( + r#" +targets: + de-extended: + overlays: [overlay.variant.extended.de] +overlays: + overlay.variant.extended.de: + depends_on: [overlay.translation.de] + kind: extension + file: overlays/variants/extended/de.yaml + overlay.translation.de: + file: overlays/languages/de.yaml + kind: translation +base: deck.yaml +"#, + ) + .expect("manifest formats"); + + assert_eq!( + formatted, + r#"base: deck.yaml +overlays: + overlay.translation.de: + file: overlays/languages/de.yaml + kind: translation + overlay.variant.extended.de: + file: overlays/variants/extended/de.yaml + kind: extension + depends_on: + - overlay.translation.de +targets: + de-extended: + overlays: + - overlay.variant.extended.de +"# + ); +} + +#[test] +fn parses_and_formats_target_extends_for_package_federation() { + let formatted = manifest::format_str( + r#" +base: deck.yaml +overlays: + overlay.extension.america: + file: overlays/america.yaml + kind: extension +targets: + en-america: + overlays: [overlay.extension.america] + extends: anki-geo.ultimate-geography:en-standard +"#, + ) + .expect("manifest formats"); + + assert_eq!( + formatted, + r#"base: deck.yaml +overlays: + overlay.extension.america: + file: overlays/america.yaml + kind: extension +targets: + en-america: + extends: anki-geo.ultimate-geography:en-standard + overlays: + - overlay.extension.america +"# + ); + let manifest = manifest::from_str(&formatted).expect("manifest parses"); + let target = manifest.expand_target("en-america").unwrap(); + assert_eq!( + target.extends.as_deref(), + Some("anki-geo.ultimate-geography:en-standard") + ); +} + +#[test] +fn formatter_emits_package_metadata_and_target_exports() { + let formatted = manifest::format_str( + r#" +targets: + en-standard: + exports: + crowdanki: + golden_allowlist: ['$.deck_config_uuid'] + golden: goldens/en-standard/deck.json + out: build/en-standard + overlays: [] +package: + depends_on: [anki-geo.shared-geography@0.1.0] + compatible_base_versions: ['>=0.1,<0.2'] + version: 0.1.0 + id: anki-geo.ultimate-geography +base: deck.yaml +"#, + ) + .expect("manifest formats"); + + assert_eq!( + formatted, + r#"package: + id: anki-geo.ultimate-geography + version: 0.1.0 + compatible_base_versions: + - '>=0.1,<0.2' + depends_on: + - 'anki-geo.shared-geography@0.1.0' +base: deck.yaml +overlays: {} +targets: + en-standard: + overlays: [] + exports: + crowdanki: + out: build/en-standard + golden: goldens/en-standard/deck.json + golden_allowlist: + - '$.deck_config_uuid' +"# + ); +} + +#[test] +fn rejects_manifest_unknown_fields() { + let error = manifest::from_str( + r#" +base: deck.yaml +unknown: true +"#, + ) + .expect_err("unknown fields are rejected"); + + assert!(error.to_string().contains("unknown field `unknown`")); +} + +#[test] +fn reports_missing_overlay_references() { + let manifest = manifest::from_str( + r#" +base: deck.yaml +targets: + broken: + overlays: + - missing.overlay +"#, + ) + .expect("manifest parses"); + + let error = manifest + .expand_target("broken") + .expect_err("missing overlay is reported"); + + assert_eq!( + error.to_string(), + "manifest overlay \"missing.overlay\" does not exist" + ); +} + +#[test] +fn reports_overlay_dependency_cycles() { + let manifest = manifest::from_str( + r#" +base: deck.yaml +overlays: + a: + file: a.yaml + depends_on: [b] + b: + file: b.yaml + depends_on: [a] +targets: + cyclic: + overlays: [a] +"#, + ) + .expect("manifest parses"); + + let error = manifest + .expand_target("cyclic") + .expect_err("cycle is reported"); + + assert_eq!( + error.to_string(), + "manifest overlay dependency cycle: a -> b -> a" + ); +} diff --git a/crates/brain-brew-formats/tests/media_references.rs b/crates/brain-brew-formats/tests/media_references.rs new file mode 100644 index 0000000..26d99b8 --- /dev/null +++ b/crates/brain-brew-formats/tests/media_references.rs @@ -0,0 +1,222 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use brain_brew_core::{ + AdapterIds, CanonicalDeck, CardTemplate, FieldDefinition, MediaReference, Note, NoteType, + StableId, +}; +use brain_brew_formats::media::{self, MediaValidationErrorKind}; + +#[test] +fn extracts_media_references_from_fields_and_templates() { + let deck = media_deck(); + + let paths = media::referenced_paths(&deck); + + assert!(paths.contains("flags/fi.png")); + assert!(paths.contains("audio/fi.mp3")); + assert!(paths.contains("maps/fi.svg")); +} + +#[test] +fn extracts_multiple_img_sources_from_one_field() { + let mut deck = media_deck(); + deck.notes + .get_mut(&sid("note.finland")) + .unwrap() + .fields + .insert( + sid("field.flag"), + "".to_owned(), + ); + + let paths = media::referenced_paths(&deck); + + assert!(paths.contains("flags/fi-blur.png")); + assert!(paths.contains("flags/fi.png")); +} + +#[test] +fn extracts_css_url_media_from_templates_and_styling() { + let mut deck = media_deck(); + let note_type = deck.note_types.get_mut(&sid("note-type.country")).unwrap(); + note_type.styling = "@import url(\"css/maps.css\");".to_owned(); + note_type.card_templates[0].question_format = + "sourcemailanswer{{Country}}".to_owned(); + note_type.card_templates[0].answer_format = + "{{Flag}}".to_owned(); + + let paths = media::referenced_paths(&deck); + + assert!(paths.contains("css/maps.css")); + assert!(paths.contains("css/interactive.css")); + assert!(paths.contains("css/review.css")); + assert!(!paths.contains("https://example.com")); + assert!(!paths.contains("mailto:geo@example.com")); + assert!(!paths.contains("#answer")); +} + +#[test] +fn validation_accepts_declared_media_references() { + let deck = media_deck(); + + assert!(media::validate_references(&deck).is_ok()); +} + +#[test] +fn validation_reports_missing_media_reference_paths() { + let mut deck = media_deck(); + deck.media.remove(&sid("media.flags-fi-png")); + + let report = media::validate_references(&deck).expect_err("missing media must fail"); + + assert!(report.has_kind(MediaValidationErrorKind::MissingReference)); + assert!( + report + .errors + .iter() + .any(|error| error.path == "flags/fi.png") + ); +} + +#[test] +fn validates_media_asset_hashes_from_supplied_bytes() { + let mut deck = media_deck(); + deck.media + .get_mut(&sid("media.flags-fi-png")) + .unwrap() + .sha256 = media::sha256_hex(b"flag-bytes"); + deck.media + .get_mut(&sid("media.audio-fi-mp3")) + .unwrap() + .sha256 = media::sha256_hex(b"audio-bytes"); + deck.media + .get_mut(&sid("media.maps-fi-svg")) + .unwrap() + .sha256 = media::sha256_hex(b"map-bytes"); + let assets = BTreeMap::from([ + ("flags/fi.png".to_owned(), b"flag-bytes".to_vec()), + ("audio/fi.mp3".to_owned(), b"audio-bytes".to_vec()), + ("maps/fi.svg".to_owned(), b"map-bytes".to_vec()), + ]); + + assert!(media::validate_hashes(&deck, &assets).is_ok()); +} + +#[test] +fn reports_media_hash_mismatches() { + let mut deck = media_deck(); + deck.media + .get_mut(&sid("media.flags-fi-png")) + .unwrap() + .sha256 = media::sha256_hex(b"expected"); + let assets = BTreeMap::from([("flags/fi.png".to_owned(), b"actual".to_vec())]); + + let report = media::validate_hashes(&deck, &assets).expect_err("hash mismatch must fail"); + + assert!(report.has_kind(MediaValidationErrorKind::HashMismatch)); + assert!( + report + .errors + .iter() + .any(|error| error.path == "flags/fi.png") + ); +} + +#[test] +fn validation_reports_unused_media_references() { + let mut deck = media_deck(); + deck.media.insert( + sid("media.unused"), + MediaReference { + id: sid("media.unused"), + path: "unused.png".to_owned(), + sha256: "abc".to_owned(), + }, + ); + + let report = media::validate_references(&deck).expect_err("unused media must fail"); + + assert!(report.has_kind(MediaValidationErrorKind::UnusedReference)); + assert!(report.errors.iter().any(|error| error.path == "unused.png")); +} + +fn media_deck() -> CanonicalDeck { + let note_type = NoteType { + id: sid("note-type.country"), + name: "Country".to_owned(), + variables: BTreeMap::new(), + fields: vec![ + FieldDefinition { + id: sid("field.country"), + name: "Country".to_owned(), + }, + FieldDefinition { + id: sid("field.flag"), + name: "Flag".to_owned(), + }, + ], + card_templates: vec![CardTemplate { + id: sid("template.country-flag"), + name: "Country - Flag".to_owned(), + variables: BTreeMap::new(), + question_format: "{{Country}} ".to_owned(), + answer_format: "{{Flag}} [sound:audio/fi.mp3]".to_owned(), + adapter_ids: AdapterIds::new(), + }], + styling: String::new(), + adapter_ids: AdapterIds::new(), + }; + + let note = Note { + id: sid("note.finland"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Finland".to_owned()), + (sid("field.flag"), "".to_owned()), + ]), + tags: BTreeSet::new(), + adapter_ids: AdapterIds::new(), + }; + + CanonicalDeck { + id: sid("deck.media"), + name: "Media".to_owned(), + description: String::new(), + variables: BTreeMap::new(), + note_types: BTreeMap::from([(note_type.id.clone(), note_type)]), + notes: BTreeMap::from([(note.id.clone(), note)]), + media: BTreeMap::from([ + ( + sid("media.flags-fi-png"), + MediaReference { + id: sid("media.flags-fi-png"), + path: "flags/fi.png".to_owned(), + sha256: "abc".to_owned(), + }, + ), + ( + sid("media.audio-fi-mp3"), + MediaReference { + id: sid("media.audio-fi-mp3"), + path: "audio/fi.mp3".to_owned(), + sha256: "def".to_owned(), + }, + ), + ( + sid("media.maps-fi-svg"), + MediaReference { + id: sid("media.maps-fi-svg"), + path: "maps/fi.svg".to_owned(), + sha256: "ghi".to_owned(), + }, + ), + ]), + tombstones: BTreeSet::new(), + adapter_ids: AdapterIds::new(), + } +} + +fn sid(value: &str) -> StableId { + StableId::new(value).expect("test stable id is valid") +} diff --git a/crates/brain-brew-formats/tests/overlay_yaml.rs b/crates/brain-brew-formats/tests/overlay_yaml.rs new file mode 100644 index 0000000..c9ca4ee --- /dev/null +++ b/crates/brain-brew-formats/tests/overlay_yaml.rs @@ -0,0 +1,736 @@ +use brain_brew_core::{ChangeIntent, ExpectedBase, OverlayKind, StableId}; +use brain_brew_formats::canonical_yaml; + +#[test] +fn formatter_canonicalizes_overlay_yaml() { + let formatted = canonical_yaml::overlay_format_str( + r#"kind: extension +id: overlay.extension.extended +note_types: + note-type.country: + card_templates: + template.country-flag: + template: + answer_format: '{{Flag}}' + question_format: '{{Country}}' + name: Country - Flag + insert_after: template.capital-country + intent: add + styling: + value: | + .card { font-family: serif; } + intent: replace + expected_base: + value: | + .card { font-family: sans-serif; } + intent: merge +"#, + ) + .expect("overlay formats"); + + assert!(formatted.starts_with("id: overlay.extension.extended\nkind: extension\n")); + assert!(formatted.contains(" styling:\n intent: replace\n")); + assert!(formatted.contains(" template.country-flag:\n intent: add\n")); + assert!(formatted.contains(" insert_after: template.capital-country\n")); + canonical_yaml::overlay_from_str(&formatted).expect("formatted overlay parses"); +} + +#[test] +fn parses_sparse_overlay_yaml_with_field_expected_base() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.patch.capital +kind: patch +notes: + note.finland: + intent: merge + fields: + field.capital: + intent: replace + value: Helsingfors + expected_base: + value: Helsinki +"#, + ) + .expect("overlay parses"); + + assert_eq!(overlay.id, sid("overlay.patch.capital")); + assert_eq!(overlay.kind, OverlayKind::Patch); + let note_change = overlay.note_changes.get(&sid("note.finland")).unwrap(); + assert_eq!(note_change.intent, ChangeIntent::Merge); + let field_change = note_change.fields.get(&sid("field.capital")).unwrap(); + assert_eq!(field_change.intent, ChangeIntent::Replace); + assert_eq!(field_change.value.as_deref(), Some("Helsingfors")); + assert_eq!( + field_change.expected_base, + Some(ExpectedBase::Value("Helsinki".to_owned())) + ); +} + +#[test] +fn parses_note_type_field_addition_overlay() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.population +kind: extension +note_types: + note-type.country: + intent: merge + fields: + field.population: + intent: add + name: Population +notes: {} +"#, + ) + .expect("overlay parses"); + + assert_eq!(overlay.kind, OverlayKind::Extension); + let note_type_change = overlay + .note_type_changes + .get(&sid("note-type.country")) + .unwrap(); + let field_change = note_type_change + .fields + .get(&sid("field.population")) + .unwrap(); + assert_eq!(field_change.intent, ChangeIntent::Add); + assert_eq!(field_change.field.as_ref().unwrap().name, "Population"); +} + +#[test] +fn parses_note_type_addition_overlay_with_payload() { + let formatted = canonical_yaml::overlay_format_str( + r#"id: overlay.extension.regions +kind: extension +note_types: + note-type.region: + intent: add + note_type: + name: Region Geography + field_order: + - field.region + - field.map + fields: + field.map: + name: Map + field.region: + name: Region + card_template_order: + - template.region-map + card_templates: + template.region-map: + name: Region - Map + question_format: '{{Region}}' + answer_format: '{{Map}}' + styling: | + .card { font-family: sans-serif; } +"#, + ) + .expect("overlay formats"); + + let overlay = canonical_yaml::overlay_from_str(&formatted).expect("formatted overlay parses"); + let change = overlay + .note_type_changes + .get(&sid("note-type.region")) + .unwrap(); + assert_eq!(change.intent, ChangeIntent::Add); + let note_type = change.note_type.as_ref().unwrap(); + assert_eq!(note_type.name, "Region Geography"); + assert_eq!(note_type.fields[0].id, sid("field.region")); + assert_eq!(note_type.card_templates[0].id, sid("template.region-map")); + assert!(formatted.contains(" note_type:\n name: Region Geography\n")); +} + +#[test] +fn parses_field_additions_shorthand_for_multiple_fields() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.population +kind: extension +field_additions: + note-type.country: + fields: + field.population: Population + field.area: Area + values: + note.australia: + field.population: 25.0 million + field.area: 7.69 million km² + note.austria: + field.population: 16.0 million +"#, + ) + .expect("overlay parses"); + + let note_type_change = overlay + .note_type_changes + .get(&sid("note-type.country")) + .unwrap(); + assert_eq!(note_type_change.intent, ChangeIntent::Merge); + assert_eq!( + note_type_change + .fields + .get(&sid("field.population")) + .unwrap() + .field + .as_ref() + .unwrap() + .name, + "Population" + ); + assert_eq!( + note_type_change + .fields + .get(&sid("field.area")) + .unwrap() + .field + .as_ref() + .unwrap() + .name, + "Area" + ); + + let note_change = overlay.note_changes.get(&sid("note.australia")).unwrap(); + assert_eq!(note_change.intent, ChangeIntent::Merge); + assert_eq!( + note_change + .fields + .get(&sid("field.population")) + .unwrap() + .value + .as_deref(), + Some("25.0 million") + ); + assert_eq!( + note_change + .fields + .get(&sid("field.area")) + .unwrap() + .value + .as_deref(), + Some("7.69 million km²") + ); +} + +#[test] +fn field_additions_shorthand_matches_verbose_overlay_semantics() { + let base = canonical_yaml::from_str( + r#"deck: + id: deck.demo + name: Demo + description: '' +note_types: + note-type.country: + name: Country + field_order: + - field.country + fields: + field.country: + name: Country + card_template_order: + - template.country + card_templates: + template.country: + name: Country + question_format: '{{Country}}' + answer_format: '{{Country}}' + styling: '' +notes: + note.australia: + note_type_id: note-type.country + fields: + field.country: Australia + tags: [] + note.austria: + note_type_id: note-type.country + fields: + field.country: Austria + tags: [] +media: {} +tombstones: [] +"#, + ) + .expect("base parses"); + let concise = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.population +kind: extension +field_additions: + note-type.country: + fields: + field.population: Population + field.area: Area + values: + note.australia: + field.population: 25.0 million + field.area: 7.69 million km² + note.austria: + field.population: 16.0 million + field.area: 83,879 km² +"#, + ) + .expect("concise overlay parses"); + let verbose = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.population +kind: extension +note_types: + note-type.country: + intent: merge + fields: + field.population: + intent: add + name: Population + field.area: + intent: add + name: Area +notes: + note.australia: + intent: merge + fields: + field.population: + intent: add + value: 25.0 million + field.area: + intent: add + value: 7.69 million km² + note.austria: + intent: merge + fields: + field.population: + intent: add + value: 16.0 million + field.area: + intent: add + value: 83,879 km² +"#, + ) + .expect("verbose overlay parses"); + + assert_eq!(concise, verbose); + let concise_deck = base.compose(&[concise]).expect("concise composes"); + let verbose_deck = base.compose(&[verbose]).expect("verbose composes"); + assert!(concise_deck.semantic_diff(&verbose_deck).is_empty()); +} + +#[test] +fn formatter_prefers_field_additions_shorthand() { + let formatted = canonical_yaml::overlay_format_str( + r#"id: overlay.extension.population +kind: extension +note_types: + note-type.country: + intent: merge + fields: + field.population: + intent: add + name: Population +notes: + note.australia: + intent: merge + fields: + field.population: + intent: add + value: 25.0 million +"#, + ) + .expect("overlay formats"); + + assert_eq!( + formatted, + "id: overlay.extension.population\nkind: extension\nfield_additions:\n note-type.country:\n fields:\n field.population: Population\n values:\n note.australia:\n field.population: 25.0 million\n" + ); +} + +#[test] +fn parses_field_fills_shorthand_for_existing_blank_fields() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.hardcore.fills.en +kind: extension +field_fills: + note.anguilla: + field.capital: The Valley + field.flag: '' +"#, + ) + .expect("overlay parses"); + + let note_change = overlay.note_changes.get(&sid("note.anguilla")).unwrap(); + assert_eq!(note_change.intent, ChangeIntent::Merge); + let capital = note_change.fields.get(&sid("field.capital")).unwrap(); + assert_eq!(capital.intent, ChangeIntent::Replace); + assert_eq!(capital.value.as_deref(), Some("The Valley")); + assert_eq!( + capital.expected_base, + Some(ExpectedBase::Value(String::new())) + ); +} + +#[test] +fn field_fills_shorthand_matches_verbose_overlay_semantics() { + let concise = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.hardcore.fills.en +kind: extension +field_fills: + note.anguilla: + field.capital: The Valley + field.flag: '' +"#, + ) + .expect("concise overlay parses"); + let verbose = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.hardcore.fills.en +kind: extension +notes: + note.anguilla: + intent: merge + fields: + field.capital: + intent: replace + value: The Valley + expected_base: + value: '' + field.flag: + intent: replace + value: '' + expected_base: + value: '' +"#, + ) + .expect("verbose overlay parses"); + + assert_eq!(concise, verbose); +} + +#[test] +fn formatter_prefers_field_fills_shorthand() { + let formatted = canonical_yaml::overlay_format_str( + r#"id: overlay.extension.hardcore.fills.en +kind: extension +notes: + note.anguilla: + intent: merge + fields: + field.capital: + intent: replace + value: The Valley + expected_base: + value: '' +"#, + ) + .expect("overlay formats"); + + assert_eq!( + formatted, + "id: overlay.extension.hardcore.fills.en\nkind: extension\nfield_fills:\n note.anguilla:\n field.capital: The Valley\n" + ); +} + +#[test] +fn formatter_orders_translation_dictionary_sections_deterministically() { + let formatted = canonical_yaml::overlay_format_str( + r#"id: overlay.translation.de +kind: translation +translations: + additions: + notes.note.anguilla.fields.field.capital: The Valley + adapter_ids: + crowdanki:guid: + old-guid: new-guid + changes: + Germany: Deutschland +"#, + ) + .expect("overlay formats"); + + assert!( + formatted.find(" changes:\n").unwrap() < formatted.find(" additions:\n").unwrap(), + "changes are emitted before additions" + ); + assert!( + formatted.find(" additions:\n").unwrap() < formatted.find(" adapter_ids:\n").unwrap(), + "additions are emitted before adapter_ids" + ); +} + +#[test] +fn parses_metadata_and_adapter_id_overlay_changes() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.translation.de +kind: translation +deck: + name: + intent: replace + value: Ultimate Geography [DE] + expected_base: + value: Ultimate Geography + adapter_ids: + crowdanki:uuid: + intent: replace + value: de-deck-uuid + expected_base: + value: en-deck-uuid +note_types: + note-type.country: + intent: merge + name: + intent: replace + value: Ultimate Geography [DE] + expected_base: + value: Ultimate Geography + adapter_ids: + crowdanki:model_id: + intent: replace + value: de-model-id + expected_base: + value: en-model-id + card_templates: + template.country-capital: + intent: merge + adapter_ids: + crowdanki:ord: + intent: add + value: '0' +notes: + note.finland: + intent: merge + adapter_ids: + crowdanki:guid: + intent: replace + value: ug-finland-de-guid + expected_base: + value: ug-finland-guid +"#, + ) + .expect("overlay parses"); + + let deck_change = overlay.deck_change.as_ref().unwrap(); + assert_eq!( + deck_change.name.as_ref().unwrap().value.as_deref(), + Some("Ultimate Geography [DE]") + ); + assert_eq!( + deck_change + .adapter_ids + .get("crowdanki:uuid") + .unwrap() + .value + .as_deref(), + Some("de-deck-uuid") + ); + + let note_type_change = overlay + .note_type_changes + .get(&sid("note-type.country")) + .unwrap(); + assert_eq!( + note_type_change.name.as_ref().unwrap().value.as_deref(), + Some("Ultimate Geography [DE]") + ); + assert_eq!( + note_type_change + .adapter_ids + .get("crowdanki:model_id") + .unwrap() + .value + .as_deref(), + Some("de-model-id") + ); + assert_eq!( + note_type_change + .card_templates + .get(&sid("template.country-capital")) + .unwrap() + .adapter_ids + .get("crowdanki:ord") + .unwrap() + .value + .as_deref(), + Some("0") + ); + + let note_change = overlay.note_changes.get(&sid("note.finland")).unwrap(); + assert_eq!( + note_change + .adapter_ids + .get("crowdanki:guid") + .unwrap() + .value + .as_deref(), + Some("ug-finland-de-guid") + ); +} + +#[test] +fn parses_add_note_overlay_payload() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.sweden +kind: extension +notes: + note.sweden: + intent: add + note: + note_type_id: note-type.country + fields: + field.country: Sweden + field.capital: Stockholm + tags: [Europe, Nordic] + adapter_ids: + crowdanki:guid: ug-sweden-guid +"#, + ) + .expect("overlay parses"); + + let note_change = overlay.note_changes.get(&sid("note.sweden")).unwrap(); + let note = note_change.note.as_ref().unwrap(); + assert_eq!(note.id, sid("note.sweden")); + assert_eq!(note.note_type_id, sid("note-type.country")); + assert_eq!(note.fields.get(&sid("field.capital")).unwrap(), "Stockholm"); + assert_eq!( + note.adapter_ids.get("crowdanki:guid"), + Some("ug-sweden-guid") + ); +} + +#[test] +fn parses_card_template_and_styling_overlay_changes() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.extended +kind: extension +note_types: + note-type.country: + intent: merge + styling: + intent: replace + value: | + .card { font-family: serif; } + expected_base: + value: | + .card { font-family: sans-serif; } + card_templates: + template.country-flag: + intent: add + insert_after: template.capital-country + template: + name: Country - Flag + question_format: '{{Country}}' + answer_format: '{{Flag}}' + adapter_ids: {} + template.country-capital: + intent: merge + question_format: + intent: replace + value: '{{Land}}' + expected_base: + value: '{{Country}}' +"#, + ) + .expect("overlay parses"); + + let note_type_change = overlay + .note_type_changes + .get(&sid("note-type.country")) + .unwrap(); + assert_eq!( + note_type_change.styling.as_ref().unwrap().value.as_deref(), + Some(".card { font-family: serif; }\n") + ); + let add_template = note_type_change + .card_templates + .get(&sid("template.country-flag")) + .unwrap(); + assert_eq!( + add_template.insert_after, + Some(sid("template.capital-country")) + ); + assert_eq!( + add_template.template.as_ref().unwrap().name, + "Country - Flag" + ); + let replace_template = note_type_change + .card_templates + .get(&sid("template.country-capital")) + .unwrap(); + assert_eq!( + replace_template + .question_format + .as_ref() + .unwrap() + .value + .as_deref(), + Some("{{Land}}") + ); +} + +#[test] +fn parses_tag_and_media_overlay_changes() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.extension.tags-media +kind: extension +notes: + note.finland: + intent: merge + tags: + UG::Nordic: + intent: add + Nordic: + intent: remove + expected_base: entity_present +media: + media.flag.sweden: + intent: add + path: flags/se.png + sha256: abcdef +"#, + ) + .expect("overlay parses"); + + let note_change = overlay.note_changes.get(&sid("note.finland")).unwrap(); + assert_eq!( + note_change.tags.get("UG::Nordic").unwrap().intent, + ChangeIntent::Add + ); + assert_eq!( + note_change.tags.get("Nordic").unwrap().expected_base, + Some(ExpectedBase::EntityPresent) + ); + + let media_change = overlay + .media_changes + .get(&sid("media.flag.sweden")) + .unwrap(); + assert_eq!(media_change.intent, ChangeIntent::Add); + assert_eq!(media_change.media.as_ref().unwrap().path, "flags/se.png"); +} + +#[test] +fn parses_remove_overlay_with_entity_present_expected_base() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.patch.remove-finland +kind: patch +notes: + note.finland: + intent: remove + expected_base: entity_present +"#, + ) + .expect("overlay parses"); + + let note_change = overlay.note_changes.get(&sid("note.finland")).unwrap(); + assert_eq!(note_change.intent, ChangeIntent::Remove); + assert_eq!(note_change.expected_base, Some(ExpectedBase::EntityPresent)); +} + +#[test] +fn rejects_unknown_overlay_fields() { + let error = canonical_yaml::overlay_from_str( + r#"id: overlay.patch.capital +kind: patch +unsupported: true +notes: {} +"#, + ) + .expect_err("unknown overlay fields must fail"); + + assert!(error.to_string().contains("unsupported")); +} + +fn sid(value: &str) -> StableId { + StableId::new(value).expect("test stable id is valid") +} diff --git a/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs new file mode 100644 index 0000000..cf6d591 --- /dev/null +++ b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs @@ -0,0 +1,1453 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +use brain_brew_formats::core::{ + AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplateChange, ChangeIntent, DeckChange, + ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, MediaChange, MediaReference, + Note, NoteChange, NoteTypeChange, Overlay, OverlayKind, PropertyChange, StableId, TagChange, + TranslationChange, +}; +use brain_brew_formats::{canonical_yaml, crowdanki, manifest, media}; + +#[test] +fn ultimate_geography_fixture_manifest_composes_all_targets() { + let root = fixture_root(); + let manifest = read_manifest(&root); + let base_path = root.join(&manifest.base); + let base_source = fs::read_to_string(&base_path).unwrap(); + assert_eq!( + canonical_yaml::format_str(&base_source).unwrap(), + base_source, + "{} is not canonicalized", + base_path.display() + ); + + let package = manifest + .package + .as_ref() + .expect("fixture has package metadata"); + assert_eq!(package.id, "anki-geo.ultimate-geography"); + assert_eq!(package.version, "0.1.0"); + + assert_eq!(manifest.targets.len(), 71); + for target in manifest.targets.keys() { + assert!( + manifest.targets[target].exports.is_empty(), + "{target} relies on the manifest-target default CrowdAnki output path" + ); + + let deck = compose_target(&root, &manifest, target); + deck.validate() + .unwrap_or_else(|error| panic!("{target} validates: {error}")); + media::validate_references(&deck) + .unwrap_or_else(|error| panic!("{target} media references validate: {error}")); + } +} + +#[test] +fn ultimate_geography_hardcore_extension_builds_on_main_deck_without_erasing_base_content() { + let root = fixture_root(); + let manifest = read_manifest(&root); + + let english = compose_target(&root, &manifest, "en-hardcore-standard"); + assert_eq!(english.notes.len(), 336); + assert!(english.notes.contains_key(&sid("note.pitcairn-islands"))); + assert_eq!( + english.notes[&sid("note.anguilla")].fields[&sid("field.capital")], + "The Valley" + ); + assert_eq!( + english.notes[&sid("note.anguilla")].fields[&sid("field.map")], + "", + "Hardcore fills extra fields without blanking the main deck's existing map card" + ); + assert!( + english.notes[&sid("note.anguilla")] + .tags + .contains("UG::Overlapping") + ); + + let german = compose_target(&root, &manifest, "de-hardcore-standard"); + assert_eq!( + german.notes[&sid("note.pitcairn-islands")].fields[&sid("field.country")], + "Pitcairninseln" + ); + assert_eq!( + german.notes[&sid("note.canary-islands")].fields[&sid("field.capital")], + "Santa Cruz de Tenerife, Las Palmas de Gran Canaria" + ); + + let extended = compose_target(&root, &manifest, "de-hardcore-extended"); + assert_eq!( + extended.note_types[&sid("note-type.ultimate-geography")] + .card_templates + .len(), + 6, + "Hardcore composes with the shared Extended variant" + ); +} + +#[test] +fn ultimate_geography_translation_overlays_use_dictionaries_not_template_copies() { + let root = fixture_root(); + let manifest = read_manifest(&root); + for overlay_ref in manifest + .overlays + .values() + .filter(|overlay| overlay.kind.as_deref() == Some("translation")) + { + let overlay = canonical_yaml::overlay_from_str( + &fs::read_to_string(root.join(&overlay_ref.file)).unwrap(), + ) + .unwrap_or_else(|error| panic!("{} parses: {error}", overlay_ref.file)); + let translations = overlay + .translations + .as_ref() + .unwrap_or_else(|| panic!("{} uses translation dictionary", overlay_ref.file)); + assert!( + !translations.changes.is_empty() + || !translations.additions.is_empty() + || !translations.variables.is_empty() + || !translations.adapter_ids.is_empty(), + "{} has translation dictionary entries", + overlay_ref.file + ); + assert!( + overlay.note_changes.is_empty(), + "{} uses dictionary changes/additions instead of per-note field replacements", + overlay_ref.file + ); + assert!( + translations.changes.values().all(|change| match change { + TranslationChange::Global(_) => true, + TranslationChange::AtPaths(paths) => paths + .keys() + .all(|path| path != "note_types.note-type.ultimate-geography.name"), + }), + "{} translates note type names through variables instead of path-scoped metadata changes", + overlay_ref.file + ); + assert!( + overlay + .note_type_changes + .values() + .all(|change| change.name.is_none()), + "{} translates note type names through variables instead of path-scoped metadata changes", + overlay_ref.file + ); + assert!( + overlay + .note_type_changes + .values() + .all(|change| change.card_templates.is_empty()), + "{} does not copy standard card template HTML", + overlay_ref.file + ); + if overlay_ref + .file + .starts_with("overlays/extensions/hardcore/translations/") + { + assert!( + !overlay_ref.file.ends_with("/en.yaml"), + "English Hardcore field content is not a translation overlay" + ); + assert!( + translations.additions.is_empty(), + "{} uses field_fills for extension-owned blank field content instead of translations.additions", + overlay_ref.file + ); + } + } + + for overlay_ref in manifest.overlays.values().filter(|overlay| { + overlay + .file + .starts_with("overlays/extensions/hardcore/field-fills/") + }) { + assert_eq!( + overlay_ref.kind.as_deref(), + Some("extension"), + "{} is extension content, not translation content", + overlay_ref.file + ); + let overlay = canonical_yaml::overlay_from_str( + &fs::read_to_string(root.join(&overlay_ref.file)).unwrap(), + ) + .unwrap_or_else(|error| panic!("{} parses: {error}", overlay_ref.file)); + assert_eq!(overlay.kind, OverlayKind::Extension); + assert!( + overlay.translations.is_none(), + "{} has field_fills rather than a translation dictionary", + overlay_ref.file + ); + assert!( + !overlay.note_changes.is_empty(), + "{} lowers field_fills into checked note field changes", + overlay_ref.file + ); + } + + for overlay_ref in manifest + .overlays + .values() + .filter(|overlay| overlay.file.starts_with("overlays/variants/extended/")) + { + let overlay = canonical_yaml::overlay_from_str( + &fs::read_to_string(root.join(&overlay_ref.file)).unwrap(), + ) + .unwrap_or_else(|error| panic!("{} parses: {error}", overlay_ref.file)); + assert!( + overlay + .note_type_changes + .values() + .all(|change| change.card_templates.is_empty()), + "{} carries only language-specific extended metadata; shared card templates live in overlays/variants/extended.yaml", + overlay_ref.file + ); + } +} + +#[test] +fn ultimate_geography_fixture_exports_match_release_oracle_semantics_when_available() { + let oracle_root = ultimate_geography_release_oracle_root(); + if !oracle_root + .join("Ultimate Geography [EN]/deck.json") + .exists() + { + eprintln!( + "skipping Ultimate Geography release parity check; {} is missing. Run `scripts/fetch_ug_release_oracle.py --tag v5.3` or set BRAINBREW_UG_CROWDANKI_ORACLE to a CrowdAnki oracle root.", + oracle_root.display() + ); + return; + } + + let root = fixture_root(); + let manifest = read_manifest(&root); + for target in manifest + .targets + .keys() + .filter(|target| matches!(target_parts(target).1, "standard" | "extended")) + { + let deck = compose_target(&root, &manifest, target); + let export = crowdanki::export_deck(&deck) + .unwrap_or_else(|error| panic!("{target} exports to CrowdAnki: {error}")); + let new: serde_json::Value = serde_json::from_str(&export.deck_json).unwrap(); + let old: serde_json::Value = serde_json::from_str( + &fs::read_to_string(release_oracle_deck_json_path(&oracle_root, target)).unwrap(), + ) + .unwrap(); + + assert_crowdanki_semantic_subset_eq(&old, &new, target); + } +} + +#[test] +fn ug_regression_deck_metadata_changes_flow_to_crowdanki_for_every_target() { + assert_all_targets_export_exact_diffs("deck name", |target, deck, _json| { + let new_name = format!("Regression Deck {target}"); + let mut deck_change = empty_deck_change(); + deck_change.name = Some(replace_property(new_name.clone(), deck.name.clone())); + let mut overlay = empty_overlay(&format!("overlay.regression.deck-name.{target}")); + overlay.deck_change = Some(deck_change); + MutationExpectation::new(overlay, vec![expected_json("/name", new_name)]) + }); + + assert_all_targets_export_exact_diffs("deck description", |target, deck, _json| { + let new_description = format!("Regression description for {target}"); + let mut deck_change = empty_deck_change(); + deck_change.description = Some(replace_property( + new_description.clone(), + deck.description.clone(), + )); + let mut overlay = empty_overlay(&format!("overlay.regression.deck-description.{target}")); + overlay.deck_change = Some(deck_change); + MutationExpectation::new(overlay, vec![expected_json("/desc", new_description)]) + }); + + assert_all_targets_export_exact_diffs("deck uuid", |target, deck, _json| { + let current_uuid = deck + .adapter_ids + .get("crowdanki:uuid") + .expect("UG fixture deck has CrowdAnki UUID") + .to_owned(); + let new_uuid = "11111111-1111-1111-1111-111111111111".to_owned(); + let mut deck_change = empty_deck_change(); + deck_change.adapter_ids.insert( + "crowdanki:uuid".to_owned(), + replace_adapter_id(new_uuid.clone(), current_uuid), + ); + let mut overlay = empty_overlay(&format!("overlay.regression.deck-uuid.{target}")); + overlay.deck_change = Some(deck_change); + MutationExpectation::new(overlay, vec![expected_json("/crowdanki_uuid", new_uuid)]) + }); + + assert_all_targets_export_exact_diffs("deck config name", |target, deck, _json| { + let current_name = deck + .adapter_ids + .get("crowdanki:deck_config_name") + .expect("UG fixture deck has CrowdAnki deck config name") + .to_owned(); + let new_name = format!("Regression Deck Config {target}"); + let mut deck_change = empty_deck_change(); + deck_change.adapter_ids.insert( + "crowdanki:deck_config_name".to_owned(), + replace_adapter_id(new_name.clone(), current_name), + ); + let mut overlay = empty_overlay(&format!("overlay.regression.deck-config-name.{target}")); + overlay.deck_change = Some(deck_change); + MutationExpectation::new( + overlay, + vec![expected_json("/deck_configurations/0/name", new_name)], + ) + }); + + assert_all_targets_export_exact_diffs("deck config uuid", |target, deck, _json| { + let current_uuid = deck + .adapter_ids + .get("crowdanki:deck_config_uuid") + .expect("UG fixture deck has CrowdAnki deck config UUID") + .to_owned(); + let new_uuid = "33333333-3333-3333-3333-333333333333".to_owned(); + let mut deck_change = empty_deck_change(); + deck_change.adapter_ids.insert( + "crowdanki:deck_config_uuid".to_owned(), + replace_adapter_id(new_uuid.clone(), current_uuid), + ); + let mut overlay = empty_overlay(&format!("overlay.regression.deck-config-uuid.{target}")); + overlay.deck_change = Some(deck_change); + MutationExpectation::new( + overlay, + vec![ + expected_json("/deck_config_uuid", new_uuid.clone()), + expected_json("/deck_configurations/0/crowdanki_uuid", new_uuid), + ], + ) + }); +} + +#[test] +fn ug_regression_note_type_changes_flow_to_crowdanki_for_every_target() { + assert_all_targets_export_exact_diffs("note type name", |target, deck, _json| { + let note_type = ug_note_type(deck); + let new_name = format!("Regression Note Type {target}"); + let mut note_type_change = empty_note_type_change(); + note_type_change.name = Some(replace_property(new_name.clone(), note_type.name.clone())); + let mut overlay = empty_overlay(&format!("overlay.regression.note-type-name.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + MutationExpectation::new( + overlay, + vec![expected_json("/note_models/0/name", new_name)], + ) + }); + + assert_all_targets_export_exact_diffs( + "note type variable rendered name", + |target, deck, _json| { + let note_type = ug_note_type(deck); + let old_source_name = note_type + .variables + .get("note-type.name") + .expect("UG note type has source name variable") + .clone(); + let suffix = note_type + .variables + .get("variant.name-suffix") + .map(String::as_str) + .unwrap_or_default(); + let new_source_name = format!("Regression Variable Name {target}"); + let mut note_type_change = empty_note_type_change(); + note_type_change.variables.insert( + "note-type.name".to_owned(), + replace_property(new_source_name.clone(), old_source_name), + ); + let mut overlay = + empty_overlay(&format!("overlay.regression.note-type-variable.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + MutationExpectation::new( + overlay, + vec![expected_json( + "/note_models/0/name", + format!("{new_source_name}{suffix}"), + )], + ) + }, + ); + + assert_all_targets_export_exact_diffs( + "note type variable rendered templates", + |target, deck, baseline_json| { + let note_type = ug_note_type(deck); + let old_label = note_type + .variables + .get("label.capital") + .expect("UG note type has capital label variable") + .clone(); + let new_label = format!("Regression Capital Label {target}"); + let mut note_type_change = empty_note_type_change(); + note_type_change.variables.insert( + "label.capital".to_owned(), + replace_property(new_label.clone(), old_label.clone()), + ); + let mut overlay = empty_overlay(&format!( + "overlay.regression.note-type-template-variable.{target}" + )); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + + let mut expected = Vec::new(); + for (template_index, template) in baseline_json["note_models"][0]["tmpls"] + .as_array() + .unwrap() + .iter() + .enumerate() + { + for property in ["qfmt", "afmt"] { + let current = template[property].as_str().unwrap(); + let old_fragment = format!("
{old_label}
"); + if current.contains(&old_fragment) { + let new_fragment = format!("
{new_label}
"); + expected.push(expected_json( + &format!("/note_models/0/tmpls/{template_index}/{property}"), + current.replace(&old_fragment, &new_fragment), + )); + } + } + } + MutationExpectation::new(overlay, expected) + }, + ); + + assert_all_targets_export_exact_diffs("note type styling", |target, deck, _json| { + let note_type = ug_note_type(deck); + let new_css = format!(".card {{ color: #123456; }} /* {target} */"); + let mut note_type_change = empty_note_type_change(); + note_type_change.styling = + Some(replace_property(new_css.clone(), note_type.styling.clone())); + let mut overlay = empty_overlay(&format!("overlay.regression.note-type-styling.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + MutationExpectation::new(overlay, vec![expected_json("/note_models/0/css", new_css)]) + }); + + assert_all_targets_export_exact_diffs("note type uuid", |target, deck, baseline_json| { + let note_type = ug_note_type(deck); + let current_uuid = note_type + .adapter_ids + .get("crowdanki:uuid") + .expect("UG note type has CrowdAnki UUID") + .to_owned(); + let new_uuid = "22222222-2222-2222-2222-222222222222".to_owned(); + let mut note_type_change = empty_note_type_change(); + note_type_change.adapter_ids.insert( + "crowdanki:uuid".to_owned(), + replace_adapter_id(new_uuid.clone(), current_uuid), + ); + let mut overlay = empty_overlay(&format!("overlay.regression.note-type-uuid.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + + let mut expected = vec![expected_json( + "/note_models/0/crowdanki_uuid", + new_uuid.clone(), + )]; + for note_index in 0..baseline_json["notes"].as_array().unwrap().len() { + expected.push(expected_json( + &format!("/notes/{note_index}/note_model_uuid"), + new_uuid.clone(), + )); + } + MutationExpectation::new(overlay, expected) + }); +} + +#[test] +fn ug_regression_field_definition_changes_flow_to_crowdanki_for_every_target() { + assert_all_targets_export_exact_diffs("field definition name", |target, deck, _json| { + let note_type = ug_note_type(deck); + let field_id = sid("field.capital"); + assert!( + note_type.fields.iter().any(|field| field.id == field_id), + "UG note type has capital field" + ); + let new_name = format!("Regression Capital Field {target}"); + let mut note_type_change = empty_note_type_change(); + note_type_change.fields.insert( + field_id.clone(), + FieldDefinitionChange { + intent: ChangeIntent::Override, + field: Some(FieldDefinition { + id: field_id, + name: new_name.clone(), + }), + expected_base: Some(ExpectedBase::EntityPresent), + }, + ); + let mut overlay = empty_overlay(&format!("overlay.regression.field-name.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + MutationExpectation::new( + overlay, + vec![expected_json( + &format!( + "/note_models/0/flds/{}/name", + field_index(deck, "field.capital") + ), + new_name, + )], + ) + }); + + assert_all_targets_export_exact_diffs("field addition", |target, deck, baseline_json| { + let note_type = ug_note_type(deck); + let field_id = sid("field.zzz-regression"); + let field_name = format!("Regression Added Field {target}"); + let field_count = note_type.fields.len(); + let finland_guid = note_guid(deck, "note.finland"); + let finland_value = format!("Regression field value {target}"); + + let mut note_type_change = empty_note_type_change(); + note_type_change.fields.insert( + field_id.clone(), + FieldDefinitionChange { + intent: ChangeIntent::Add, + field: Some(FieldDefinition { + id: field_id.clone(), + name: field_name.clone(), + }), + expected_base: None, + }, + ); + + let mut note_change = empty_note_change(); + note_change.fields.insert( + field_id, + FieldChange { + intent: ChangeIntent::Add, + value: Some(finland_value.clone()), + expected_base: None, + }, + ); + + let mut overlay = empty_overlay(&format!("overlay.regression.field-addition.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + overlay + .note_changes + .insert(sid("note.finland"), note_change); + + let mut expected = vec![ExpectedJsonValue { + path: format!("/note_models/0/flds/{field_count}"), + value: Some(serde_json::json!({ + "font": "Arial", + "media": [], + "name": field_name, + "ord": field_count, + "rtl": false, + "size": 20, + "sticky": false, + })), + }]; + for (note_index, note) in baseline_json["notes"] + .as_array() + .unwrap() + .iter() + .enumerate() + { + let value = if note["guid"].as_str() == Some(finland_guid.as_str()) { + finland_value.clone() + } else { + String::new() + }; + expected.push(expected_json( + &format!("/notes/{note_index}/fields/{field_count}"), + value, + )); + } + MutationExpectation::new(overlay, expected) + }); +} + +#[test] +fn ug_regression_card_template_changes_flow_to_crowdanki_for_every_target() { + assert_all_targets_export_exact_diffs("template name", |target, deck, _json| { + let note_type = ug_note_type(deck); + let template_id = sid("template.country-capital"); + let template = note_type + .card_templates + .iter() + .find(|template| template.id == template_id) + .expect("UG note type has Country - Capital template"); + let new_name = format!("Regression Template Name {target}"); + let mut template_change = empty_card_template_change(); + template_change.name = Some(replace_property(new_name.clone(), template.name.clone())); + let mut note_type_change = empty_note_type_change(); + note_type_change + .card_templates + .insert(template_id, template_change); + let mut overlay = empty_overlay(&format!("overlay.regression.template-name.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + MutationExpectation::new( + overlay, + vec![expected_json( + &format!( + "/note_models/0/tmpls/{}/name", + template_index(deck, "template.country-capital") + ), + new_name, + )], + ) + }); + + assert_all_targets_export_exact_diffs("template question", |target, deck, _json| { + let note_type = ug_note_type(deck); + let template_id = sid("template.country-capital"); + let template = note_type + .card_templates + .iter() + .find(|template| template.id == template_id) + .expect("UG note type has Country - Capital template"); + let new_question = format!("
Regression question {target}
"); + let mut template_change = empty_card_template_change(); + template_change.question_format = Some(replace_property( + new_question.clone(), + template.question_format.clone(), + )); + let mut note_type_change = empty_note_type_change(); + note_type_change + .card_templates + .insert(template_id, template_change); + let mut overlay = empty_overlay(&format!("overlay.regression.template-question.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + MutationExpectation::new( + overlay, + vec![expected_json( + &format!( + "/note_models/0/tmpls/{}/qfmt", + template_index(deck, "template.country-capital") + ), + new_question, + )], + ) + }); + + assert_all_targets_export_exact_diffs("template answer", |target, deck, _json| { + let note_type = ug_note_type(deck); + let template_id = sid("template.country-capital"); + let template = note_type + .card_templates + .iter() + .find(|template| template.id == template_id) + .expect("UG note type has Country - Capital template"); + let new_answer = format!("
Regression answer {target}
"); + let mut template_change = empty_card_template_change(); + template_change.answer_format = Some(replace_property( + new_answer.clone(), + template.answer_format.clone(), + )); + let mut note_type_change = empty_note_type_change(); + note_type_change + .card_templates + .insert(template_id, template_change); + let mut overlay = empty_overlay(&format!("overlay.regression.template-answer.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + MutationExpectation::new( + overlay, + vec![expected_json( + &format!( + "/note_models/0/tmpls/{}/afmt", + template_index(deck, "template.country-capital") + ), + new_answer, + )], + ) + }); + + assert_all_targets_export_exact_diffs( + "template variable rendered question", + |target, deck, _json| { + let note_type = ug_note_type(deck); + let template_id = sid("template.country-capital"); + let template = note_type + .card_templates + .iter() + .find(|template| template.id == template_id) + .expect("UG note type has Country - Capital template"); + let rendered_value = format!("Regression template variable {target}"); + let mut template_change = empty_card_template_change(); + template_change.variables.insert( + "regression.template".to_owned(), + PropertyChange { + intent: ChangeIntent::Add, + value: Some(rendered_value.clone()), + expected_base: None, + }, + ); + template_change.question_format = Some(replace_property( + "${regression.template}".to_owned(), + template.question_format.clone(), + )); + let mut note_type_change = empty_note_type_change(); + note_type_change + .card_templates + .insert(template_id, template_change); + let mut overlay = + empty_overlay(&format!("overlay.regression.template-variable.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + MutationExpectation::new( + overlay, + vec![expected_json( + &format!( + "/note_models/0/tmpls/{}/qfmt", + template_index(deck, "template.country-capital") + ), + rendered_value, + )], + ) + }, + ); + + assert_all_targets_export_exact_diffs("template addition", |target, deck, _json| { + let note_type = ug_note_type(deck); + let template_count = note_type.card_templates.len(); + let template_id = sid("template.zzz-regression"); + let mut template_change = empty_card_template_change(); + template_change.intent = ChangeIntent::Add; + template_change.insert_after = note_type + .card_templates + .last() + .map(|template| template.id.clone()); + template_change.template = Some(brain_brew_formats::core::CardTemplate { + id: template_id.clone(), + name: format!("Regression Added Template {target}"), + variables: BTreeMap::new(), + question_format: format!("Regression added question {target}"), + answer_format: format!("Regression added answer {target}"), + adapter_ids: AdapterIds::new(), + }); + let expected_template = template_change.template.as_ref().unwrap().clone(); + let mut note_type_change = empty_note_type_change(); + note_type_change + .card_templates + .insert(template_id, template_change); + let mut overlay = empty_overlay(&format!("overlay.regression.template-addition.{target}")); + overlay + .note_type_changes + .insert(note_type.id.clone(), note_type_change); + MutationExpectation::new( + overlay, + vec![ExpectedJsonValue { + path: format!("/note_models/0/tmpls/{template_count}"), + value: Some(serde_json::json!({ + "afmt": expected_template.answer_format, + "bafmt": "", + "bfont": "", + "bqfmt": "", + "bsize": 0, + "did": null, + "name": expected_template.name, + "ord": template_count, + "qfmt": expected_template.question_format, + "scratchPad": 0, + })), + }], + ) + }); +} + +#[test] +fn ug_regression_note_changes_flow_to_crowdanki_for_every_target() { + assert_all_targets_export_exact_diffs("note field", |target, deck, _json| { + let note_id = sid("note.finland"); + let field_id = sid("field.capital"); + let current_value = deck.notes[¬e_id].fields[&field_id].clone(); + let new_value = format!("Regression capital {target}"); + let mut note_change = empty_note_change(); + note_change.fields.insert( + field_id, + FieldChange { + intent: ChangeIntent::Override, + value: Some(new_value.clone()), + expected_base: Some(ExpectedBase::Value(current_value)), + }, + ); + let mut overlay = empty_overlay(&format!("overlay.regression.note-field.{target}")); + overlay.note_changes.insert(note_id, note_change); + MutationExpectation::new( + overlay, + vec![expected_json( + ¬e_field_json_path(deck, "note.finland", "field.capital"), + new_value, + )], + ) + }); + + assert_all_targets_export_exact_diffs("note variable rendered field", |target, deck, _json| { + let note_id = sid("note.finland"); + let field_id = sid("field.country"); + let current_value = deck.notes[¬e_id].fields[&field_id].clone(); + let rendered_value = format!("Regression rendered country {target}"); + let mut note_change = empty_note_change(); + note_change.variables.insert( + "regression.country".to_owned(), + PropertyChange { + intent: ChangeIntent::Add, + value: Some(rendered_value.clone()), + expected_base: None, + }, + ); + note_change.fields.insert( + field_id, + FieldChange { + intent: ChangeIntent::Override, + value: Some("${regression.country}".to_owned()), + expected_base: Some(ExpectedBase::Value(current_value)), + }, + ); + let mut overlay = empty_overlay(&format!("overlay.regression.note-variable.{target}")); + overlay.note_changes.insert(note_id, note_change); + MutationExpectation::new( + overlay, + vec![expected_json( + ¬e_field_json_path(deck, "note.finland", "field.country"), + rendered_value, + )], + ) + }); + + assert_all_targets_export_exact_diffs("note tag", |target, deck, baseline_json| { + let tag = "ZZZ::Regression".to_owned(); + let mut note_change = empty_note_change(); + note_change.tags.insert( + tag.clone(), + TagChange { + intent: ChangeIntent::Add, + expected_base: None, + }, + ); + let mut overlay = empty_overlay(&format!("overlay.regression.note-tag.{target}")); + overlay + .note_changes + .insert(sid("note.finland"), note_change); + let note_index = note_json_index(baseline_json, ¬e_guid(deck, "note.finland")); + let tag_index = baseline_json["notes"][note_index]["tags"] + .as_array() + .unwrap() + .len(); + MutationExpectation::new( + overlay, + vec![expected_json( + &format!("/notes/{note_index}/tags/{tag_index}"), + tag, + )], + ) + }); + + assert_all_targets_export_exact_diffs("note guid", |target, deck, baseline_json| { + let note_id = sid("note.finland"); + let current_guid = note_guid(deck, "note.finland"); + let new_guid = format!("regression-guid-{target}"); + let mut note_change = empty_note_change(); + note_change.adapter_ids.insert( + "crowdanki:guid".to_owned(), + replace_adapter_id(new_guid.clone(), current_guid.clone()), + ); + let mut overlay = empty_overlay(&format!("overlay.regression.note-guid.{target}")); + overlay.note_changes.insert(note_id, note_change); + let note_index = note_json_index(baseline_json, ¤t_guid); + MutationExpectation::new( + overlay, + vec![expected_json( + &format!("/notes/{note_index}/guid"), + new_guid, + )], + ) + }); + + assert_all_targets_export_exact_diffs("note removal", |target, deck, baseline_json| { + let (note_id, note) = deck + .notes + .iter() + .rev() + .find(|(note_id, _)| !deck.tombstones.contains(note_id)) + .expect("UG fixture has at least one exported note"); + let note_index = baseline_json["notes"].as_array().unwrap().len() - 1; + assert_eq!( + baseline_json["notes"][note_index]["guid"].as_str(), + note.adapter_ids.get("crowdanki:guid"), + "test removes the final exported note so the exact CrowdAnki diff is one missing array item" + ); + let mut note_change = empty_note_change(); + note_change.intent = ChangeIntent::Remove; + note_change.expected_base = Some(ExpectedBase::EntityPresent); + let mut overlay = empty_overlay(&format!("overlay.regression.note-removal.{target}")); + overlay.note_changes.insert(note_id.clone(), note_change); + MutationExpectation::new( + overlay, + vec![expected_absent(&format!("/notes/{note_index}"))], + ) + }); + + assert_all_targets_export_exact_diffs("note addition", |target, deck, baseline_json| { + let note_type = ug_note_type(deck); + let note_id = sid("note.zzz-regression"); + let guid = format!("regression-added-note-{target}"); + let mut adapter_ids = AdapterIds::new(); + adapter_ids.insert("crowdanki:guid", guid.clone()); + let fields = note_type + .fields + .iter() + .map(|field| { + ( + field.id.clone(), + format!("Regression {} {target}", field.name), + ) + }) + .collect::>(); + let expected_fields = note_type + .fields + .iter() + .map(|field| fields[&field.id].clone()) + .collect::>(); + let note = Note { + id: note_id.clone(), + note_type_id: note_type.id.clone(), + variables: BTreeMap::new(), + fields, + tags: BTreeSet::from(["ZZZ::Regression".to_owned()]), + adapter_ids, + }; + let mut note_change = empty_note_change(); + note_change.intent = ChangeIntent::Add; + note_change.note = Some(note); + let mut overlay = empty_overlay(&format!("overlay.regression.note-addition.{target}")); + overlay.note_changes.insert(note_id, note_change); + let note_index = baseline_json["notes"].as_array().unwrap().len(); + MutationExpectation::new( + overlay, + vec![ExpectedJsonValue { + path: format!("/notes/{note_index}"), + value: Some(serde_json::json!({ + "__type__": "Note", + "data": "", + "fields": expected_fields, + "flags": 0, + "guid": guid, + "note_model_uuid": baseline_json["note_models"][0]["crowdanki_uuid"].as_str().unwrap(), + "tags": ["ZZZ::Regression"], + })), + }], + ) + }); +} + +#[test] +fn ug_regression_media_changes_flow_to_crowdanki_for_every_target() { + assert_all_targets_export_exact_diffs("media addition", |target, _deck, baseline_json| { + let path = format!("zzzz-regression-{target}.css"); + let mut overlay = empty_overlay(&format!("overlay.regression.media-addition.{target}")); + overlay.media_changes.insert( + sid("media.zzzz-regression"), + MediaChange { + intent: ChangeIntent::Add, + media: Some(MediaReference { + id: sid("media.zzzz-regression"), + path: path.clone(), + sha256: String::new(), + }), + expected_base: None, + }, + ); + let media_index = baseline_json["media_files"].as_array().unwrap().len(); + MutationExpectation::new( + overlay, + vec![expected_json(&format!("/media_files/{media_index}"), path)], + ) + }); + + assert_all_targets_export_exact_diffs("media path override", |target, deck, baseline_json| { + let (media_id, media) = deck.media.iter().next_back().expect("UG fixture has media"); + let media_index = baseline_json["media_files"].as_array().unwrap().len() - 1; + assert_eq!( + baseline_json["media_files"][media_index].as_str(), + Some(media.path.as_str()), + "test updates the final exported media path so the exact CrowdAnki diff is one array item" + ); + let new_path = format!("zzzz-regression-override-{target}.css"); + let mut overlay = empty_overlay(&format!("overlay.regression.media-path.{target}")); + overlay.media_changes.insert( + media_id.clone(), + MediaChange { + intent: ChangeIntent::Override, + media: Some(MediaReference { + id: media_id.clone(), + path: new_path.clone(), + sha256: media.sha256.clone(), + }), + expected_base: Some(ExpectedBase::EntityPresent), + }, + ); + MutationExpectation::new( + overlay, + vec![expected_json( + &format!("/media_files/{media_index}"), + new_path, + )], + ) + }); +} + +fn compose_target( + root: &Path, + manifest: &manifest::FederatedDeckManifest, + target: &str, +) -> CanonicalDeck { + compose_target_with_extra_overlay(root, manifest, target, None) +} + +fn compose_target_with_extra_overlay( + root: &Path, + manifest: &manifest::FederatedDeckManifest, + target: &str, + extra_overlay: Option, +) -> CanonicalDeck { + let expanded = manifest + .expand_target(target) + .unwrap_or_else(|error| panic!("{target} expands: {error}")); + let base = canonical_yaml::from_str(&fs::read_to_string(root.join(&expanded.base)).unwrap()) + .unwrap_or_else(|error| panic!("{target} base parses: {error}")); + let mut overlays = expanded + .overlays + .iter() + .map(|overlay| { + canonical_yaml::overlay_from_str(&fs::read_to_string(root.join(&overlay.file)).unwrap()) + .unwrap_or_else(|error| panic!("{target} overlay {} parses: {error}", overlay.id)) + }) + .collect::>(); + if let Some(extra_overlay) = extra_overlay { + overlays.push(extra_overlay); + } + base.compose(&overlays) + .unwrap_or_else(|error| panic!("{target} composes: {error}")) +} + +struct MutationExpectation { + overlay: Overlay, + expected: Vec, +} + +impl MutationExpectation { + fn new(overlay: Overlay, expected: Vec) -> Self { + Self { overlay, expected } + } +} + +struct ExpectedJsonValue { + path: String, + value: Option, +} + +fn assert_all_targets_export_exact_diffs( + case_name: &str, + build: impl Fn(&str, &CanonicalDeck, &serde_json::Value) -> MutationExpectation, +) { + let root = fixture_root(); + let manifest = read_manifest(&root); + for target in manifest.targets.keys() { + let baseline_deck = compose_target(&root, &manifest, target); + let baseline_json = exported_json(&baseline_deck); + let expectation = build(target, &baseline_deck, &baseline_json); + assert!( + !expectation.expected.is_empty(), + "{case_name} for {target} must expect at least one CrowdAnki difference" + ); + + let changed_deck = + compose_target_with_extra_overlay(&root, &manifest, target, Some(expectation.overlay)); + let changed_json = exported_json(&changed_deck); + let actual_paths = json_diff_paths(&baseline_json, &changed_json); + let expected_paths = expectation + .expected + .iter() + .map(|expected| expected.path.clone()) + .collect::>(); + assert_eq!( + actual_paths, expected_paths, + "{case_name} for {target} changed unexpected CrowdAnki JSON paths" + ); + for expected in expectation.expected { + match expected.value { + Some(value) => assert_eq!( + changed_json.pointer(&expected.path), + Some(&value), + "{case_name} for {target} expected CrowdAnki value at {}", + expected.path + ), + None => assert!( + changed_json.pointer(&expected.path).is_none(), + "{case_name} for {target} expected no CrowdAnki value at {}", + expected.path + ), + } + } + } +} + +fn exported_json(deck: &CanonicalDeck) -> serde_json::Value { + let export = crowdanki::export_deck(deck).expect("deck exports to CrowdAnki"); + serde_json::from_str(&export.deck_json).expect("CrowdAnki export is JSON") +} + +fn json_diff_paths(left: &serde_json::Value, right: &serde_json::Value) -> BTreeSet { + let mut paths = BTreeSet::new(); + collect_json_diff_paths(left, right, "", &mut paths); + paths +} + +fn collect_json_diff_paths( + left: &serde_json::Value, + right: &serde_json::Value, + path: &str, + paths: &mut BTreeSet, +) { + match (left, right) { + (serde_json::Value::Object(left), serde_json::Value::Object(right)) => { + let keys = left.keys().chain(right.keys()).collect::>(); + for key in keys { + let child_path = format!("{path}/{}", json_pointer_token(key)); + match (left.get(key), right.get(key)) { + (Some(left), Some(right)) => { + collect_json_diff_paths(left, right, &child_path, paths) + } + _ => { + paths.insert(child_path); + } + } + } + } + (serde_json::Value::Array(left), serde_json::Value::Array(right)) => { + for index in 0..left.len().max(right.len()) { + let child_path = format!("{path}/{index}"); + match (left.get(index), right.get(index)) { + (Some(left), Some(right)) => { + collect_json_diff_paths(left, right, &child_path, paths) + } + _ => { + paths.insert(child_path); + } + } + } + } + _ if left == right => {} + _ => { + paths.insert(if path.is_empty() { + "/".to_owned() + } else { + path.to_owned() + }); + } + } +} + +fn json_pointer_token(token: &str) -> String { + token.replace('~', "~0").replace('/', "~1") +} + +fn expected_json(path: &str, value: impl Into) -> ExpectedJsonValue { + ExpectedJsonValue { + path: path.to_owned(), + value: Some(value.into()), + } +} + +fn expected_absent(path: &str) -> ExpectedJsonValue { + ExpectedJsonValue { + path: path.to_owned(), + value: None, + } +} + +fn empty_overlay(id: &str) -> Overlay { + Overlay { + id: sid(id), + kind: OverlayKind::Patch, + translations: None, + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + } +} + +fn empty_deck_change() -> DeckChange { + DeckChange { + name: None, + description: None, + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + } +} + +fn empty_note_type_change() -> NoteTypeChange { + NoteTypeChange { + intent: ChangeIntent::Merge, + note_type: None, + name: None, + variables: BTreeMap::new(), + styling: None, + fields: BTreeMap::new(), + card_templates: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + } +} + +fn empty_card_template_change() -> CardTemplateChange { + CardTemplateChange { + intent: ChangeIntent::Merge, + template: None, + insert_after: None, + name: None, + variables: BTreeMap::new(), + question_format: None, + answer_format: None, + adapter_ids: BTreeMap::new(), + expected_base: None, + } +} + +fn empty_note_change() -> NoteChange { + NoteChange { + intent: ChangeIntent::Merge, + note: None, + variables: BTreeMap::new(), + fields: BTreeMap::new(), + tags: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + expected_base: None, + } +} + +fn replace_property(value: String, expected_base: String) -> PropertyChange { + PropertyChange { + intent: ChangeIntent::Override, + value: Some(value), + expected_base: Some(ExpectedBase::Value(expected_base)), + } +} + +fn replace_adapter_id(value: String, expected_base: String) -> AdapterIdChange { + AdapterIdChange { + intent: ChangeIntent::Override, + value: Some(value), + expected_base: Some(ExpectedBase::Value(expected_base)), + } +} + +fn ug_note_type(deck: &CanonicalDeck) -> &brain_brew_formats::core::NoteType { + deck.note_types + .get(&sid("note-type.ultimate-geography")) + .expect("UG fixture has ultimate geography note type") +} + +fn field_index(deck: &CanonicalDeck, field_id: &str) -> usize { + let field_id = sid(field_id); + ug_note_type(deck) + .fields + .iter() + .position(|field| field.id == field_id) + .unwrap_or_else(|| panic!("field {field_id} exists")) +} + +fn template_index(deck: &CanonicalDeck, template_id: &str) -> usize { + let template_id = sid(template_id); + ug_note_type(deck) + .card_templates + .iter() + .position(|template| template.id == template_id) + .unwrap_or_else(|| panic!("template {template_id} exists")) +} + +fn note_guid(deck: &CanonicalDeck, note_id: &str) -> String { + deck.notes[&sid(note_id)] + .adapter_ids + .get("crowdanki:guid") + .unwrap_or_else(|| panic!("{note_id} has CrowdAnki guid")) + .to_owned() +} + +fn note_json_index(json: &serde_json::Value, guid: &str) -> usize { + json["notes"] + .as_array() + .unwrap() + .iter() + .position(|note| note["guid"].as_str() == Some(guid)) + .unwrap_or_else(|| panic!("CrowdAnki note {guid} exists")) +} + +fn note_field_json_path(deck: &CanonicalDeck, note_id: &str, field_id: &str) -> String { + let json = exported_json(deck); + let note_index = note_json_index(&json, ¬e_guid(deck, note_id)); + let field_index = field_index(deck, field_id); + format!("/notes/{note_index}/fields/{field_index}") +} + +fn read_manifest(root: &Path) -> manifest::FederatedDeckManifest { + manifest::from_str(&fs::read_to_string(root.join("brainbrew.yaml")).unwrap()) + .expect("manifest parses") +} + +fn release_oracle_deck_json_path(root: &Path, target: &str) -> PathBuf { + let (language, variant) = target_parts(target); + let suffix = if variant == "extended" { + " [Extended]" + } else { + "" + }; + root.join(format!("Ultimate Geography [{language}]{suffix}/deck.json")) +} + +fn target_parts(target: &str) -> (&str, &str) { + if let Some(variant) = target.strip_prefix("zh-tw-") { + return ("ZH-TW", variant); + } + let (language, variant) = target.split_once('-').unwrap(); + (language.to_ascii_uppercase().leak(), variant) +} + +fn assert_crowdanki_semantic_subset_eq( + old: &serde_json::Value, + new: &serde_json::Value, + target: &str, +) { + assert_eq!(new["name"], old["name"], "{target} deck name"); + assert_eq!( + new["crowdanki_uuid"], old["crowdanki_uuid"], + "{target} deck UUID" + ); + assert_eq!(new["desc"], old["desc"], "{target} deck description"); + + assert_eq!( + string_set(new["media_files"].as_array().unwrap()), + string_set(old["media_files"].as_array().unwrap()), + "{target} media files" + ); + + let old_model = &old["note_models"][0]; + let new_model = &new["note_models"][0]; + assert_eq!(new_model["name"], old_model["name"], "{target} model name"); + assert_eq!( + new_model["crowdanki_uuid"], old_model["crowdanki_uuid"], + "{target} model UUID" + ); + assert_eq!(new_model["css"], old_model["css"], "{target} CSS"); + assert_eq!( + field_names(new_model), + field_names(old_model), + "{target} fields" + ); + assert_eq!( + templates_by_ord(new_model), + templates_by_ord(old_model), + "{target} templates" + ); + + let old_notes = notes_by_guid(old); + let new_notes = notes_by_guid(new); + assert_eq!(new_notes.len(), old_notes.len(), "{target} note count"); + for (guid, old_note) in old_notes { + let new_note = new_notes + .get(&guid) + .unwrap_or_else(|| panic!("{target} missing note {guid}")); + assert_eq!( + new_note["note_model_uuid"], old_note["note_model_uuid"], + "{target} note model differs for {guid}" + ); + assert_eq!( + new_note["fields"], old_note["fields"], + "{target} fields differ for {guid}" + ); + assert_eq!( + string_set(new_note["tags"].as_array().unwrap()), + string_set(old_note["tags"].as_array().unwrap()), + "{target} tags differ for {guid}" + ); + } +} + +fn field_names(model: &serde_json::Value) -> Vec { + model["flds"] + .as_array() + .unwrap() + .iter() + .map(|field| field["name"].as_str().unwrap().to_owned()) + .collect() +} + +fn templates_by_ord(model: &serde_json::Value) -> BTreeMap { + model["tmpls"] + .as_array() + .unwrap() + .iter() + .map(|template| { + ( + template["ord"].as_i64().unwrap(), + ( + template["name"].as_str().unwrap().to_owned(), + template["qfmt"].as_str().unwrap().to_owned(), + template["afmt"].as_str().unwrap().to_owned(), + ), + ) + }) + .collect() +} + +fn notes_by_guid(deck: &serde_json::Value) -> BTreeMap { + deck["notes"] + .as_array() + .unwrap() + .iter() + .map(|note| (note["guid"].as_str().unwrap().to_owned(), note)) + .collect() +} + +fn string_set(values: &[serde_json::Value]) -> BTreeSet { + values + .iter() + .map(|value| value.as_str().unwrap().to_owned()) + .collect() +} + +fn sid(value: &str) -> StableId { + StableId::new(value).expect("test stable id is valid") +} + +fn fixture_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/ultimate-geography") +} + +fn ultimate_geography_release_oracle_root() -> PathBuf { + std::env::var_os("BRAINBREW_UG_CROWDANKI_ORACLE") + .map(PathBuf::from) + .unwrap_or_else(|| { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../.cache/brainbrew/ug-release-oracle/v5.3/crowdanki") + }) +} diff --git a/fixtures/ug-style/brainbrew.yaml b/fixtures/ug-style/brainbrew.yaml new file mode 100644 index 0000000..0390ec5 --- /dev/null +++ b/fixtures/ug-style/brainbrew.yaml @@ -0,0 +1,29 @@ +base: deck.yaml +overlays: + extension.population: + file: extension-population.yaml + kind: extension + patch.netherlands-capital: + file: patch-netherlands-capital.yaml + kind: patch + personal.finland-hint: + file: personal-finland-hint.yaml + kind: personal + tombstone.australia: + file: tombstone-australia.yaml + kind: patch + translation.es: + file: translation-es.yaml + kind: translation + translation.sv: + file: translation-sv.yaml + kind: translation +targets: + full-demo: + overlays: + - translation.es + - translation.sv + - extension.population + - patch.netherlands-capital + - personal.finland-hint + - tombstone.australia diff --git a/fixtures/ug-style/deck.yaml b/fixtures/ug-style/deck.yaml new file mode 100644 index 0000000..f208d73 --- /dev/null +++ b/fixtures/ug-style/deck.yaml @@ -0,0 +1,659 @@ +deck: + id: deck.ultimate-geography-fixture + name: Ultimate Geography Fixture + description: Milestone 1 Ultimate Geography-style fixture. + adapter_ids: + crowdanki:uuid: 11111111-1111-1111-1111-111111111111 +note_types: + note-type.country: + name: Country + field_order: + - field.country + - field.capital + - field.country-es + - field.capital-es + - field.country-sv + - field.capital-sv + - field.capital-hint + - field.flag + - field.map + fields: + field.capital: + name: Capital + field.capital-es: + name: Capital ES + field.capital-hint: + name: Capital hint + field.capital-sv: + name: Capital SV + field.country: + name: Country + field.country-es: + name: Country ES + field.country-sv: + name: Country SV + field.flag: + name: Flag + field.map: + name: Map + card_template_order: + - template.country-capital + - template.capital-country + - template.country-flag + - template.flag-country + - template.country-map-extended + - template.map-country-extended + card_templates: + template.capital-country: + name: Capital - Country + question_format: '{{Capital}}' + answer_format: '{{FrontSide}}
{{Country}}' + adapter_ids: {} + template.country-capital: + name: Country - Capital + question_format: '{{Country}}' + answer_format: '{{FrontSide}}
{{Capital}}' + adapter_ids: {} + template.country-flag: + name: Country - Flag + question_format: '{{Country}}' + answer_format: '{{FrontSide}}
{{Flag}}' + adapter_ids: {} + template.country-map-extended: + name: 'Country - Map [Extended]' + question_format: '{{Country}}' + answer_format: '{{FrontSide}}
{{Map}}' + adapter_ids: {} + template.flag-country: + name: Flag - Country + question_format: '{{Flag}}' + answer_format: '{{FrontSide}}
{{Country}}' + adapter_ids: {} + template.map-country-extended: + name: 'Map - Country [Extended]' + question_format: '{{Map}}' + answer_format: '{{FrontSide}}
{{Country}}' + adapter_ids: {} + styling: | + .card { font-family: sans-serif; } + .extended { color: #2255aa; } + adapter_ids: + crowdanki:uuid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa +notes: + note.australia: + note_type_id: note-type.country + fields: + field.capital: Canberra + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Australia + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Oceania + - Pacific + adapter_ids: + crowdanki:guid: ug-australia-guid + note.austria: + note_type_id: note-type.country + fields: + field.capital: Vienna + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Austria + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Central + - Europe + adapter_ids: + crowdanki:guid: ug-austria-guid + note.belgium: + note_type_id: note-type.country + fields: + field.capital: Brussels + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Belgium + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Western + adapter_ids: + crowdanki:guid: ug-belgium-guid + note.canada: + note_type_id: note-type.country + fields: + field.capital: Ottawa + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Canada + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Americas + - North America + adapter_ids: + crowdanki:guid: ug-canada-guid + note.czechia: + note_type_id: note-type.country + fields: + field.capital: Prague + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Czechia + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Central + - Europe + adapter_ids: + crowdanki:guid: ug-czechia-guid + note.denmark: + note_type_id: note-type.country + fields: + field.capital: Copenhagen + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Denmark + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Nordic + adapter_ids: + crowdanki:guid: ug-denmark-guid + note.estonia: + note_type_id: note-type.country + fields: + field.capital: Tallinn + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Estonia + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Baltic + - Europe + adapter_ids: + crowdanki:guid: ug-estonia-guid + note.finland: + note_type_id: note-type.country + fields: + field.capital: Helsinki + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Finland + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Nordic + adapter_ids: + crowdanki:guid: ug-finland-guid + note.france: + note_type_id: note-type.country + fields: + field.capital: Paris + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: France + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Western + adapter_ids: + crowdanki:guid: ug-france-guid + note.germany: + note_type_id: note-type.country + fields: + field.capital: Berlin + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Germany + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Central + - Europe + adapter_ids: + crowdanki:guid: ug-germany-guid + note.iceland: + note_type_id: note-type.country + fields: + field.capital: Reykjavik + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Iceland + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Nordic + adapter_ids: + crowdanki:guid: ug-iceland-guid + note.ireland: + note_type_id: note-type.country + fields: + field.capital: Dublin + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Ireland + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Western + adapter_ids: + crowdanki:guid: ug-ireland-guid + note.italy: + note_type_id: note-type.country + fields: + field.capital: Rome + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Italy + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Southern + adapter_ids: + crowdanki:guid: ug-italy-guid + note.japan: + note_type_id: note-type.country + fields: + field.capital: Tokyo + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Japan + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Asia + - East Asia + adapter_ids: + crowdanki:guid: ug-japan-guid + note.latvia: + note_type_id: note-type.country + fields: + field.capital: Riga + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Latvia + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Baltic + - Europe + adapter_ids: + crowdanki:guid: ug-latvia-guid + note.lithuania: + note_type_id: note-type.country + fields: + field.capital: Vilnius + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Lithuania + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Baltic + - Europe + adapter_ids: + crowdanki:guid: ug-lithuania-guid + note.mexico: + note_type_id: note-type.country + fields: + field.capital: Mexico City + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Mexico + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Americas + - North America + adapter_ids: + crowdanki:guid: ug-mexico-guid + note.netherlands: + note_type_id: note-type.country + fields: + field.capital: Amsterdam + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Netherlands + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Western + adapter_ids: + crowdanki:guid: ug-netherlands-guid + note.norway: + note_type_id: note-type.country + fields: + field.capital: Oslo + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Norway + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Nordic + adapter_ids: + crowdanki:guid: ug-norway-guid + note.poland: + note_type_id: note-type.country + fields: + field.capital: Warsaw + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Poland + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Central + - Europe + adapter_ids: + crowdanki:guid: ug-poland-guid + note.portugal: + note_type_id: note-type.country + fields: + field.capital: Lisbon + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Portugal + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Southern + adapter_ids: + crowdanki:guid: ug-portugal-guid + note.spain: + note_type_id: note-type.country + fields: + field.capital: Madrid + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Spain + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Southern + adapter_ids: + crowdanki:guid: ug-spain-guid + note.sweden: + note_type_id: note-type.country + fields: + field.capital: Stockholm + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Sweden + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Nordic + adapter_ids: + crowdanki:guid: ug-sweden-guid + note.switzerland: + note_type_id: note-type.country + fields: + field.capital: Bern + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: Switzerland + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Central + - Europe + adapter_ids: + crowdanki:guid: ug-switzerland-guid + note.united-kingdom: + note_type_id: note-type.country + fields: + field.capital: London + field.capital-es: '' + field.capital-hint: '' + field.capital-sv: '' + field.country: United Kingdom + field.country-es: '' + field.country-sv: '' + field.flag: '' + field.map: '' + tags: + - Europe + - Western + adapter_ids: + crowdanki:guid: ug-united-kingdom-guid +media: + media.flags-at-png: + path: flags/at.png + sha256: '' + media.flags-au-png: + path: flags/au.png + sha256: '' + media.flags-be-png: + path: flags/be.png + sha256: '' + media.flags-ca-png: + path: flags/ca.png + sha256: '' + media.flags-ch-png: + path: flags/ch.png + sha256: '' + media.flags-cz-png: + path: flags/cz.png + sha256: '' + media.flags-de-png: + path: flags/de.png + sha256: '' + media.flags-dk-png: + path: flags/dk.png + sha256: '' + media.flags-ee-png: + path: flags/ee.png + sha256: '' + media.flags-es-png: + path: flags/es.png + sha256: '' + media.flags-fi-png: + path: flags/fi.png + sha256: '' + media.flags-fr-png: + path: flags/fr.png + sha256: '' + media.flags-gb-png: + path: flags/gb.png + sha256: '' + media.flags-ie-png: + path: flags/ie.png + sha256: '' + media.flags-is-png: + path: flags/is.png + sha256: '' + media.flags-it-png: + path: flags/it.png + sha256: '' + media.flags-jp-png: + path: flags/jp.png + sha256: '' + media.flags-lt-png: + path: flags/lt.png + sha256: '' + media.flags-lv-png: + path: flags/lv.png + sha256: '' + media.flags-mx-png: + path: flags/mx.png + sha256: '' + media.flags-nl-png: + path: flags/nl.png + sha256: '' + media.flags-no-png: + path: flags/no.png + sha256: '' + media.flags-pl-png: + path: flags/pl.png + sha256: '' + media.flags-pt-png: + path: flags/pt.png + sha256: '' + media.flags-se-png: + path: flags/se.png + sha256: '' + media.maps-at-svg: + path: maps/at.svg + sha256: '' + media.maps-au-svg: + path: maps/au.svg + sha256: '' + media.maps-be-svg: + path: maps/be.svg + sha256: '' + media.maps-ca-svg: + path: maps/ca.svg + sha256: '' + media.maps-ch-svg: + path: maps/ch.svg + sha256: '' + media.maps-cz-svg: + path: maps/cz.svg + sha256: '' + media.maps-de-svg: + path: maps/de.svg + sha256: '' + media.maps-dk-svg: + path: maps/dk.svg + sha256: '' + media.maps-ee-svg: + path: maps/ee.svg + sha256: '' + media.maps-es-svg: + path: maps/es.svg + sha256: '' + media.maps-fi-svg: + path: maps/fi.svg + sha256: '' + media.maps-fr-svg: + path: maps/fr.svg + sha256: '' + media.maps-gb-svg: + path: maps/gb.svg + sha256: '' + media.maps-ie-svg: + path: maps/ie.svg + sha256: '' + media.maps-is-svg: + path: maps/is.svg + sha256: '' + media.maps-it-svg: + path: maps/it.svg + sha256: '' + media.maps-jp-svg: + path: maps/jp.svg + sha256: '' + media.maps-lt-svg: + path: maps/lt.svg + sha256: '' + media.maps-lv-svg: + path: maps/lv.svg + sha256: '' + media.maps-mx-svg: + path: maps/mx.svg + sha256: '' + media.maps-nl-svg: + path: maps/nl.svg + sha256: '' + media.maps-no-svg: + path: maps/no.svg + sha256: '' + media.maps-pl-svg: + path: maps/pl.svg + sha256: '' + media.maps-pt-svg: + path: maps/pt.svg + sha256: '' + media.maps-se-svg: + path: maps/se.svg + sha256: '' +tombstones: [] diff --git a/fixtures/ug-style/extension-population.yaml b/fixtures/ug-style/extension-population.yaml new file mode 100644 index 0000000..c4ce202 --- /dev/null +++ b/fixtures/ug-style/extension-population.yaml @@ -0,0 +1,57 @@ +id: overlay.extension.population +kind: extension +field_additions: + note-type.country: + fields: + field.population: Population + values: + note.australia: + field.population: 25.0 million + note.austria: + field.population: 16.0 million + note.belgium: + field.population: 19.0 million + note.canada: + field.population: 22.0 million + note.czechia: + field.population: 15.0 million + note.denmark: + field.population: 4.0 million + note.estonia: + field.population: 6.0 million + note.finland: + field.population: 1.0 million + note.france: + field.population: 10.0 million + note.germany: + field.population: 9.0 million + note.iceland: + field.population: 5.0 million + note.ireland: + field.population: 20.0 million + note.italy: + field.population: 13.0 million + note.japan: + field.population: 24.0 million + note.latvia: + field.population: 7.0 million + note.lithuania: + field.population: 8.0 million + note.mexico: + field.population: 23.0 million + note.netherlands: + field.population: 18.0 million + note.norway: + field.population: 3.0 million + note.poland: + field.population: 14.0 million + note.portugal: + field.population: 12.0 million + note.spain: + field.population: 11.0 million + note.sweden: + field.population: 2.0 million + note.switzerland: + field.population: 17.0 million + note.united-kingdom: + field.population: 21.0 million diff --git a/fixtures/ug-style/patch-netherlands-capital.yaml b/fixtures/ug-style/patch-netherlands-capital.yaml new file mode 100644 index 0000000..7e0003e --- /dev/null +++ b/fixtures/ug-style/patch-netherlands-capital.yaml @@ -0,0 +1,11 @@ +id: overlay.patch.netherlands-capital +kind: patch +notes: + note.netherlands: + intent: merge + fields: + field.capital: + intent: replace + value: 'Amsterdam (constitutional capital)' + expected_base: + value: Amsterdam diff --git a/fixtures/ug-style/personal-finland-hint.yaml b/fixtures/ug-style/personal-finland-hint.yaml new file mode 100644 index 0000000..667481b --- /dev/null +++ b/fixtures/ug-style/personal-finland-hint.yaml @@ -0,0 +1,9 @@ +id: overlay.personal.finland-hint +kind: personal +notes: + note.finland: + intent: merge + fields: + field.capital-hint: + intent: merge + value: Starts with H diff --git a/fixtures/ug-style/tombstone-australia.yaml b/fixtures/ug-style/tombstone-australia.yaml new file mode 100644 index 0000000..612e5a7 --- /dev/null +++ b/fixtures/ug-style/tombstone-australia.yaml @@ -0,0 +1,6 @@ +id: overlay.patch.remove-australia +kind: patch +notes: + note.australia: + intent: remove + expected_base: entity_present diff --git a/fixtures/ug-style/translation-es.yaml b/fixtures/ug-style/translation-es.yaml new file mode 100644 index 0000000..4ed853e --- /dev/null +++ b/fixtures/ug-style/translation-es.yaml @@ -0,0 +1,228 @@ +id: overlay.translation.es +kind: translation +notes: + note.australia: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Canberra (ES)' + field.country-es: + intent: merge + value: 'Australia (ES)' + note.austria: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Vienna (ES)' + field.country-es: + intent: merge + value: 'Austria (ES)' + note.belgium: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Brussels (ES)' + field.country-es: + intent: merge + value: 'Belgium (ES)' + note.canada: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Ottawa (ES)' + field.country-es: + intent: merge + value: 'Canada (ES)' + note.czechia: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Prague (ES)' + field.country-es: + intent: merge + value: 'Czechia (ES)' + note.denmark: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Copenhagen (ES)' + field.country-es: + intent: merge + value: 'Denmark (ES)' + note.estonia: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Tallinn (ES)' + field.country-es: + intent: merge + value: 'Estonia (ES)' + note.finland: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Helsinki (ES)' + field.country-es: + intent: merge + value: 'Finland (ES)' + note.france: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Paris (ES)' + field.country-es: + intent: merge + value: 'France (ES)' + note.germany: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Berlin (ES)' + field.country-es: + intent: merge + value: 'Germany (ES)' + note.iceland: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Reykjavik (ES)' + field.country-es: + intent: merge + value: 'Iceland (ES)' + note.ireland: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Dublin (ES)' + field.country-es: + intent: merge + value: 'Ireland (ES)' + note.italy: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Rome (ES)' + field.country-es: + intent: merge + value: 'Italy (ES)' + note.japan: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Tokyo (ES)' + field.country-es: + intent: merge + value: 'Japan (ES)' + note.latvia: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Riga (ES)' + field.country-es: + intent: merge + value: 'Latvia (ES)' + note.lithuania: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Vilnius (ES)' + field.country-es: + intent: merge + value: 'Lithuania (ES)' + note.mexico: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Mexico City (ES)' + field.country-es: + intent: merge + value: 'Mexico (ES)' + note.netherlands: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Amsterdam (ES)' + field.country-es: + intent: merge + value: 'Netherlands (ES)' + note.norway: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Oslo (ES)' + field.country-es: + intent: merge + value: 'Norway (ES)' + note.poland: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Warsaw (ES)' + field.country-es: + intent: merge + value: 'Poland (ES)' + note.portugal: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Lisbon (ES)' + field.country-es: + intent: merge + value: 'Portugal (ES)' + note.spain: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Madrid (ES)' + field.country-es: + intent: merge + value: 'Spain (ES)' + note.sweden: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Stockholm (ES)' + field.country-es: + intent: merge + value: 'Sweden (ES)' + note.switzerland: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'Bern (ES)' + field.country-es: + intent: merge + value: 'Switzerland (ES)' + note.united-kingdom: + intent: merge + fields: + field.capital-es: + intent: merge + value: 'London (ES)' + field.country-es: + intent: merge + value: 'United Kingdom (ES)' diff --git a/fixtures/ug-style/translation-sv.yaml b/fixtures/ug-style/translation-sv.yaml new file mode 100644 index 0000000..e3ec8e8 --- /dev/null +++ b/fixtures/ug-style/translation-sv.yaml @@ -0,0 +1,228 @@ +id: overlay.translation.sv +kind: translation +notes: + note.australia: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Canberra (SV)' + field.country-sv: + intent: merge + value: 'Australia (SV)' + note.austria: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Vienna (SV)' + field.country-sv: + intent: merge + value: 'Austria (SV)' + note.belgium: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Brussels (SV)' + field.country-sv: + intent: merge + value: 'Belgium (SV)' + note.canada: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Ottawa (SV)' + field.country-sv: + intent: merge + value: 'Canada (SV)' + note.czechia: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Prague (SV)' + field.country-sv: + intent: merge + value: 'Czechia (SV)' + note.denmark: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Copenhagen (SV)' + field.country-sv: + intent: merge + value: 'Denmark (SV)' + note.estonia: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Tallinn (SV)' + field.country-sv: + intent: merge + value: 'Estonia (SV)' + note.finland: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Helsinki (SV)' + field.country-sv: + intent: merge + value: 'Finland (SV)' + note.france: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Paris (SV)' + field.country-sv: + intent: merge + value: 'France (SV)' + note.germany: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Berlin (SV)' + field.country-sv: + intent: merge + value: 'Germany (SV)' + note.iceland: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Reykjavik (SV)' + field.country-sv: + intent: merge + value: 'Iceland (SV)' + note.ireland: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Dublin (SV)' + field.country-sv: + intent: merge + value: 'Ireland (SV)' + note.italy: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Rome (SV)' + field.country-sv: + intent: merge + value: 'Italy (SV)' + note.japan: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Tokyo (SV)' + field.country-sv: + intent: merge + value: 'Japan (SV)' + note.latvia: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Riga (SV)' + field.country-sv: + intent: merge + value: 'Latvia (SV)' + note.lithuania: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Vilnius (SV)' + field.country-sv: + intent: merge + value: 'Lithuania (SV)' + note.mexico: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Mexico City (SV)' + field.country-sv: + intent: merge + value: 'Mexico (SV)' + note.netherlands: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Amsterdam (SV)' + field.country-sv: + intent: merge + value: 'Netherlands (SV)' + note.norway: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Oslo (SV)' + field.country-sv: + intent: merge + value: 'Norway (SV)' + note.poland: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Warsaw (SV)' + field.country-sv: + intent: merge + value: 'Poland (SV)' + note.portugal: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Lisbon (SV)' + field.country-sv: + intent: merge + value: 'Portugal (SV)' + note.spain: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Madrid (SV)' + field.country-sv: + intent: merge + value: 'Spain (SV)' + note.sweden: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Stockholm (SV)' + field.country-sv: + intent: merge + value: 'Sweden (SV)' + note.switzerland: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'Bern (SV)' + field.country-sv: + intent: merge + value: 'Switzerland (SV)' + note.united-kingdom: + intent: merge + fields: + field.capital-sv: + intent: merge + value: 'London (SV)' + field.country-sv: + intent: merge + value: 'United Kingdom (SV)' diff --git a/fixtures/ultimate-geography/brainbrew.yaml b/fixtures/ultimate-geography/brainbrew.yaml new file mode 100644 index 0000000..a14c2cf --- /dev/null +++ b/fixtures/ultimate-geography/brainbrew.yaml @@ -0,0 +1,581 @@ +package: + id: anki-geo.ultimate-geography + version: 0.1.0 + compatible_base_versions: + - '>=0.1,<0.2' +base: deck.yaml +overlays: + overlay.extension.hardcore: + file: overlays/extensions/hardcore.yaml + kind: extension + overlay.extension.hardcore.field-fills: + file: overlays/extensions/hardcore/field-fills.yaml + kind: extension + depends_on: + - overlay.extension.hardcore + overlay.extension.hardcore.field-fills.de: + file: overlays/extensions/hardcore/field-fills/de.yaml + kind: extension + depends_on: + - overlay.translation.hardcore.de + - overlay.extension.hardcore.field-fills + overlay.extension.hardcore.field-fills.es: + file: overlays/extensions/hardcore/field-fills/es.yaml + kind: extension + depends_on: + - overlay.translation.hardcore.es + - overlay.extension.hardcore.field-fills + overlay.extension.hardcore.field-fills.fr: + file: overlays/extensions/hardcore/field-fills/fr.yaml + kind: extension + depends_on: + - overlay.translation.hardcore.fr + - overlay.extension.hardcore.field-fills + overlay.extension.hardcore.field-fills.nb: + file: overlays/extensions/hardcore/field-fills/nb.yaml + kind: extension + depends_on: + - overlay.translation.hardcore.nb + - overlay.extension.hardcore.field-fills + overlay.translation.cs: + file: overlays/languages/cs.yaml + kind: translation + overlay.translation.da: + file: overlays/languages/da.yaml + kind: translation + overlay.translation.de: + file: overlays/languages/de.yaml + kind: translation + overlay.translation.es: + file: overlays/languages/es.yaml + kind: translation + overlay.translation.fr: + file: overlays/languages/fr.yaml + kind: translation + overlay.translation.hardcore.cs: + file: overlays/extensions/hardcore/translations/cs.yaml + kind: translation + depends_on: + - overlay.translation.cs + - overlay.extension.hardcore + overlay.translation.hardcore.de: + file: overlays/extensions/hardcore/translations/de.yaml + kind: translation + depends_on: + - overlay.translation.de + - overlay.extension.hardcore + overlay.translation.hardcore.es: + file: overlays/extensions/hardcore/translations/es.yaml + kind: translation + depends_on: + - overlay.translation.es + - overlay.extension.hardcore + overlay.translation.hardcore.fr: + file: overlays/extensions/hardcore/translations/fr.yaml + kind: translation + depends_on: + - overlay.translation.fr + - overlay.extension.hardcore + overlay.translation.hardcore.it: + file: overlays/extensions/hardcore/translations/it.yaml + kind: translation + depends_on: + - overlay.translation.it + - overlay.extension.hardcore + overlay.translation.hardcore.nb: + file: overlays/extensions/hardcore/translations/nb.yaml + kind: translation + depends_on: + - overlay.translation.nb + - overlay.extension.hardcore + overlay.translation.hardcore.nl: + file: overlays/extensions/hardcore/translations/nl.yaml + kind: translation + depends_on: + - overlay.translation.nl + - overlay.extension.hardcore + overlay.translation.hardcore.pl: + file: overlays/extensions/hardcore/translations/pl.yaml + kind: translation + depends_on: + - overlay.translation.pl + - overlay.extension.hardcore + overlay.translation.hardcore.pt: + file: overlays/extensions/hardcore/translations/pt.yaml + kind: translation + depends_on: + - overlay.translation.pt + - overlay.extension.hardcore + overlay.translation.hardcore.ru: + file: overlays/extensions/hardcore/translations/ru.yaml + kind: translation + depends_on: + - overlay.translation.ru + - overlay.extension.hardcore + overlay.translation.hardcore.sv: + file: overlays/extensions/hardcore/translations/sv.yaml + kind: translation + depends_on: + - overlay.translation.sv + - overlay.extension.hardcore + overlay.translation.hardcore.zh: + file: overlays/extensions/hardcore/translations/zh.yaml + kind: translation + depends_on: + - overlay.translation.zh + - overlay.extension.hardcore + overlay.translation.it: + file: overlays/languages/it.yaml + kind: translation + overlay.translation.nb: + file: overlays/languages/nb.yaml + kind: translation + overlay.translation.nl: + file: overlays/languages/nl.yaml + kind: translation + overlay.translation.pl: + file: overlays/languages/pl.yaml + kind: translation + overlay.translation.pt: + file: overlays/languages/pt.yaml + kind: translation + overlay.translation.ru: + file: overlays/languages/ru.yaml + kind: translation + overlay.translation.sv: + file: overlays/languages/sv.yaml + kind: translation + overlay.translation.zh: + file: overlays/languages/zh.yaml + kind: translation + overlay.translation.zh-tw: + file: overlays/languages/zh-tw.yaml + kind: translation + overlay.variant.experimental: + file: overlays/variants/experimental.yaml + kind: extension + depends_on: + - overlay.variant.extended + overlay.variant.experimental.cs: + file: overlays/variants/experimental/cs.yaml + kind: extension + depends_on: + - overlay.translation.cs + - overlay.variant.experimental + overlay.variant.experimental.da: + file: overlays/variants/experimental/da.yaml + kind: extension + depends_on: + - overlay.translation.da + - overlay.variant.experimental + overlay.variant.experimental.de: + file: overlays/variants/experimental/de.yaml + kind: extension + depends_on: + - overlay.translation.de + - overlay.variant.experimental + overlay.variant.experimental.en: + file: overlays/variants/experimental/en.yaml + kind: extension + depends_on: + - overlay.variant.experimental + overlay.variant.experimental.es: + file: overlays/variants/experimental/es.yaml + kind: extension + depends_on: + - overlay.translation.es + - overlay.variant.experimental + overlay.variant.experimental.fr: + file: overlays/variants/experimental/fr.yaml + kind: extension + depends_on: + - overlay.translation.fr + - overlay.variant.experimental + overlay.variant.experimental.it: + file: overlays/variants/experimental/it.yaml + kind: extension + depends_on: + - overlay.translation.it + - overlay.variant.experimental + overlay.variant.experimental.nb: + file: overlays/variants/experimental/nb.yaml + kind: extension + depends_on: + - overlay.translation.nb + - overlay.variant.experimental + overlay.variant.experimental.nl: + file: overlays/variants/experimental/nl.yaml + kind: extension + depends_on: + - overlay.translation.nl + - overlay.variant.experimental + overlay.variant.experimental.pl: + file: overlays/variants/experimental/pl.yaml + kind: extension + depends_on: + - overlay.translation.pl + - overlay.variant.experimental + overlay.variant.experimental.pt: + file: overlays/variants/experimental/pt.yaml + kind: extension + depends_on: + - overlay.translation.pt + - overlay.variant.experimental + overlay.variant.experimental.ru: + file: overlays/variants/experimental/ru.yaml + kind: extension + depends_on: + - overlay.translation.ru + - overlay.variant.experimental + overlay.variant.experimental.sv: + file: overlays/variants/experimental/sv.yaml + kind: extension + depends_on: + - overlay.translation.sv + - overlay.variant.experimental + overlay.variant.experimental.zh: + file: overlays/variants/experimental/zh.yaml + kind: extension + depends_on: + - overlay.translation.zh + - overlay.variant.experimental + overlay.variant.experimental.zh-tw: + file: overlays/variants/experimental/zh-tw.yaml + kind: extension + depends_on: + - overlay.translation.zh-tw + - overlay.variant.experimental + overlay.variant.extended: + file: overlays/variants/extended.yaml + kind: extension + overlay.variant.extended.cs: + file: overlays/variants/extended/cs.yaml + kind: extension + depends_on: + - overlay.translation.cs + - overlay.variant.extended + overlay.variant.extended.da: + file: overlays/variants/extended/da.yaml + kind: extension + depends_on: + - overlay.translation.da + - overlay.variant.extended + overlay.variant.extended.de: + file: overlays/variants/extended/de.yaml + kind: extension + depends_on: + - overlay.translation.de + - overlay.variant.extended + overlay.variant.extended.en: + file: overlays/variants/extended/en.yaml + kind: extension + depends_on: + - overlay.variant.extended + overlay.variant.extended.es: + file: overlays/variants/extended/es.yaml + kind: extension + depends_on: + - overlay.translation.es + - overlay.variant.extended + overlay.variant.extended.fr: + file: overlays/variants/extended/fr.yaml + kind: extension + depends_on: + - overlay.translation.fr + - overlay.variant.extended + overlay.variant.extended.it: + file: overlays/variants/extended/it.yaml + kind: extension + depends_on: + - overlay.translation.it + - overlay.variant.extended + overlay.variant.extended.nb: + file: overlays/variants/extended/nb.yaml + kind: extension + depends_on: + - overlay.translation.nb + - overlay.variant.extended + overlay.variant.extended.nl: + file: overlays/variants/extended/nl.yaml + kind: extension + depends_on: + - overlay.translation.nl + - overlay.variant.extended + overlay.variant.extended.pl: + file: overlays/variants/extended/pl.yaml + kind: extension + depends_on: + - overlay.translation.pl + - overlay.variant.extended + overlay.variant.extended.pt: + file: overlays/variants/extended/pt.yaml + kind: extension + depends_on: + - overlay.translation.pt + - overlay.variant.extended + overlay.variant.extended.ru: + file: overlays/variants/extended/ru.yaml + kind: extension + depends_on: + - overlay.translation.ru + - overlay.variant.extended + overlay.variant.extended.sv: + file: overlays/variants/extended/sv.yaml + kind: extension + depends_on: + - overlay.translation.sv + - overlay.variant.extended + overlay.variant.extended.zh: + file: overlays/variants/extended/zh.yaml + kind: extension + depends_on: + - overlay.translation.zh + - overlay.variant.extended + overlay.variant.extended.zh-tw: + file: overlays/variants/extended/zh-tw.yaml + kind: extension + depends_on: + - overlay.translation.zh-tw + - overlay.variant.extended +targets: + cs-experimental: + overlays: + - overlay.variant.experimental.cs + cs-extended: + overlays: + - overlay.variant.extended.cs + cs-hardcore-extended: + overlays: + - overlay.variant.extended.cs + - overlay.translation.hardcore.cs + - overlay.extension.hardcore.field-fills + cs-hardcore-standard: + overlays: + - overlay.translation.hardcore.cs + - overlay.extension.hardcore.field-fills + cs-standard: + overlays: + - overlay.translation.cs + da-experimental: + overlays: + - overlay.variant.experimental.da + da-extended: + overlays: + - overlay.variant.extended.da + da-standard: + overlays: + - overlay.translation.da + de-experimental: + overlays: + - overlay.variant.experimental.de + de-extended: + overlays: + - overlay.variant.extended.de + de-hardcore-extended: + overlays: + - overlay.variant.extended.de + - overlay.extension.hardcore.field-fills.de + de-hardcore-standard: + overlays: + - overlay.extension.hardcore.field-fills.de + de-standard: + overlays: + - overlay.translation.de + en-experimental: + overlays: + - overlay.variant.experimental.en + en-extended: + overlays: + - overlay.variant.extended.en + en-hardcore-extended: + overlays: + - overlay.variant.extended.en + - overlay.extension.hardcore.field-fills + en-hardcore-standard: + overlays: + - overlay.extension.hardcore.field-fills + en-standard: + overlays: [] + es-experimental: + overlays: + - overlay.variant.experimental.es + es-extended: + overlays: + - overlay.variant.extended.es + es-hardcore-extended: + overlays: + - overlay.variant.extended.es + - overlay.extension.hardcore.field-fills.es + es-hardcore-standard: + overlays: + - overlay.extension.hardcore.field-fills.es + es-standard: + overlays: + - overlay.translation.es + fr-experimental: + overlays: + - overlay.variant.experimental.fr + fr-extended: + overlays: + - overlay.variant.extended.fr + fr-hardcore-extended: + overlays: + - overlay.variant.extended.fr + - overlay.extension.hardcore.field-fills.fr + fr-hardcore-standard: + overlays: + - overlay.extension.hardcore.field-fills.fr + fr-standard: + overlays: + - overlay.translation.fr + it-experimental: + overlays: + - overlay.variant.experimental.it + it-extended: + overlays: + - overlay.variant.extended.it + it-hardcore-extended: + overlays: + - overlay.variant.extended.it + - overlay.translation.hardcore.it + - overlay.extension.hardcore.field-fills + it-hardcore-standard: + overlays: + - overlay.translation.hardcore.it + - overlay.extension.hardcore.field-fills + it-standard: + overlays: + - overlay.translation.it + nb-experimental: + overlays: + - overlay.variant.experimental.nb + nb-extended: + overlays: + - overlay.variant.extended.nb + nb-hardcore-extended: + overlays: + - overlay.variant.extended.nb + - overlay.extension.hardcore.field-fills.nb + nb-hardcore-standard: + overlays: + - overlay.extension.hardcore.field-fills.nb + nb-standard: + overlays: + - overlay.translation.nb + nl-experimental: + overlays: + - overlay.variant.experimental.nl + nl-extended: + overlays: + - overlay.variant.extended.nl + nl-hardcore-extended: + overlays: + - overlay.variant.extended.nl + - overlay.translation.hardcore.nl + - overlay.extension.hardcore.field-fills + nl-hardcore-standard: + overlays: + - overlay.translation.hardcore.nl + - overlay.extension.hardcore.field-fills + nl-standard: + overlays: + - overlay.translation.nl + pl-experimental: + overlays: + - overlay.variant.experimental.pl + pl-extended: + overlays: + - overlay.variant.extended.pl + pl-hardcore-extended: + overlays: + - overlay.variant.extended.pl + - overlay.translation.hardcore.pl + - overlay.extension.hardcore.field-fills + pl-hardcore-standard: + overlays: + - overlay.translation.hardcore.pl + - overlay.extension.hardcore.field-fills + pl-standard: + overlays: + - overlay.translation.pl + pt-experimental: + overlays: + - overlay.variant.experimental.pt + pt-extended: + overlays: + - overlay.variant.extended.pt + pt-hardcore-extended: + overlays: + - overlay.variant.extended.pt + - overlay.translation.hardcore.pt + - overlay.extension.hardcore.field-fills + pt-hardcore-standard: + overlays: + - overlay.translation.hardcore.pt + - overlay.extension.hardcore.field-fills + pt-standard: + overlays: + - overlay.translation.pt + ru-experimental: + overlays: + - overlay.variant.experimental.ru + ru-extended: + overlays: + - overlay.variant.extended.ru + ru-hardcore-extended: + overlays: + - overlay.variant.extended.ru + - overlay.translation.hardcore.ru + - overlay.extension.hardcore.field-fills + ru-hardcore-standard: + overlays: + - overlay.translation.hardcore.ru + - overlay.extension.hardcore.field-fills + ru-standard: + overlays: + - overlay.translation.ru + sv-experimental: + overlays: + - overlay.variant.experimental.sv + sv-extended: + overlays: + - overlay.variant.extended.sv + sv-hardcore-extended: + overlays: + - overlay.variant.extended.sv + - overlay.translation.hardcore.sv + - overlay.extension.hardcore.field-fills + sv-hardcore-standard: + overlays: + - overlay.translation.hardcore.sv + - overlay.extension.hardcore.field-fills + sv-standard: + overlays: + - overlay.translation.sv + zh-experimental: + overlays: + - overlay.variant.experimental.zh + zh-extended: + overlays: + - overlay.variant.extended.zh + zh-hardcore-extended: + overlays: + - overlay.variant.extended.zh + - overlay.translation.hardcore.zh + - overlay.extension.hardcore.field-fills + zh-hardcore-standard: + overlays: + - overlay.translation.hardcore.zh + - overlay.extension.hardcore.field-fills + zh-standard: + overlays: + - overlay.translation.zh + zh-tw-experimental: + overlays: + - overlay.variant.experimental.zh-tw + zh-tw-extended: + overlays: + - overlay.variant.extended.zh-tw + zh-tw-standard: + overlays: + - overlay.translation.zh-tw diff --git a/fixtures/ultimate-geography/deck.yaml b/fixtures/ultimate-geography/deck.yaml new file mode 100644 index 0000000..202f954 --- /dev/null +++ b/fixtures/ultimate-geography/deck.yaml @@ -0,0 +1,7036 @@ +deck: + id: deck.ultimate-geography + name: Ultimate Geography + description: | + FULL DESCRIPTION | RELEASE NOTES | CONTRIBUTING + + Ultimate Geography v5.3 features: + + - the world's 205 sovereign states (820 cards) + - 59 territories, world regions, and other entities (103 cards) + - 48 oceans and seas (48 cards, maps only) + - 7 continents (7 cards, maps only) + - for a total of 319 unique notes, 978 cards, 221 flags and 319 maps. + + The deck is available in English, German, Spanish, French, Norwegian, Czech, Russian, Dutch, Swedish, Portuguese, Chinese (simplified and traditional), Polish, Italian and Danish. An extended version is also available in each language. To help with memorisation and provide context while learning, some notes include extra information such as similar flags, governance information, alternative country names, etc. + + You can use Anki's filtered deck feature to focus your study on a subset of the deck, such as sovereign states, a single note template (e.g. map to country), or a specific continent (e.g. Europe). + + This deck is maintained on GitHub. If you spot a mistake, have a suggestion or want to help, please don't hesitate to open an issue. Want to stay informed of new releases? Watch the GitHub repository or subscribe to the releases feed! + adapter_ids: + crowdanki:deck_config_name: Ultimate Geography + crowdanki:deck_config_uuid: 43d37fac-9a65-11e8-9cc5-a0481cc15658 + crowdanki:uuid: 43c5ba66-9a65-11e8-90c9-a0481cc15658 +note_types: + note-type.ultimate-geography: + name: '${note-type.name}${variant.name-suffix}' + variables: + label.capital: Capital + label.capital-hint.answer: 'Hint: {{Capital hint}}' + label.capital-hint.question: 'Hint: {{Capital hint}}' + label.flag: Flag + label.location: Location + note-type.name: Ultimate Geography + sentence.flag-similar: 'Flag similar to {{Flag similarity}}.' + variant.name-suffix: '' + field_order: + - field.country + - field.country-info + - field.capital + - field.capital-info + - field.capital-hint + - field.flag + - field.flag-similarity + - field.map + fields: + field.capital: + name: Capital + field.capital-hint: + name: Capital hint + field.capital-info: + name: Capital info + field.country: + name: Country + field.country-info: + name: Country info + field.flag: + name: Flag + field.flag-similarity: + name: Flag similarity + field.map: + name: Map + card_template_order: + - template.country-capital + - template.capital-country + - template.flag-country + - template.map-country + card_templates: + template.capital-country: + name: Capital - Country + question_format: |- + {{#Capital}} +
?
+ +
+ +
${label.capital}
+
{{Capital}}
+ {{#Capital hint}}
${label.capital-hint.question}
{{/Capital hint}} + {{/Capital}} + answer_format: | +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.capital}
+
{{Capital}}
+ {{#Capital hint}}
${label.capital-hint.answer}
{{/Capital hint}} + {{#Capital info}}
{{Capital info}}
{{/Capital info}} + adapter_ids: {} + template.country-capital: + name: Country - Capital + question_format: |- + {{#Capital}} +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.capital}
+
?
+ {{/Capital}} + answer_format: | +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.capital}
+
{{Capital}}
+ {{#Capital info}}
{{Capital info}}
{{/Capital info}} + adapter_ids: {} + template.flag-country: + name: Flag - Country + question_format: |- + {{#Flag}} +
?
+ +
+ +
${label.flag}
+
{{Flag}}
+ {{/Flag}} + answer_format: | +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.flag}
+
{{Flag}}
+ {{#Flag similarity}}
${sentence.flag-similar}
{{/Flag similarity}} + adapter_ids: {} + template.map-country: + name: Map - Country + question_format: |- + {{#Map}} +
?
+ +
+ +
${label.location}
+
{{Map}}
+ {{/Map}} + answer_format: | +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.location}
+
{{Map}}
+ adapter_ids: {} + styling: | + .card { + padding: 1em 0; + background-color: white; + color: black; + font-family: Verdana; + font-size: 16px; + text-align: center; + } + + .type { + margin-bottom: 0.25em; + color: #333; + font-size: 70%; + font-weight: bold; + text-transform: uppercase; + } + + .info { + max-width: 30em; + margin: 0.75em auto; + color: #333; + font-size: 90%; + font-style: italic; + } + + .value { + font-size: 150%; + } + + .value--top { + margin-top: 1em; + } + + .value--image { + margin-top: 0.75em; + } + + .value--map { + width: 90%; + height: 65vh; + margin: 2em auto 0 auto; + } + + .value > img, + .value > .placeholder { + max-width: 100%; + height: auto; + } + + /** + * Apply shadow to images, notably to bring out white areas on flags. + * Ignore images with non-rectangular outlines (e.g. flag of Nepal). + */ + .value > img:not([src*="-nobox"]) { + box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.2); + } + + /** + * Some flags (e.g. Guam's) contain identifying words that can give away the answer. + * If a blurred version is available, show it on the front but not on the back. + */ + .value--front > img[src*="-blur"] + img { + display: none; + } + + .value--back > img[src*="-blur"] { + display: none; + } + + /** + * Placeholder SVG to hint at the type of answer that is expected. + * Used on "Country - Flag" and "Country - Map" templates. + */ + .placeholder { + color: #333; + } + + .placeholder > path { + fill: none; + stroke: currentColor; + stroke-width: 1; + } + + .night_mode .info, + .night_mode .type, + .night_mode .placeholder, + .nightMode .info, + .nightMode .type, + .nightMode .placeholder { + color: #ccc; + } + + /** + * Apply shadow to images, to bring out black areas on flags, in night + mode. + */ + .nightMode .value > img:not([src*="-nobox"]), + .night_mode .value > img:not([src*="-nobox"]) { + box-shadow: 0 0 4px 1px rgba(54, 54, 54, 0.9); + } + + hr { + margin: 1.5em 0; + } + adapter_ids: + crowdanki:uuid: 43e2586a-9a65-11e8-a777-a0481cc15658 +notes: + note.abkhazia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Sukhumi + field.capital-hint: '' + field.capital-info: '' + field.country: Abkhazia + field.country-info: Independent state claimed by Georgia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'sRw-ik:I$v' + note.adriatic-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Adriatic Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'yX:tmrd5;]' + note.aegean-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Aegean Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'iB-8Br~onq' + note.afghanistan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kabul + field.capital-hint: '' + field.capital-info: '' + field.country: Afghanistan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'm5j,b/y' + tags: + - 'UG::Continents' + adapter_ids: + crowdanki:guid: 'pwwZu{7f~(' + note.akrotiri-and-dhekelia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Akrotiri and Dhekelia + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Mediterranean' + - 'UG::Middle_East' + adapter_ids: + crowdanki:guid: 'wa>{k!2cXc' + note.alaska: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Alaska + field.country-info: State of the United States. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'ssbyA9}]QP' + note.albania: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tirana + field.capital-hint: '' + field.capital-info: '' + field.country: Albania + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'gz9${p}er*' + note.algeria: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Algiers + field.capital-hint: '' + field.capital-info: '' + field.country: Algeria + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'bd~Ght5tLR' + note.american-samoa: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: American Samoa + field.country-info: Unincorporated territory of the United States. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'niJUFZF$Y:' + note.andorra: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Andorra la Vella + field.capital-hint: '' + field.capital-info: '' + field.country: Andorra + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Moldova (wider, coat of arms with eagle)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'dY96`C-f(_' + note.angola: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Luanda + field.capital-hint: '' + field.capital-info: '' + field.country: Angola + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'onMKID2d;D' + note.anguilla: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Anguilla + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'e' + tags: + - 'UG::Continents' + adapter_ids: + crowdanki:guid: 'Cf2@;p' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'm2F|cUkKhv' + note.arabian-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Arabian Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'r(h9&#wc~B' + note.aral-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Aral Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'qiv/4tOlf=' + note.arctic-ocean: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Arctic Ocean + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'gG6SWfM>H0' + note.argentina: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Buenos Aires + field.capital-hint: '' + field.capital-info: '' + field.country: Argentina + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'n&>e`2FO/S' + note.armenia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Yerevan + field.capital-hint: '' + field.capital-info: '' + field.country: Armenia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'c(,2{yXI#E' + note.aruba: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Oranjestad + field.capital-hint: '' + field.capital-info: '' + field.country: Aruba + field.country-info: Constituent country of the Kingdom of the Netherlands. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'uv1fGXHN@)' + note.asia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Asia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Continents' + adapter_ids: + crowdanki:guid: 'Qlib4)q2Hk' + note.atlantic-ocean: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Atlantic Ocean + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'nuU8A_$i2M' + note.australia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Canberra + field.capital-hint: '' + field.capital-info: '' + field.country: Australia + field.country-info: '' + field.flag: '' + field.flag-similarity: 'New Zealand (red stars, two fewer stars)' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'k0{O[6l]bH' + note.austria: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Vienna + field.capital-hint: '' + field.capital-info: '' + field.country: Austria + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Latvia (darker red, narrower white band)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'r>aI-$$|wT' + note.azerbaijan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Baku + field.capital-hint: '' + field.capital-info: '' + field.country: Azerbaijan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'k9E.p^' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'j=L,YBvsBk' + note.bahrain: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Manama + field.capital-hint: '' + field.capital-info: '' + field.country: Bahrain + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Qatar (wider, more serrated edges, maroon)' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'stRa/S(0$F' + note.bali: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Bali + field.country-info: Island of Indonesia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + adapter_ids: + crowdanki:guid: 'O~pXkAbd3' + note.balkan-peninsula: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Balkan Peninsula + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'zSc]-^U=0=' + note.baltic-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Baltic Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'epvt3>HH?U' + note.banda-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Banda Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'kopwzzB``A' + note.bangladesh: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Dhaka + field.capital-hint: '' + field.capital-info: '' + field.country: Bangladesh + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'l+>Ki!j#v+' + note.barbados: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bridgetown + field.capital-hint: '' + field.capital-info: '' + field.country: Barbados + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'e{e8vi@^PY' + note.barents-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Barents Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'h9Jr(H[=1~' + note.bay-of-bengal: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Bay of Bengal + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'z:*d{z(~V2' + note.bay-of-biscay: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Bay of Biscay + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'H*c[ML+`s=' + note.belarus: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Minsk + field.capital-hint: '' + field.capital-info: '' + field.country: Belarus + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'l$zM~ihPFE' + note.belgium: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Brussels + field.capital-hint: '' + field.capital-info: '' + field.country: Belgium + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'jTNoKo}Bu+' + note.belize: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Belmopan + field.capital-hint: '' + field.capital-info: '' + field.country: Belize + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'tcneuK7v%`' + note.benin: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Porto-Novo + field.capital-hint: '' + field.capital-info: While Porto-Novo is the official capital, Cotonou is the de facto seat of government. + field.country: Benin + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'q8fF$4,c6_' + note.bering-strait: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Bering Strait + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'J%,fuysl*&' + note.bermuda: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Bermuda + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'gBCR?!O*;E' + note.bhutan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Thimphu + field.capital-hint: '' + field.capital-info: '' + field.country: Bhutan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'kQ:NIhJy~E' + note.black-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Black Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'e_;dzXj8de' + note.bolivia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Sucre + field.capital-hint: '' + field.capital-info: While Sucre is the constitutional capital, La Paz is the seat of government. + field.country: Bolivia + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Ghana (star instead of coat of arms)' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'khB~(m^@Z5' + note.bosnia-and-herzegovina: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Sarajevo + field.capital-hint: '' + field.capital-info: '' + field.country: Bosnia and Herzegovina + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'mz[>>AxIr4' + note.botswana: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Gaborone + field.capital-hint: '' + field.capital-info: '' + field.country: Botswana + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: upiX0g1quR + note.bougainville: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Bougainville + field.country-info: Autonomous region of Papua New Guinea. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'vKx<0Qa!aP' + note.brazil: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Brasília' + field.capital-hint: '' + field.capital-info: '' + field.country: Brazil + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: spKoFRM4if + note.british-virgin-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: British Virgin Islands + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'q[}7hk(+=e' + note.brunei: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bandar Seri Begawan + field.capital-hint: '' + field.capital-info: '' + field.country: Brunei + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'sY>@9w~i^p' + note.bulgaria: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Sofia + field.capital-hint: '' + field.capital-info: '' + field.country: Bulgaria + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'u4^lH.xem5' + note.burkina-faso: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Ouagadougou + field.capital-hint: '' + field.capital-info: '' + field.country: Burkina Faso + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'n:7&>YT5A=' + note.burundi: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Gitega + field.capital-hint: '' + field.capital-info: Official capital was moved from Bujumbura to Gitega in 2019. + field.country: Burundi + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'EWXNc' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'bW)ch:vs1P' + note.cameroon: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Yaoundé' + field.capital-hint: '' + field.capital-info: '' + field.country: Cameroon + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Senegal (green/yellow/red, green star)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'j2IQ=f=w3@' + note.canada: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Ottawa + field.capital-hint: '' + field.capital-info: '' + field.country: Canada + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'e?h=Xhs+K&' + note.canary-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Canary Islands + field.country-info: Autonomous community of Spain. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + adapter_ids: + crowdanki:guid: 'e?Xq9gy=@[' + note.cape-verde: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Praia + field.capital-hint: '' + field.capital-info: '' + field.country: Cape Verde + field.country-info: Also known as Cabo Verde. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'gFOojQO(MH' + note.caribbean-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Caribbean Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'jYz[ibrr9m' + note.caspian-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Caspian Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: x2d4n4ADcP + note.cayman-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Cayman Islands + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'vwc3}.=Z#&' + note.celebes-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Celebes Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'e!KSebP}pt' + note.celtic-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Celtic Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'E3u*2.<9#`' + note.central-african-republic: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bangui + field.capital-hint: '' + field.capital-info: '' + field.country: Central African Republic + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'vNiD6A.v#s' + note.chad: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'N''Djamena' + field.capital-hint: '' + field.capital-info: '' + field.country: Chad + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Romania (slightly lighter blue)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: '?n!)c/h=Q' + note.chile: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Santiago + field.capital-hint: '' + field.capital-info: '' + field.country: Chile + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'q8][)Ri}=q' + note.china: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Beijing + field.capital-hint: '' + field.capital-info: '' + field.country: China + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: mJH4kwPF.- + note.colombia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Bogotá' + field.capital-hint: '' + field.capital-info: '' + field.country: Colombia + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Ecuador (with coat of arms)' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: hoHandbYAy + note.comoros: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Moroni + field.capital-hint: '' + field.capital-info: '' + field.country: Comoros + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 't`cagaWApo' + note.cook-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Avarua + field.capital-hint: '' + field.capital-info: '' + field.country: Cook Islands + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 's4E?ZF[E<>' + note.coral-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Coral Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 's|K&2S,UP|' + note.corsica: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Corsica + field.country-info: Region of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'xb&P*pH(Q' + note.costa-rica: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'San José' + field.capital-hint: '' + field.capital-info: '' + field.country: Costa Rica + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'uH*Q(mcRt5' + note.croatia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Zagreb + field.capital-hint: '' + field.capital-info: '' + field.country: Croatia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'bk2ByZIeU+' + note.cuba: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Havana + field.capital-hint: '' + field.capital-info: '' + field.country: Cuba + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Puerto Rico (blue triangle, red stripes)' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'rPekx@n9:L' + note.cura-ao: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Willemstad + field.capital-hint: '' + field.capital-info: '' + field.country: 'Curaçao' + field.country-info: Constituent country of the Kingdom of the Netherlands. + field.flag: '' + field.flag-similarity: 'Nauru (single star below yellow band)' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'oa87&PB&Bm' + note.cyprus: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Nicosia + field.capital-hint: '' + field.capital-info: '' + field.country: Cyprus + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::European_Union' + - 'UG::Mediterranean' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'kkbYQXw5b;' + note.czech-republic: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Prague + field.capital-hint: '' + field.capital-info: '' + field.country: Czech Republic + field.country-info: Also known as Czechia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'twrhzJ[k:Z' + note.dead-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Dead Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'B:x' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'o3VS!KWb/@' + note.denmark: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Copenhagen + field.capital-hint: '' + field.capital-info: '' + field.country: Denmark + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'kS)hDm%w}R' + note.denmark-strait: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Denmark Strait + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'eN#^kPGv*L' + note.djibouti: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Djibouti + field.capital-hint: '' + field.capital-info: '' + field.country: Djibouti + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'iR;U' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'k}4/oj#$~s' + note.dominican-republic: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Santo Domingo + field.capital-hint: '' + field.capital-info: '' + field.country: Dominican Republic + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'lAxH.CfJB@' + note.east-china-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: East China Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'pYRw[=6;JI' + note.east-siberian-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: East Siberian Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'gRVZ]qJ#>O' + note.ecuador: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Quito + field.capital-hint: '' + field.capital-info: '' + field.country: Ecuador + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Colombia (no coat of arms)' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'lmRujD!(A7' + note.egypt: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Cairo + field.capital-hint: '' + field.capital-info: '' + field.country: Egypt + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Iraq (text instead of emblem), Yemen (no emblem)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Mediterranean' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'j%*8gN)q>d' + note.el-salvador: + note_type_id: note-type.ultimate-geography + fields: + field.capital: San Salvador + field.capital-hint: '' + field.capital-info: '' + field.country: El Salvador + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Nicaragua (different coat of arms, slightly lighter blue)' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'k$1_yaF?9#' + note.england: + note_type_id: note-type.ultimate-geography + fields: + field.capital: London + field.capital-hint: Not a sovereign country + field.capital-info: '' + field.country: England + field.country-info: Constituent country of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'e+/O]%*qfk' + note.english-channel: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: English Channel + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'q1Q3Do8S>g' + note.equatorial-guinea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Ciudad de la Paz + field.capital-hint: '' + field.capital-info: Official capital was moved from Malabo to Ciudad de la Paz in 2026. + field.country: Equatorial Guinea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'gYIoR`|AxW' + note.eritrea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Asmara + field.capital-hint: '' + field.capital-info: '' + field.country: Eritrea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'pS>DrFhd%u' + note.estonia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tallinn + field.capital-hint: '' + field.capital-info: '' + field.country: Estonia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'AES],s:8=' + note.eswatini: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Mbabane + field.capital-hint: '' + field.capital-info: While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital. + field.country: Eswatini + field.country-info: Known as Swaziland until 2018. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'e5P*D#I{m{' + note.ethiopia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Addis Ababa + field.capital-hint: '' + field.capital-info: '' + field.country: Ethiopia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 't|h3|;E4g{' + note.europe: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Europe + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Continents' + adapter_ids: + crowdanki:guid: oV/JG7fwZq + note.european-union: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: European Union + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: Bszsaz0Iuo + note.falkland-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Falkland Islands + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + adapter_ids: + crowdanki:guid: 'p#R$-mG/L' + note.faroe-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Tórshavn' + field.capital-hint: '' + field.capital-info: '' + field.country: Faroe Islands + field.country-info: Constituent country in the Kingdom of Denmark. + field.flag: '' + field.flag-similarity: 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'k[)#L4.[v3' + note.federated-states-of-micronesia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Palikir + field.capital-hint: '' + field.capital-info: '' + field.country: Federated States of Micronesia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'k1]FH+s8j@' + note.fiji: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Suva + field.capital-hint: '' + field.capital-info: '' + field.country: Fiji + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'gB|K:+r?1V' + note.finland: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Helsinki + field.capital-hint: '' + field.capital-info: '' + field.country: Finland + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'k}A9]O:0xw' + note.france: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Paris + field.capital-hint: '' + field.capital-info: '' + field.country: France + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'm*#%sE.`.;' + note.french-guiana: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: French Guiana + field.country-info: Overseas department of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + adapter_ids: + crowdanki:guid: 'b^Try&me1A' + note.french-polynesia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Papeete + field.capital-hint: '' + field.capital-info: '' + field.country: French Polynesia + field.country-info: Overseas territory of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'lH^2D]w;[H' + note.gabon: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Libreville + field.capital-hint: '' + field.capital-info: '' + field.country: Gabon + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'p.yv>BQ470' + note.georgia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tbilisi + field.capital-hint: '' + field.capital-info: '' + field.country: Georgia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'jN|NAUP*h}' + note.germany: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Berlin + field.capital-hint: '' + field.capital-info: '' + field.country: Germany + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'j|z@PMgdx,' + note.ghana: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Accra + field.capital-hint: '' + field.capital-info: '' + field.country: Ghana + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Bolivia (coat of arms instead of star)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'gx:;za!?C9' + note.gibraltar: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Gibraltar + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Mediterranean' + adapter_ids: + crowdanki:guid: 'o``:?0>/yz' + note.greece: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Athens + field.capital-hint: '' + field.capital-info: '' + field.country: Greece + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'n+t>2`:;P/' + note.greenland: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Nuuk + field.capital-hint: '' + field.capital-info: '' + field.country: Greenland + field.country-info: Constituent country in the Kingdom of Denmark. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'ilGtw#=asM' + note.grenada: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'St. George''s' + field.capital-hint: '' + field.capital-info: '' + field.country: Grenada + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'f*LO~>!0PN' + note.guadeloupe: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Guadeloupe + field.country-info: Overseas department of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'tTnCoO/*tk' + note.guam: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Hagåtña' + field.capital-hint: '' + field.capital-info: '' + field.country: Guam + field.country-info: Unincorporated territory of the United States. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'dWA49p}F{k' + note.guatemala: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Guatemala City + field.capital-hint: '' + field.capital-info: '' + field.country: Guatemala + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'dUZ^=SE5BG' + note.guernsey: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Guernsey + field.country-info: Crown dependency of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'pz(G*' + field.flag-similarity: 'Mali (red and green flipped, slightly brighter green)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'r]*Ioa%$bK' + note.guinea-bissau: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bissau + field.capital-hint: '' + field.capital-info: '' + field.country: Guinea-Bissau + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'i^J8&$(1]|' + note.gulf-of-alaska: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Gulf of Alaska + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'igiK,ff[eO' + note.gulf-of-california: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Gulf of California + field.country-info: Also known as the Sea of Cortez. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'SlOEXfq#|' + note.gulf-of-carpentaria: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Gulf of Carpentaria + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'pQpX%(EVOH' + note.gulf-of-guinea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Gulf of Guinea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'BJ,,^wMC%N' + note.gulf-of-mexico: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Gulf of Mexico + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'JOT:U4ayh@' + note.gulf-of-thailand: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Gulf of Thailand + field.country-info: Also known as the Gulf of Siam. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'rSneU!3}bn' + note.guyana: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Georgetown + field.capital-hint: '' + field.capital-info: '' + field.country: Guyana + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'q:XyGf#>Wg' + note.haiti: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Port-au-Prince + field.capital-hint: '' + field.capital-info: '' + field.country: Haiti + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'hZ/V/D&rO`' + note.hawaii: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Hawaii + field.country-info: State of the United States. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 's>f40rn,O]' + note.honduras: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tegucigalpa + field.capital-hint: '' + field.capital-info: '' + field.country: Honduras + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 't_h*fbN#:(' + note.hong-kong: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Hong Kong + field.country-info: Special Administrative Region of China. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + adapter_ids: + crowdanki:guid: 'lpbD0St&OU' + note.hudson-bay: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Hudson Bay + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: sM0GPt1HS1 + note.hungary: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Budapest + field.capital-hint: '' + field.capital-info: '' + field.country: Hungary + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'mcc&0Kq^xE' + note.iceland: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Reykjavík' + field.capital-hint: '' + field.capital-info: '' + field.country: Iceland + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: j7eEZkzsCZ + note.india: + note_type_id: note-type.ultimate-geography + fields: + field.capital: New Delhi + field.capital-hint: '' + field.capital-info: '' + field.country: India + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'q2#Yv[O0:|' + note.indian-ocean: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Indian Ocean + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'fT!w8R#>dg' + note.indonesia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Jakarta + field.capital-hint: '' + field.capital-info: '' + field.country: Indonesia + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'sx|l+io`B@' + note.iran: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tehran + field.capital-hint: '' + field.capital-info: '' + field.country: Iran + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'mbz=7n[0v9' + note.iraq: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Baghdad + field.capital-hint: '' + field.capital-info: '' + field.country: Iraq + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Egypt (emblem instead of text), Yemen (no text)' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: jI9P-f6r3M + note.ireland: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Dublin + field.capital-hint: '' + field.capital-info: '' + field.country: Ireland + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Ivory Coast (green and orange flipped, narrower)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 's<_WLSB3I/' + note.isle-of-man: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Isle of Man + field.country-info: Crown dependency of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'cV4idO^r8W' + note.israel: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Jerusalem + field.capital-hint: Claimed and controlled + field.capital-info: 'Disputed; claimed by Palestine.' + field.country: Israel + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Mediterranean' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'ijP>aJBT!9' + note.italy: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Rome + field.capital-hint: '' + field.capital-info: '' + field.country: Italy + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'cYKvF#ptF7' + note.ivory-coast: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Yamoussoukro + field.capital-hint: '' + field.capital-info: While Yamoussoukro is the official capital, Abidjan is the de facto seat of government. + field.country: Ivory Coast + field.country-info: 'Officially Côte d''Ivoire.' + field.flag: '' + field.flag-similarity: 'Ireland (orange and green flipped, wider)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'jN^Vc%9OQ5' + note.jamaica: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kingston + field.capital-hint: '' + field.capital-info: '' + field.country: Jamaica + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'h&`>n98~V1' + note.japan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tokyo + field.capital-hint: '' + field.capital-info: '' + field.country: Japan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'l}[BG9.nQS' + note.java: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Java + field.country-info: Island of Indonesia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + adapter_ids: + crowdanki:guid: 'gDsLPj(9S#' + note.jeju: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Jeju + field.country-info: Autonomous province of South Korea. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + adapter_ids: + crowdanki:guid: fkq7lhLkdH + note.jersey: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Jersey + field.country-info: Crown dependency of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'm*PYE=#wr^' + note.jordan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Amman + field.capital-hint: '' + field.capital-info: '' + field.country: Jordan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'i)]+09JUR&' + note.kaliningrad-oblast: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Kaliningrad Oblast + field.country-info: 'Oblast (administrative region) of the Russian Federation.' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'dC:e5G4J/p' + note.kazakhstan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Astana + field.capital-hint: '' + field.capital-info: Known as Nur-Sultan between 2019 and 2022 + field.country: Kazakhstan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'ia8as9sCY|' + note.kenya: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Nairobi + field.capital-hint: '' + field.capital-info: '' + field.country: Kenya + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'j' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: sXzvT.g31p + note.kosovo: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Pristina + field.capital-hint: '' + field.capital-info: '' + field.country: Kosovo + field.country-info: Partially recognised state claimed by Serbia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: libliptkB4 + note.kuwait: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kuwait City + field.capital-hint: '' + field.capital-info: '' + field.country: Kuwait + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'euW0jm=wfR' + note.kyrgyzstan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bishkek + field.capital-hint: '' + field.capital-info: '' + field.country: Kyrgyzstan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: pkj3RPBUrF + note.labrador-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Labrador Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'dlU7VG|3Gd' + note.land-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Mariehamn + field.capital-hint: '' + field.capital-info: '' + field.country: 'Åland Islands' + field.country-info: Autonomous region of Finland. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'd``B*6:eCx' + note.laos: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Vientiane + field.capital-hint: '' + field.capital-info: '' + field.country: Laos + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'iW>m(L/~c=' + note.latvia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Riga + field.capital-hint: '' + field.capital-info: '' + field.country: Latvia + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Austria (brighter red, wider white band)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'm)WUtJS`H&' + note.lebanon: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Beirut + field.capital-hint: '' + field.capital-info: '' + field.country: Lebanon + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Mediterranean' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'mtd;>7T|g>' + note.lesotho: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Maseru + field.capital-hint: '' + field.capital-info: '' + field.country: Lesotho + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'oNpApS' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'nsS59c`VL,' + note.libya: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tripoli + field.capital-hint: '' + field.capital-info: '' + field.country: Libya + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'jrM28*HbyG' + note.liechtenstein: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Vaduz + field.capital-hint: '' + field.capital-info: '' + field.country: Liechtenstein + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'l$2O|9w`F~' + note.lithuania: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Vilnius + field.capital-hint: '' + field.capital-info: '' + field.country: Lithuania + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'kZ52zV#[Iv' + note.luxembourg: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Luxembourg City + field.capital-hint: '' + field.capital-info: Officially Luxembourg. + field.country: Luxembourg + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Netherlands (darker blue)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'i2]v|D3@:j' + note.macau: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Macau + field.country-info: Special Administrative Region of China. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + adapter_ids: + crowdanki:guid: 'mq)8`hi{hb' + note.madagascar: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Antananarivo + field.capital-hint: '' + field.capital-info: '' + field.country: Madagascar + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'm{noqE29%I' + note.madeira: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Madeira + field.country-info: Autonomous region of Portugal. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'h}lnSN_f$_' + note.malawi: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Lilongwe + field.capital-hint: '' + field.capital-info: '' + field.country: Malawi + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'sSW/?WMy17' + note.malaysia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kuala Lumpur + field.capital-hint: '' + field.capital-info: '' + field.country: Malaysia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'mk20`,N=`=' + note.maldives: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Malé' + field.capital-hint: '' + field.capital-info: '' + field.country: Maldives + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'hXNTVF<:SH' + note.mali: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bamako + field.capital-hint: '' + field.capital-info: '' + field.country: Mali + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Guinea (green and red flipped, slightly darker green)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'd-q4J~CNW_' + note.malta: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Valletta + field.capital-hint: '' + field.capital-info: '' + field.country: Malta + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'nIfRn' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'eT' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'u;|vR#mVMW' + note.mauritania: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Nouakchott + field.capital-hint: '' + field.capital-info: '' + field.country: Mauritania + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'jKO,9y0M;#' + note.mauritius: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Port Louis + field.capital-hint: '' + field.capital-info: '' + field.country: Mauritius + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'skOM(?n$GG' + note.mayotte: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Mayotte + field.country-info: Overseas department of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + adapter_ids: + crowdanki:guid: 'xqrJ^d[.(O' + note.mediterranean-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Mediterranean Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'Gn;q2Y)Xe4' + note.melanesia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Melanesia + field.country-info: Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'o9{x' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'sxOMs+bE8@' + note.micronesia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Micronesia + field.country-info: Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'cv+Nq9:a' + field.flag-similarity: 'Andorra (narrower, coat of arms with motto)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: '5S(f7%O-$' + note.monaco: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Monaco + field.capital-hint: '' + field.capital-info: '' + field.country: Monaco + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'm?}%V6s8]*' + note.mongolia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Ulaanbaatar + field.capital-hint: '' + field.capital-info: '' + field.country: Mongolia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'd3c`&/1Y^D' + note.montenegro: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Podgorica + field.capital-hint: '' + field.capital-info: Cetinje is an honorary capital. + field.country: Montenegro + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'uRo^w#p5Be' + note.morocco: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Rabat + field.capital-hint: '' + field.capital-info: '' + field.country: Morocco + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'mi+kf0LTW.' + note.mozambique: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Maputo + field.capital-hint: '' + field.capital-info: '' + field.country: Mozambique + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'vgdj#X?8dB' + note.myanmar: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Naypyidaw + field.capital-hint: '' + field.capital-info: '' + field.country: Myanmar + field.country-info: Also known as Burma. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'l*j*EE:!Y?' + note.namibia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Windhoek + field.capital-hint: '' + field.capital-info: '' + field.country: Namibia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'mkgcU7[bA%' + note.nauru: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Yaren + field.capital-hint: '' + field.capital-info: 'Nauru has no official capital; the Yaren District is the de facto capital.' + field.country: Nauru + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Curaçao (two stars in top-left corner)' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'vE[T$`n9T)' + note.nepal: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kathmandu + field.capital-hint: '' + field.capital-info: '' + field.country: Nepal + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'l3Ly8PBxt(' + note.netherlands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Amsterdam + field.capital-hint: '' + field.capital-info: While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches. + field.country: Netherlands + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Luxembourg (lighter blue)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'o=,%hU&yX%' + note.new-caledonia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Nouméa' + field.capital-hint: '' + field.capital-info: '' + field.country: New Caledonia + field.country-info: Overseas territory of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'tj&bK}%P!F' + note.new-zealand: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Wellington + field.capital-hint: '' + field.capital-info: '' + field.country: New Zealand + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Australia (white stars, two more stars)' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: ')Y}Qy.3GK' + note.nicaragua: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Managua + field.capital-hint: '' + field.capital-info: '' + field.country: Nicaragua + field.country-info: '' + field.flag: '' + field.flag-similarity: 'El Salvador (different coat of arms, slightly darker blue)' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'q=w[' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'jcu!gLw/&r' + note.nigeria: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Abuja + field.capital-hint: '' + field.capital-info: '' + field.country: Nigeria + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'sB7rZQsR' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'kX]hN=_.c-' + note.north-america: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: North America + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Continents' + adapter_ids: + crowdanki:guid: 'bD?R}i8_[c' + note.north-korea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Pyongyang + field.capital-hint: '' + field.capital-info: '' + field.country: North Korea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'r6BV%|..*5' + note.north-macedonia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Skopje + field.capital-hint: '' + field.capital-info: '' + field.country: North Macedonia + field.country-info: Formerly known as Macedonia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'r5C646H*+*' + note.north-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: North Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'b`5Q_Z=HQT' + note.northern-cyprus: + note_type_id: note-type.ultimate-geography + fields: + field.capital: North Nicosia + field.capital-hint: '' + field.capital-info: '' + field.country: Northern Cyprus + field.country-info: State recognised only by Turkey and claimed by Cyprus. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Mediterranean' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'r/B|ra%L,.' + note.northern-ireland: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Belfast + field.capital-hint: '' + field.capital-info: '' + field.country: Northern Ireland + field.country-info: Constituent country of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'cPO|Cru8n<' + note.northern-mariana-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Northern Mariana Islands + field.country-info: Unincorporated territory of the United States. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'p}Qa|!7PpR' + note.norway: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Oslo + field.capital-hint: '' + field.capital-info: '' + field.country: Norway + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'lq`T^ZBF2Y' + note.norwegian-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Norwegian Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'lp@e##m.!+' + note.oceania: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Oceania + field.country-info: World region covering the Australian continent and most of the islands in the Pacific Ocean. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Continents' + adapter_ids: + crowdanki:guid: 'qFuGdDx^Yv' + note.oman: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Muscat + field.capital-hint: '' + field.capital-info: '' + field.country: Oman + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'eALZe^HCHq' + note.pacific-ocean: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Pacific Ocean + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'f*2@qiHh_1' + note.pakistan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Islamabad + field.capital-hint: '' + field.capital-info: '' + field.country: Pakistan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'iIS#Nw]tr]' + note.palau: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Ngerulmud + field.capital-hint: '' + field.capital-info: '' + field.country: Palau + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'r?E$4f-d0/' + note.palestine: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Jerusalem + field.capital-hint: Claimed but not controlled + field.capital-info: 'Disputed; claimed by Israel; Ramallah is the administrative centre.' + field.country: Palestine + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Mediterranean' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'g<[f89~U)A' + note.panama: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Panama City + field.capital-hint: '' + field.capital-info: '' + field.country: Panama + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'vpOTK6tMr@' + note.papua-new-guinea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Port Moresby + field.capital-hint: '' + field.capital-info: '' + field.country: Papua New Guinea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'lhWI;$JC9(' + note.paraguay: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Asunción' + field.capital-hint: '' + field.capital-info: '' + field.country: Paraguay + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 's#:aIK$9^x' + note.persian-gulf: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Persian Gulf + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'M$bc0[jYf0' + note.peru: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Lima + field.capital-hint: '' + field.capital-info: '' + field.country: Peru + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'jBd@A?>g|}' + note.philippine-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Philippine Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'G/^_##fcPN' + note.philippines: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Manila + field.capital-hint: '' + field.capital-info: '' + field.country: Philippines + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'qx%>fB$d+$' + note.poland: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Warsaw + field.capital-hint: '' + field.capital-info: '' + field.country: Poland + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'f>c7x2E]Q6' + note.polynesia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Polynesia + field.country-info: Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'd5m8%D6,;u' + note.portugal: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Lisbon + field.capital-hint: '' + field.capital-info: '' + field.country: Portugal + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'q[S4`,F0D=' + note.puerto-rico: + note_type_id: note-type.ultimate-geography + fields: + field.capital: San Juan + field.capital-hint: '' + field.capital-info: '' + field.country: Puerto Rico + field.country-info: Unincorporated territory of the United States. + field.flag: '' + field.flag-similarity: 'Cuba (red triangle, blue stripes)' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: t3aZXtpNyi + note.qatar: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Doha + field.capital-hint: '' + field.capital-info: '' + field.country: Qatar + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Bahrain (narrower, fewer serrated edges, red)' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'm{oM,jr>C#' + note.r-union: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: 'Réunion' + field.country-info: Overseas department of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + adapter_ids: + crowdanki:guid: 'vrn%j%6{nu' + note.red-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Red Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: '_]DH+g9!$' + note.republic-of-the-congo: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Brazzaville + field.capital-hint: '' + field.capital-info: '' + field.country: Republic of the Congo + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'u|I}oElaH)' + note.romania: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bucharest + field.capital-hint: '' + field.capital-info: '' + field.country: Romania + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Chad (slightly darker blue)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'jCd]`-=k,:' + note.russia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Moscow + field.capital-hint: '' + field.capital-info: '' + field.country: Russia + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Slovakia (with coat of arms)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'vf)G?[_?Pf' + note.rwanda: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kigali + field.capital-hint: '' + field.capital-info: '' + field.country: Rwanda + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'j)i)pB*HJ,' + note.s-o-tom-and-pr-ncipe: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'São Tomé' + field.capital-hint: '' + field.capital-info: '' + field.country: 'São Tomé and Príncipe' + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'gePC:Xt_nW' + note.sahrawi-arab-democratic-republic: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Laayoune + field.capital-hint: '' + field.capital-info: 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.' + field.country: Sahrawi Arab Democratic Republic + field.country-info: Partially recognised state claimed by Morocco. Also known as Western Sahara. + field.flag: '' + field.flag-similarity: 'Palestine (no symbol)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'cAlyc~G.y%' + note.saint-kitts-and-nevis: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Basseterre + field.capital-hint: '' + field.capital-info: '' + field.country: Saint Kitts and Nevis + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'b[(2`4t&?J' + note.saint-lucia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Castries + field.capital-hint: '' + field.capital-info: '' + field.country: Saint Lucia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'leh^fmrt*#' + note.saint-martin: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Saint Martin + field.country-info: Overseas territory of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 't2&ri|[Un:' + note.saint-vincent-and-the-grenadines: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kingstown + field.capital-hint: '' + field.capital-info: '' + field.country: Saint Vincent and the Grenadines + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'va/ky=f5pK' + note.samoa: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Apia + field.capital-hint: '' + field.capital-info: '' + field.country: Samoa + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: qvITjNxkLN + note.san-marino: + note_type_id: note-type.ultimate-geography + fields: + field.capital: City of San Marino + field.capital-hint: '' + field.capital-info: '' + field.country: San Marino + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'j9:K,~DQ2v' + note.sardinia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Sardinia + field.country-info: Autonomous region of Italy. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'jKIDq|}J&c' + note.saudi-arabia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Riyadh + field.capital-hint: '' + field.capital-info: '' + field.country: Saudi Arabia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'ln^3p)j3b.' + note.scandinavia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Scandinavia + field.country-info: Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'He!e=h?_2V' + note.scotland: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Edinburgh + field.capital-hint: '' + field.capital-info: '' + field.country: Scotland + field.country-info: Constituent country of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'h~5xz+=ke~' + note.sea-of-galilee: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Sea of Galilee + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'QSq%~rFaP-' + note.sea-of-japan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Sea of Japan + field.country-info: 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'd}R:qHj1,7' + note.sea-of-okhotsk: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Sea of Okhotsk + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: tfCY4j6KEZ + note.senegal: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Dakar + field.capital-hint: '' + field.capital-info: '' + field.country: Senegal + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Cameroon (green/red/yellow, yellow star)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 't-0>F]bu>P' + note.serbia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Belgrade + field.capital-hint: '' + field.capital-info: '' + field.country: Serbia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'ui3>b-?a@m' + note.seychelles: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Victoria + field.capital-hint: '' + field.capital-info: '' + field.country: Seychelles + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'hrI_uiW>E]' + note.sicily: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Sicily + field.country-info: Autonomous region of Italy. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'n{{o2K1:dw' + note.sierra-leone: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Freetown + field.capital-hint: '' + field.capital-info: '' + field.country: Sierra Leone + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'uc61ToT%gt' + note.singapore: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Singapore + field.capital-hint: '' + field.capital-info: '' + field.country: Singapore + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'cL?8!xP#Wj' + note.sint-maarten: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Sint Maarten + field.country-info: Constituent country of the Kingdom of the Netherlands. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'bnEK#)[;qT' + note.slovakia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bratislava + field.capital-hint: '' + field.capital-info: '' + field.country: Slovakia + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'qVi;t%/`F|' + note.slovenia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Ljubljana + field.capital-hint: '' + field.capital-info: '' + field.country: Slovenia + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Slovakia (narrower, bigger coat of arms)' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'g-]bb&[.E!' + note.solomon-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Honiara + field.capital-hint: '' + field.capital-info: '' + field.country: Solomon Islands + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'r>g<|,FWi%' + note.somalia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Mogadishu + field.capital-hint: '' + field.capital-info: '' + field.country: Somalia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'c,5(mUGzYT' + note.somaliland: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Hargeisa + field.capital-hint: '' + field.capital-info: '' + field.country: Somaliland + field.country-info: Independent state claimed by Somalia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'jX=#G8wu#(' + note.south-africa: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Pretoria, Cape Town, Bloemfontein + field.capital-hint: '' + field.capital-info: 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).' + field.country: South Africa + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'e.t$Vi5,>f' + note.south-america: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: South America + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Continents' + adapter_ids: + crowdanki:guid: 'Lol+XIvY21' + note.south-china-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: South China Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 't$y1C2R-Av' + note.south-korea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Seoul + field.capital-hint: '' + field.capital-info: '' + field.country: South Korea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'h?a*UKfT_M' + note.south-ossetia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tskhinvali + field.capital-hint: '' + field.capital-info: '' + field.country: South Ossetia + field.country-info: Independent state claimed by Georgia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'p]t8:Tka7q' + note.south-sudan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Juba + field.capital-hint: '' + field.capital-info: '' + field.country: South Sudan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'lI)Ah/E' + note.southern-ocean: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Southern Ocean + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: LPF4AfaHKR + note.spain: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Madrid + field.capital-hint: '' + field.capital-info: '' + field.country: Spain + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'f21=-A~W;~' + note.sri-lanka: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Sri Jayawardenepura Kotte + field.capital-hint: '' + field.capital-info: Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital. + field.country: Sri Lanka + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'g9v~y/;=z(' + note.sudan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Khartoum + field.capital-hint: '' + field.capital-info: '' + field.country: Sudan + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Palestine (black/white/green, red arrow)' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'k/u1:B%DJH' + note.sumatra: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Sumatra + field.country-info: Island of Indonesia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + adapter_ids: + crowdanki:guid: 'ck!6;xK$/^' + note.suriname: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Paramaribo + field.capital-hint: '' + field.capital-info: '' + field.country: Suriname + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'koI(}OyJ:@' + note.svalbard: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Svalbard + field.country-info: Unincorporated internal area of Norway. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: '5JJ2]i)p4' + note.sweden: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Stockholm + field.capital-hint: '' + field.capital-info: '' + field.country: Sweden + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::European_Union' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'sT5=H|@KU|' + note.switzerland: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bern + field.capital-hint: '' + field.capital-info: 'Switzerland has no official capital; Bern is the de facto capital.' + field.country: Switzerland + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'lU@C3' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Mediterranean' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'e=.VHQ9E+M' + note.taiwan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Taipei + field.capital-hint: '' + field.capital-info: '' + field.country: Taiwan + field.country-info: Partially recognised state claimed by China. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'm[96fa+HDS' + note.tajikistan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Dushanbe + field.capital-hint: '' + field.capital-info: '' + field.country: Tajikistan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'g~%pF`(x{u' + note.tanzania: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Dodoma + field.capital-hint: '' + field.capital-info: While Dodoma is the official capital, Dar es Salaam is the de facto seat of government. + field.country: Tanzania + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'p:SPZ:e/*[' + note.tasman-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Tasman Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'eHJ?)TAdTo' + note.thailand: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Bangkok + field.capital-hint: '' + field.capital-info: '' + field.country: Thailand + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'hE5j*e,Q?[' + note.the-bahamas: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Nassau + field.capital-hint: '' + field.capital-info: '' + field.country: The Bahamas + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'W5?6yQZD2' + note.the-gambia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Banjul + field.capital-hint: '' + field.capital-info: '' + field.country: The Gambia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'f#4-L[mp5*' + note.timor-leste: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Dili + field.capital-hint: '' + field.capital-info: '' + field.country: Timor-Leste + field.country-info: Also known as East Timor. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'fRSM{K+;[w' + note.timor-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Timor Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'vppEA|$@~g' + note.togo: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Lomé' + field.capital-hint: '' + field.capital-info: '' + field.country: Togo + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Sovereign_State' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 't+v9b--,qs' + note.tonga: + note_type_id: note-type.ultimate-geography + fields: + field.capital: 'Nukuʻalofa' + field.capital-hint: '' + field.capital-info: '' + field.country: Tonga + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'c^,~EC6Pb2' + note.transnistria: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tiraspol + field.capital-hint: '' + field.capital-info: '' + field.country: Transnistria + field.country-info: Independent state claimed by Moldova. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'tMMtBUpb$&' + note.trinidad-and-tobago: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Port of Spain + field.capital-hint: '' + field.capital-info: '' + field.country: Trinidad and Tobago + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 't50y/[AEhF' + note.tunisia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tunis + field.capital-hint: '' + field.capital-info: '' + field.country: Tunisia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::Mediterranean' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'fV`&p81%d(' + note.turkey: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Ankara + field.capital-hint: '' + field.capital-info: '' + field.country: Turkey + field.country-info: 'Also known as Türkiye' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Mediterranean' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'jDA-V?/hVj' + note.turkmenistan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Ashgabat + field.capital-hint: '' + field.capital-info: '' + field.country: Turkmenistan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'u-`cspdp)D' + note.turks-and-caicos-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Turks and Caicos Islands + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'q7do-wLx%E' + note.tuvalu: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Funafuti + field.capital-hint: '' + field.capital-info: '' + field.country: Tuvalu + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'de=9c:,@j9' + note.uganda: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kampala + field.capital-hint: '' + field.capital-info: '' + field.country: Uganda + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'gsS{jR,]|Q' + note.ukraine: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kyiv + field.capital-hint: '' + field.capital-info: Also known as Kiev. + field.country: Ukraine + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'h_9=mkZXAH' + note.united-arab-emirates: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Abu Dhabi + field.capital-hint: '' + field.capital-info: '' + field.country: United Arab Emirates + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'o~}KVA:VbI' + note.united-kingdom: + note_type_id: note-type.ultimate-geography + fields: + field.capital: London + field.capital-hint: Sovereign country + field.capital-info: '' + field.country: United Kingdom + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 's9-GL*@AXD' + note.united-states-of-america: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Washington, D.C. + field.capital-hint: '' + field.capital-info: '' + field.country: United States of America + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::North_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'b@M[XTc?}5' + note.united-states-virgin-islands: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Charlotte Amalie + field.capital-hint: '' + field.capital-info: '' + field.country: United States Virgin Islands + field.country-info: Unincorporated territory of the United States. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'kE!iKHLN_g' + note.uruguay: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Montevideo + field.capital-hint: '' + field.capital-info: '' + field.country: Uruguay + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'e?gIaPc$Ox' + note.uzbekistan: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Tashkent + field.capital-hint: '' + field.capital-info: '' + field.country: Uzbekistan + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: mbGXc0y3Pe + note.vanuatu: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Port Vila + field.capital-hint: '' + field.capital-info: '' + field.country: Vanuatu + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'K*b}' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'mk1VltJl<]' + note.venezuela: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Caracas + field.capital-hint: '' + field.capital-info: '' + field.country: Venezuela + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::South_America' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'o#&RMA_REo' + note.vietnam: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Hanoi + field.capital-hint: '' + field.capital-info: '' + field.country: Vietnam + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'pD' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'hNE9}>;Q&-' + note.wallis-and-futuna: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Wallis and Futuna + field.country-info: Overseas territory of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'dbP/Cj}sB+' + note.white-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: White Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'cEO^y)Md[G' + note.yellow-sea: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Yellow Sea + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceans+Seas' + adapter_ids: + crowdanki:guid: 'L5M1*yG7eH' + note.yemen: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Sanaa + field.capital-hint: '' + field.capital-info: 'Also spelled as Sana''a.' + field.country: Yemen + field.country-info: '' + field.flag: '' + field.flag-similarity: 'Egypt (with emblem), Iraq (with text)' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Middle_East' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'e42BpV,P}C' + note.zambia: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Lusaka + field.capital-hint: '' + field.capital-info: '' + field.country: Zambia + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'fxAN),FAc$' + note.zanzibar: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Zanzibar + field.country-info: Semi-autonomous region of Tanzania. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + adapter_ids: + crowdanki:guid: 'gp[h@]y13l' + note.zimbabwe: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Harare + field.capital-hint: '' + field.capital-info: '' + field.country: Zimbabwe + field.country-info: '' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::East_Africa' + - 'UG::Sovereign_State' + adapter_ids: + crowdanki:guid: 'r' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: '~)7-62ytBX' + note.british-indian-ocean-territory: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Camp Justice + field.capital-hint: '' + field.capital-info: '' + field.country: British Indian Ocean Territory + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + adapter_ids: + crowdanki:guid: 'ArmciP%a;a' + note.british-virgin-islands: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.canary-islands: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.cayman-islands: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.ceuta: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Ceuta + field.country-info: Autonomous city of Spain. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'rHh8*,y.yc' + note.christmas-island: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Flying Fish Cove + field.capital-hint: '' + field.capital-info: '' + field.country: Christmas Island + field.country-info: External territory of Australia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + adapter_ids: + crowdanki:guid: 'k3]]2^DP1R' + note.cocos-keeling-islands: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: West Island + field.capital-hint: '' + field.capital-info: '' + field.country: 'Cocos (Keeling) Islands' + field.country-info: External territory of Australia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Asia' + - 'UG::Southeast_Asia' + adapter_ids: + crowdanki:guid: mvNnHkGnVl + note.corsica: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.easter-island: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Easter Island + field.country-info: Polynesian island and special territory of Chile. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'rNv|0qxR]K' + note.falkland-islands: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.french-guiana: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.gal-pagos-islands: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Puerto Baquerizo Moreno + field.capital-hint: '' + field.capital-info: '' + field.country: 'Galápagos Islands' + field.country-info: Province of Ecuador. + field.flag: '' + field.flag-similarity: 'Sierra Leone (slightly lighter blue)' + field.map: '' + tags: + - 'UG::South_America' + adapter_ids: + crowdanki:guid: 'j=,1:f39$]' + note.gibraltar: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.guadeloupe: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.guernsey: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.isle-of-man: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.jersey: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.kaliningrad-oblast: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.madeira: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.martinique: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.mayotte: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.melilla: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Melilla + field.country-info: Autonomous city of Spain + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'd~3m`ihA*b' + note.montserrat: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Brades + field.capital-hint: '' + field.capital-info: Eruptions in 1997 destroyed the capital city of Plymouth, making Brades the de facto capital. + field.country: Montserrat + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'iq7o#n0juD' + note.mount-athos: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Karyes + field.capital-hint: '' + field.capital-info: '' + field.country: Mount Athos + field.country-info: Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Europe' + adapter_ids: + crowdanki:guid: 'qAsU}ONgC#' + note.norfolk-island: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Kingston + field.capital-hint: Not a sovereign country + field.capital-info: '' + field.country: Norfolk Island + field.country-info: External territory of Australia. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'nipI.lOoC;' + note.northern-mariana-islands: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.pitcairn-islands: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Adamstown + field.capital-hint: '' + field.capital-info: '' + field.country: Pitcairn Islands + field.country-info: Overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'mCh>s18~IX' + note.r-union: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.saba: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: The Bottom + field.capital-hint: '' + field.capital-info: '' + field.country: Saba + field.country-info: Special municipality of the Netherlands. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'i;=jdbhl(w' + note.saint-barth-lemy: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Gustavia + field.capital-hint: '' + field.capital-info: '' + field.country: 'Saint Barthélemy' + field.country-info: Overseas territory of France. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'ga97y4Zy0{' + note.saint-helena: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Jamestown + field.capital-hint: '' + field.capital-info: '' + field.country: Saint Helena + field.country-info: Part of Saint Helena, Ascension and Tristan da Cunha, an overseas territory of the United Kingdom. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Africa' + - 'UG::West_Africa' + adapter_ids: + crowdanki:guid: 'khTp#J9C!!' + note.saint-martin: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.sardinia: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.sicily: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.sint-eustatius: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: Oranjestad + field.capital-hint: Special municipality + field.capital-info: '' + field.country: Sint Eustatius + field.country-info: Special municipality of the Netherlands. + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Caribbean' + - 'UG::North_America' + adapter_ids: + crowdanki:guid: 'r3?J{NB0J3' + note.sint-maarten: + intent: merge + tags: + 'UG::Overlapping': + intent: add + note.tokelau: + intent: add + note: + note_type_id: note-type.ultimate-geography + fields: + field.capital: '' + field.capital-hint: '' + field.capital-info: '' + field.country: Tokelau + field.country-info: 'Dependent territory of New Zealand. Tokelau has no official capital; the meeting place of the Tokelau Council rotates annually.' + field.flag: '' + field.flag-similarity: '' + field.map: '' + tags: + - 'UG::Oceania' + adapter_ids: + crowdanki:guid: 'nN' + note.anguilla: + field.capital: The Valley + field.flag: '' + note.azores: + field.capital: Ponta Delgada + field.flag: '' + note.bali: + field.capital: Denpasar + field.flag: '' + note.bermuda: + field.capital: Hamilton + field.flag: '' + note.british-virgin-islands: + field.capital: Road Town + field.flag: '' + note.canary-islands: + field.capital: Santa Cruz de Tenerife, Las Palmas + field.capital-info: The capital is shared between the two cities of Santa Cruz de Tenerife and Las Palmas. + field.flag: '' + note.cayman-islands: + field.capital: George Town + field.capital-hint: Not a sovereign country + field.flag: '' + note.corsica: + field.capital: Ajaccio + field.flag: '' + note.falkland-islands: + field.capital: Stanley + field.flag: '' + note.french-guiana: + field.capital: Cayenne + note.gibraltar: + field.capital: Gibraltar + field.flag: '' + note.guadeloupe: + field.capital: Basse-Terre + field.capital-hint: Not a sovereign country + note.guernsey: + field.capital: St. Peter Port + field.flag: '' + note.isle-of-man: + field.capital: Douglas + field.flag: '' + note.jersey: + field.capital: Saint Helier + field.flag: '' + note.kaliningrad-oblast: + field.capital: Kaliningrad + field.flag: '' + note.madeira: + field.capital: Funchal + field.flag: '' + note.martinique: + field.capital: Fort-de-France + note.mayotte: + field.capital: Mamoudzou + note.northern-mariana-islands: + field.capital: Saipan + field.flag: '' + note.r-union: + field.capital: Saint-Denis + note.saint-martin: + field.capital: Marigot + note.sardinia: + field.capital: Cagliari + field.flag: '' + note.sicily: + field.capital: Palermo + field.flag: '' + note.sint-maarten: + field.capital: Philipsburg + field.flag: '' + note.turks-and-caicos-islands: + field.capital: Cockburn Town + field.flag: '' + note.zanzibar: + field.capital: Zanzibar City + field.flag: '' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/de.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/de.yaml new file mode 100644 index 0000000..7779813 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/de.yaml @@ -0,0 +1,40 @@ +id: overlay.extension.hardcore.field-fills.de +kind: extension +notes: + note.canary-islands: + intent: merge + fields: + field.capital: + intent: override + value: Santa Cruz de Tenerife, Las Palmas de Gran Canaria + expected_base: + value: Santa Cruz de Tenerife, Las Palmas + field.capital-info: + intent: override + value: 'Die zwei Städte Santa Cruz de Tenerife und Las Palmas de Gran Canaria sind zusammen Hauptstädte der Autonomen Gemeinschaft der Kanaren.' + expected_base: + value: The capital is shared between the two cities of Santa Cruz de Tenerife and Las Palmas. + note.cayman-islands: + intent: merge + fields: + field.capital-hint: + intent: override + value: 'Kein souveräner Staat' + expected_base: + value: Not a sovereign country + note.guadeloupe: + intent: merge + fields: + field.capital-hint: + intent: override + value: 'Kein souveräner Staat' + expected_base: + value: Not a sovereign country + note.zanzibar: + intent: merge + fields: + field.capital: + intent: override + value: Sansibar-Stadt + expected_base: + value: Zanzibar City diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/es.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/es.yaml new file mode 100644 index 0000000..2e7ac59 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/es.yaml @@ -0,0 +1,88 @@ +id: overlay.extension.hardcore.field-fills.es +kind: extension +notes: + note.anguilla: + intent: merge + fields: + field.capital: + intent: override + value: El Valle + expected_base: + value: The Valley + note.canary-islands: + intent: merge + fields: + field.capital: + intent: override + value: Santa Cruz de Tenerife, Las Palmas de Gran Canaria + expected_base: + value: Santa Cruz de Tenerife, Las Palmas + field.capital-info: + intent: override + value: La capitalidad es compartida entre las dos ciudades de Santa Cruz de Tenerife y Las Palmas de Gran Canaria. + expected_base: + value: The capital is shared between the two cities of Santa Cruz de Tenerife and Las Palmas. + note.cayman-islands: + intent: merge + fields: + field.capital-hint: + intent: override + value: 'No es un país soberano' + expected_base: + value: Not a sovereign country + note.falkland-islands: + intent: merge + fields: + field.capital: + intent: override + value: Puerto Argentino/Stanley + expected_base: + value: Stanley + note.french-guiana: + intent: merge + fields: + field.capital: + intent: override + value: Cayena + expected_base: + value: Cayenne + note.guadeloupe: + intent: merge + fields: + field.capital-hint: + intent: override + value: 'No es un país soberano' + expected_base: + value: Not a sovereign country + note.guernsey: + intent: merge + fields: + field.capital: + intent: override + value: Saint Peter Port + expected_base: + value: St. Peter Port + note.kaliningrad-oblast: + intent: merge + fields: + field.capital: + intent: override + value: Kaliningrado + expected_base: + value: Kaliningrad + note.northern-mariana-islands: + intent: merge + fields: + field.capital: + intent: override + value: 'Saipán' + expected_base: + value: Saipan + note.zanzibar: + intent: merge + fields: + field.capital: + intent: override + value: 'Zanzíbar' + expected_base: + value: Zanzibar City diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/fr.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/fr.yaml new file mode 100644 index 0000000..088806d --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/fr.yaml @@ -0,0 +1,64 @@ +id: overlay.extension.hardcore.field-fills.fr +kind: extension +notes: + note.canary-islands: + intent: merge + fields: + field.capital: + intent: override + value: Santa Cruz de Tenerife, Las Palmas de Grande Canarie + expected_base: + value: Santa Cruz de Tenerife, Las Palmas + field.capital-info: + intent: override + value: 'Le statut de capitale est partagé entre les villes de Santa Cruz et Las Palmas.' + expected_base: + value: The capital is shared between the two cities of Santa Cruz de Tenerife and Las Palmas. + note.cayman-islands: + intent: merge + fields: + field.capital-hint: + intent: override + value: Pas une nation souveraine + expected_base: + value: Not a sovereign country + note.guadeloupe: + intent: merge + fields: + field.capital-hint: + intent: override + value: Pas une nation souveraine + expected_base: + value: Not a sovereign country + note.guernsey: + intent: merge + fields: + field.capital: + intent: override + value: Saint-Pierre-Port + expected_base: + value: St. Peter Port + note.jersey: + intent: merge + fields: + field.capital: + intent: override + value: 'Saint-Hélier' + expected_base: + value: Saint Helier + note.sicily: + intent: merge + fields: + field.capital: + intent: override + value: Palerme + expected_base: + value: Palermo + note.zanzibar: + intent: merge + fields: + field.capital: + intent: override + value: Zanzibar + expected_base: + value: Zanzibar City diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/nb.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/nb.yaml new file mode 100644 index 0000000..323b532 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/field-fills/nb.yaml @@ -0,0 +1,48 @@ +id: overlay.extension.hardcore.field-fills.nb +kind: extension +notes: + note.canary-islands: + intent: merge + fields: + field.capital: + intent: override + value: 'Santa Cruz på Tenerife, Las Palmas på Gran Canaria' + expected_base: + value: Santa Cruz de Tenerife, Las Palmas + field.capital-info: + intent: override + value: 'De to byene Santa Cruz på Tenerife og Las Palmas på Gran Canaria deler hovedstadsstatusen.' + expected_base: + value: The capital is shared between the two cities of Santa Cruz de Tenerife and Las Palmas. + note.cayman-islands: + intent: merge + fields: + field.capital-hint: + intent: override + value: Ikke selvstendig land + expected_base: + value: Not a sovereign country + note.corsica: + intent: merge + fields: + field.capital: + intent: override + value: Bastia i Haute-Corse og Ajaccio i Corse-du-Sud + expected_base: + value: Ajaccio + note.guadeloupe: + intent: merge + fields: + field.capital-hint: + intent: override + value: Ikke selvstendig land + expected_base: + value: Not a sovereign country + note.zanzibar: + intent: merge + fields: + field.capital: + intent: override + value: Zanzibar by + expected_base: + value: Zanzibar City diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/cs.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/cs.yaml new file mode 100644 index 0000000..5f75e15 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/cs.yaml @@ -0,0 +1,22 @@ +id: overlay.translation.hardcore.cs +kind: translation +translations: + adapter_ids: + crowdanki:guid: + 'ArmciP%a;a': 'Ju3]WYq65s' + 'd~3m`ihA*b': 'Ni[B!/Wc_H' + 'ga97y4Zy0{': 'BN*,Os+[o{' + 'i;=jdbhl(w': 'n*:!>x->:X' + 'iq7o#n0juD': 'M]Y[7L`(o2' + 'j=,1:f39$]': 'czIuoMXe-@' + 'k3]]2^DP1R': 'nJP%0B-.&0' + 'khTp#J9C!!': 'Lg*P{H@!&|' + 'mCh>s18~IX': 'HUgiJtE-@m' + mvNnHkGnVl: 'jg5b%hj_mx' + 'nN2' + 'rNv|0qxR]K': 'bRZEGpT*)f' + '~)7-62ytBX': 'O#]/)`FDUL' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml new file mode 100644 index 0000000..28e1ae7 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml @@ -0,0 +1,79 @@ +id: overlay.translation.hardcore.de +kind: translation +translations: + changes: + Autonomous city of Spain: + notes.note.melilla.fields.field.country-info: Autonome spanische Stadt. + Autonomous city of Spain.: + notes.note.ceuta.fields.field.country-info: Autonome spanische Stadt. + Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain.: + notes.note.mount-athos.fields.field.country-info: 'Autonomer orthodoxer Klosterstaat im Nordosten Griechenlands, auch bekannt als „Heilige Berg“.' + British Indian Ocean Territory: + notes.note.british-indian-ocean-territory.fields.field.country: Britisches Territorium im Indischen Ozean + Christmas Island: + notes.note.christmas-island.fields.field.country: Weihnachtsinsel + 'Cocos (Keeling) Islands': + notes.note.cocos-keeling-islands.fields.field.country: Kokosinseln + 'Dependent territory of New Zealand. Tokelau has no official capital; the meeting place of the Tokelau Council rotates annually.': + notes.note.tokelau.fields.field.country-info: 'Von Neuseeland abhängiges Gebiet. Tokelau hat keine offizielle Hauptstadt; der Treffpunkt des Tokelauer Rates wechselt jährlich.' + Easter Island: + notes.note.easter-island.fields.field.country: Osterinsel + Eruptions in 1997 destroyed the capital city of Plymouth, making Brades the de facto capital.: + notes.note.montserrat.fields.field.capital-info: 'Nach einer Reihe von Vulkanausbrüchen wurde die eigentliche Hauptstadt Plymouth 1997 aufgegeben und Brades zur de-facto-Hauptstadt.' + External territory of Australia.: + notes.note.christmas-island.fields.field.country-info: 'Australisches Außengebiet.' + notes.note.cocos-keeling-islands.fields.field.country-info: 'Australisches Außengebiet.' + notes.note.norfolk-island.fields.field.country-info: 'Außengebiet von Australien.' + 'Galápagos Islands': + notes.note.gal-pagos-islands.fields.field.country: Galapagosinseln + Mount Athos: + notes.note.mount-athos.fields.field.country: Athos + Norfolk Island: + notes.note.norfolk-island.fields.field.country: Norfolkinsel + Not a sovereign country: + notes.note.norfolk-island.fields.field.capital-hint: 'Kein souveräner Staat' + Overseas territory of France.: + notes.note.saint-barth-lemy.fields.field.country-info: 'Französisches Überseegebiet.' + Overseas territory of the United Kingdom.: + notes.note.british-indian-ocean-territory.fields.field.country-info: 'Britisches Überseegebiet.' + notes.note.montserrat.fields.field.country-info: 'Überseegebiet des Vereinigten Königreichs.' + notes.note.pitcairn-islands.fields.field.country-info: 'Britisches Überseegebiet.' + Part of Saint Helena, Ascension and Tristan da Cunha, an overseas territory of the United Kingdom.: + notes.note.saint-helena.fields.field.country-info: 'Teil von St. Helena, Ascension und Tristan da Cunha, einem Überseegebiet des Vereinigten Königreichs.' + Pitcairn Islands: + notes.note.pitcairn-islands.fields.field.country: Pitcairninseln + Polynesian island and special territory of Chile.: + notes.note.easter-island.fields.field.country-info: Polynesische Insel unter Chilenischer Regierung. + Province of Ecuador.: + notes.note.gal-pagos-islands.fields.field.country-info: Ecuadorianische Provinz. + 'Saint Barthélemy': + notes.note.saint-barth-lemy.fields.field.country: 'Saint-Barthélemy' + Saint Helena: + notes.note.saint-helena.fields.field.country: St. Helena + 'Sierra Leone (slightly lighter blue)': + notes.note.gal-pagos-islands.fields.field.flag-similarity: 'Sierra Leone (etwas helleres Blau)' + Special municipality: + notes.note.sint-eustatius.fields.field.capital-hint: Besondere Gemeinde + Special municipality of the Netherlands.: + notes.note.bonaire.fields.field.country-info: Besondere Gemeinde der Niederlande. + notes.note.saba.fields.field.country-info: Besondere Gemeinde der Niederlande. + notes.note.sint-eustatius.fields.field.country-info: Besondere Gemeinde der Niederlande. + adapter_ids: + crowdanki:guid: + 'ArmciP%a;a': 'MkUL%72%~W' + 'd~3m`ihA*b': 'LQy|aYu1j>' + 'ga97y4Zy0{': 'H3`{Wgs:76' + 'i;=jdbhl(w': 'Q.;-RG8ri4' + 'iq7o#n0juD': 'pan$P6;e=>' + 'j=,1:f39$]': 'Fpw!^G_FDy' + 'k3]]2^DP1R': 'qfk|s},0L9' + 'khTp#J9C!!': 'P@lPUyD7Lm' + 'mCh>s18~IX': 'PleM${.)8^' + mvNnHkGnVl: 's(%/V{:-A(' + 'nNM;lK%' + 'qAsU}ONgC#': 'z1k!ACW8^y' + 'r3?J{NB0J3': 'prEIV(Y5@o' + 'rHh8*,y.yc': 't!8/EM-' + 'rNv|0qxR]K': 'j_W6YP&/cN' + '~)7-62ytBX': 'L$iBOE&p}o' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml new file mode 100644 index 0000000..ac88a6a --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml @@ -0,0 +1,81 @@ +id: overlay.translation.hardcore.es +kind: translation +translations: + changes: + Autonomous city of Spain: + notes.note.melilla.fields.field.country-info: 'Ciudad autónoma de España.' + Autonomous city of Spain.: + notes.note.ceuta.fields.field.country-info: 'Ciudad autónoma de España.' + Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain.: + notes.note.mount-athos.fields.field.country-info: 'Estado monástico ortodoxo autónomo en la parte noreste de Grecia, también conocido como la Montaña Sagrada.' + British Indian Ocean Territory: + notes.note.british-indian-ocean-territory.fields.field.country: 'Territorio Británico del Océano Índico' + Christmas Island: + notes.note.christmas-island.fields.field.country: Isla de Navidad + 'Cocos (Keeling) Islands': + notes.note.cocos-keeling-islands.fields.field.country: Islas Cocos + 'Dependent territory of New Zealand. Tokelau has no official capital; the meeting place of the Tokelau Council rotates annually.': + notes.note.tokelau.fields.field.country-info: 'Territorio dependiente de Nueva Zelanda. Tokelau no tiene capital oficial; el sitio de encuentro del Consejo de Tokelau rota anualmente.' + Easter Island: + notes.note.easter-island.fields.field.country: Isla de Pascua + Eruptions in 1997 destroyed the capital city of Plymouth, making Brades the de facto capital.: + notes.note.montserrat.fields.field.capital-info: Las erupciones de 1997 destruyeron la capital de Plymouth, haciendo Brades la capital de facto. + External territory of Australia.: + notes.note.christmas-island.fields.field.country-info: Territorio externo de Australia. + notes.note.cocos-keeling-islands.fields.field.country-info: Territorio externo de Australia. + notes.note.norfolk-island.fields.field.country-info: Territorio externo de Australia. + 'Galápagos Islands': + notes.note.gal-pagos-islands.fields.field.country: 'Islas Galápagos' + Mount Athos: + notes.note.mount-athos.fields.field.country: Monte Athos + Norfolk Island: + notes.note.norfolk-island.fields.field.country: Isla Norfolk + Not a sovereign country: + notes.note.norfolk-island.fields.field.capital-hint: 'No es un país soberano' + Overseas territory of France.: + notes.note.saint-barth-lemy.fields.field.country-info: Territorio de ultramar de Francia. + Overseas territory of the United Kingdom.: + notes.note.british-indian-ocean-territory.fields.field.country-info: Territorio de ultramar del Reino Unido. + notes.note.montserrat.fields.field.country-info: Territorio de ultramar del Reino Unido. + notes.note.pitcairn-islands.fields.field.country-info: Territorio de ultramar del Reino Unido. + Part of Saint Helena, Ascension and Tristan da Cunha, an overseas territory of the United Kingdom.: + notes.note.saint-helena.fields.field.country-info: 'Parte de Santa Elena, Ascensión y Tristán de Acuña, un territorio de ultramar del Reino Unido.' + Pitcairn Islands: + notes.note.pitcairn-islands.fields.field.country: Islas Pitcairn + Polynesian island and special territory of Chile.: + notes.note.easter-island.fields.field.country-info: Isla de la Polinesia y territorio especial de Chile. + Province of Ecuador.: + notes.note.gal-pagos-islands.fields.field.country-info: Provincia de Ecuador. + 'Saint Barthélemy': + notes.note.saint-barth-lemy.fields.field.country: 'San Bartolomé' + Saint Helena: + notes.note.saint-helena.fields.field.country: Santa Helena + 'Sierra Leone (slightly lighter blue)': + notes.note.gal-pagos-islands.fields.field.flag-similarity: 'Sierra Leona (azul ligeramente más claro)' + Sint Eustatius: + notes.note.sint-eustatius.fields.field.country: San Eustaquio + Special municipality: + notes.note.sint-eustatius.fields.field.capital-hint: Es un municipio especial + Special municipality of the Netherlands.: + notes.note.bonaire.fields.field.country-info: 'Municipio especial de los Países Bajos.' + notes.note.saba.fields.field.country-info: 'Municipio especial de los Países Bajos.' + notes.note.sint-eustatius.fields.field.country-info: 'Municipio especial de los Países Bajos.' + adapter_ids: + crowdanki:guid: + 'ArmciP%a;a': 'k?SUMTaEzl' + 'd~3m`ihA*b': 'zN<)OWkrGG' + 'ga97y4Zy0{': 'N32K/E2ME^' + 'i;=jdbhl(w': 'JqG7,i}^M#' + 'iq7o#n0juD': 'Ed[OQ]II[]' + 'j=,1:f39$]': 'rKe&a,vxE:' + 'k3]]2^DP1R': 'zgEsqA)/M?' + 'khTp#J9C!!': 'n:Y4?FV.r9' + 'mCh>s18~IX': 'K/neGJj;{X' + mvNnHkGnVl: 'lcrrXX*L,[' + 'nN9VSV0}' + 'rHh8*,y.yc': 'MvAmJR.a*H' + 'rNv|0qxR]K': 'iVZ:94)BH<' + '~)7-62ytBX': 'p{AX:uXi_<' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml new file mode 100644 index 0000000..975c244 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml @@ -0,0 +1,83 @@ +id: overlay.translation.hardcore.fr +kind: translation +translations: + changes: + Autonomous city of Spain: + notes.note.melilla.fields.field.country-info: 'Ville autonome d’Espagne.' + Autonomous city of Spain.: + notes.note.ceuta.fields.field.country-info: 'Ville autonome d’Espagne.' + Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain.: + notes.note.mount-athos.fields.field.country-info: 'République monastique autonome au nord-est de la Grêce, aussi connue comme la Sainte-Montagne.' + British Indian Ocean Territory: + notes.note.british-indian-ocean-territory.fields.field.country: 'Territoire britannique de l''océan Indien' + Christmas Island: + notes.note.christmas-island.fields.field.country: 'Île Christmas' + 'Cocos (Keeling) Islands': + notes.note.cocos-keeling-islands.fields.field.country: 'Îles Cocos' + 'Dependent territory of New Zealand. Tokelau has no official capital; the meeting place of the Tokelau Council rotates annually.': + notes.note.tokelau.fields.field.country-info: 'Territoire dépendant de la Nouvelle-Zélande. La capitale, de même que la présidence du Conseil permanent du Gouvernement, tournent entre les trois atolls.' + Easter Island: + notes.note.easter-island.fields.field.country: 'Île de Pâques' + Eruptions in 1997 destroyed the capital city of Plymouth, making Brades the de facto capital.: + notes.note.montserrat.fields.field.capital-info: 'Des éruptions en 1997 ont détruit la capitale Plymouth, faisant de Brades la capitale de fait.' + External territory of Australia.: + notes.note.christmas-island.fields.field.country-info: 'Territoire extérieur australien.' + notes.note.cocos-keeling-islands.fields.field.country-info: 'Territoire extérieur australien.' + notes.note.norfolk-island.fields.field.country-info: 'Territoire extérieur australien.' + 'Galápagos Islands': + notes.note.gal-pagos-islands.fields.field.country: 'Îles Galápagos' + Karyes: + notes.note.mount-athos.fields.field.capital: 'Karyès' + Mount Athos: + notes.note.mount-athos.fields.field.country: Mont Athos + Norfolk Island: + notes.note.norfolk-island.fields.field.country: 'Île Norfolk' + Not a sovereign country: + notes.note.norfolk-island.fields.field.capital-hint: Pas une nation souveraine + Overseas territory of France.: + notes.note.saint-barth-lemy.fields.field.country-info: 'Collectivité française d''outre-mer.' + Overseas territory of the United Kingdom.: + notes.note.british-indian-ocean-territory.fields.field.country-info: 'Territoire britannique d''outre-mer.' + notes.note.montserrat.fields.field.country-info: 'Territoire britannique d''outre-mer.' + notes.note.pitcairn-islands.fields.field.country-info: 'Territoire britannique d''outre-mer.' + Part of Saint Helena, Ascension and Tristan da Cunha, an overseas territory of the United Kingdom.: + notes.note.saint-helena.fields.field.country-info: 'Fait partie de Sainte-Hélène, Ascension et Tristan da Cunha, un territoire britannique d''outre-mer.' + Pitcairn Islands: + notes.note.pitcairn-islands.fields.field.country: 'Îles Pitcairn' + Polynesian island and special territory of Chile.: + notes.note.easter-island.fields.field.country-info: 'Île polynésienne et territoire spécial du Chili.' + Province of Ecuador.: + notes.note.gal-pagos-islands.fields.field.country-info: 'Province de l''Équateur.' + 'Saint Barthélemy': + notes.note.saint-barth-lemy.fields.field.country: 'Saint-Barthélemy' + Saint Helena: + notes.note.saint-helena.fields.field.country: 'Sainte-Hélène' + 'Sierra Leone (slightly lighter blue)': + notes.note.gal-pagos-islands.fields.field.flag-similarity: 'Sierra Leone (bleu légèrement plus clair)' + Sint Eustatius: + notes.note.sint-eustatius.fields.field.country: Saint-Eustache + Special municipality: + notes.note.sint-eustatius.fields.field.capital-hint: 'Commune à statut particulier' + Special municipality of the Netherlands.: + notes.note.bonaire.fields.field.country-info: 'Commune des Pays-Bas à statut particulier.' + notes.note.saba.fields.field.country-info: 'Commune des Pays-Bas à statut particulier.' + notes.note.sint-eustatius.fields.field.country-info: 'Commune des Pays-Bas à statut particulier.' + adapter_ids: + crowdanki:guid: + 'ArmciP%a;a': 'b{vct.#IZc' + 'd~3m`ihA*b': 'n&_>y_UGVj' + 'ga97y4Zy0{': 'j[t$1r]e!z' + 'i;=jdbhl(w': 'La@o-D7' + 'iq7o#n0juD': 'b(u8IoFmpT' + 'j=,1:f39$]': 'H@=}/tnJ+r' + 'k3]]2^DP1R': 'D&*IGrwa$z' + 'khTp#J9C!!': 'D%b^RAFZPX' + 'mCh>s18~IX': 't~:|totC7S' + mvNnHkGnVl: 'Ft%(~s0yZU' + 'nNh~F3D' + 'r3?J{NB0J3': 'ti,@|wZ>0w' + 'rHh8*,y.yc': 'Q~`$pP/xjA' + 'rNv|0qxR]K': 'B]FW$v]S+/' + '~)7-62ytBX': 'Iv0W>oAB^c' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/it.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/it.yaml new file mode 100644 index 0000000..d35e44a --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/it.yaml @@ -0,0 +1,22 @@ +id: overlay.translation.hardcore.it +kind: translation +translations: + adapter_ids: + crowdanki:guid: + 'ArmciP%a;a': 'n3X9D,,qq&' + 'd~3m`ihA*b': 'd5kTpV;w>8' + 'ga97y4Zy0{': 'dn-Q;H1&a6' + 'i;=jdbhl(w': 'n~BnbS~^PC' + 'iq7o#n0juD': 'o:fHe>s-zE' + 'j=,1:f39$]': 'rtTV>aanq;' + 'k3]]2^DP1R': 'M:s18~IX': 'GO^SB7bB_I' + mvNnHkGnVl: 'J-%2)|vJJh' + 'nNB;' + 'rHh8*,y.yc': tANaxvYd9 + 'rNv|0qxR]K': 'E}j_D&Q7mL' + '~)7-62ytBX': 'x!LQD,?v=4' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml new file mode 100644 index 0000000..edb3adf --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml @@ -0,0 +1,79 @@ +id: overlay.translation.hardcore.nb +kind: translation +translations: + changes: + Autonomous city of Spain: + notes.note.melilla.fields.field.country-info: 'Spansk by og enklave på Marokkos middelhavskyst.' + Autonomous city of Spain.: + notes.note.ceuta.fields.field.country-info: Selvstyrt by i Spania. + Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain.: + notes.note.mount-athos.fields.field.country-info: 'Autonom munkerepublikk i Nord-Hellas. Også kalt Det hellige berget.' + British Indian Ocean Territory: + notes.note.british-indian-ocean-territory.fields.field.country: Det britiske territoriet i Indiahavet + Christmas Island: + notes.note.christmas-island.fields.field.country: 'Christmasøya' + 'Cocos (Keeling) Islands': + notes.note.cocos-keeling-islands.fields.field.country: 'Kokosøyene' + 'Dependent territory of New Zealand. Tokelau has no official capital; the meeting place of the Tokelau Council rotates annually.': + notes.note.tokelau.fields.field.country-info: Newzealandsk territorium. + Easter Island: + notes.note.easter-island.fields.field.country: 'Påskeøya' + Eruptions in 1997 destroyed the capital city of Plymouth, making Brades the de facto capital.: + notes.note.montserrat.fields.field.capital-info: 'Vulkanutbrudd i 1997 ødela hovedstaden Plymouth, og Brades ble de facto hovedstad.' + External territory of Australia.: + notes.note.christmas-island.fields.field.country-info: 'Australsk territorium. ' + notes.note.cocos-keeling-islands.fields.field.country-info: Australsk territorium. + notes.note.norfolk-island.fields.field.country-info: Selvstyrt australsk territorium. + 'Galápagos Islands': + notes.note.gal-pagos-islands.fields.field.country: 'Galápagosøyene' + Mount Athos: + notes.note.mount-athos.fields.field.country: Athos + Norfolk Island: + notes.note.norfolk-island.fields.field.country: 'Norfolkøya' + Not a sovereign country: + notes.note.norfolk-island.fields.field.capital-hint: Ikke selvstendig land + Overseas territory of France.: + notes.note.saint-barth-lemy.fields.field.country-info: 'Fransk oversjøisk land.' + Overseas territory of the United Kingdom.: + notes.note.british-indian-ocean-territory.fields.field.country-info: Britisk besittelse. + notes.note.montserrat.fields.field.country-info: 'Britisk oversjøisk territorium i Karibia.' + notes.note.pitcairn-islands.fields.field.country-info: 'Oversjøisk britisk territorium.' + Part of Saint Helena, Ascension and Tristan da Cunha, an overseas territory of the United Kingdom.: + notes.note.saint-helena.fields.field.country-info: 'Del av det britiske oversjøiske territoriet Sankt Helena, Ascencion og Tristan da Cunha.' + Pitcairn Islands: + notes.note.pitcairn-islands.fields.field.country: 'Pitcairnøyene' + Polynesian island and special territory of Chile.: + notes.note.easter-island.fields.field.country-info: Territorium i Chile. + Province of Ecuador.: + notes.note.gal-pagos-islands.fields.field.country-info: Region i Ecuador. + 'Saint Barthélemy': + notes.note.saint-barth-lemy.fields.field.country: 'Saint-Barthélemy' + Saint Helena: + notes.note.saint-helena.fields.field.country: Sankt Helena + 'Sierra Leone (slightly lighter blue)': + notes.note.gal-pagos-islands.fields.field.flag-similarity: 'Sierra Leone (noe lysere blå)' + Special municipality: + notes.note.sint-eustatius.fields.field.capital-hint: 'Kommune med særstatus' + Special municipality of the Netherlands.: + notes.note.bonaire.fields.field.country-info: Kommune med spesiell status i Nederland. + notes.note.saba.fields.field.country-info: 'Kommune med særstatus i Nederland. Øy i Karibia.' + notes.note.sint-eustatius.fields.field.country-info: 'Kommune med særstatus i Nederland.' + adapter_ids: + crowdanki:guid: + 'ArmciP%a;a': 'MbG/JnL:&+' + 'd~3m`ihA*b': 'r7?UN(TOGN' + 'ga97y4Zy0{': 'P`s}X]%%n.' + 'i;=jdbhl(w': 'jIz>`|hUIr' + 'iq7o#n0juD': 'DW#h,Zuo@q' + 'j=,1:f39$]': 'zi5;ldYAqH' + 'k3]]2^DP1R': EnAv,,VVPR + 'khTp#J9C!!': 'plI1yBs18~IX': 'vs18~IX': 'r{6o3nrs*;' + mvNnHkGnVl: 'Bv_gnaZ#pV' + 'nN|0p!->PF' + 'rHh8*,y.yc': 'uXd1K*pY=RSRGrI?' + 'k3]]2^DP1R': 'GhLqns_#`/' + 'khTp#J9C!!': 'dV/s]*7V,D' + 'mCh>s18~IX': 'GCESq|^pef' + mvNnHkGnVl: 'gO+<:S:)b,' + 'nN%|#(ccw' + 'iq7o#n0juD': 'D%[sR22:93' + 'j=,1:f39$]': 'pCQ/diK:{C' + 'k3]]2^DP1R': 'P-D1eiKP[T' + 'khTp#J9C!!': 'OPChG<,5/8' + 'mCh>s18~IX': 'GnD:(T#C' + '~)7-62ytBX': 'fJKeT[,~9>' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/ru.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/ru.yaml new file mode 100644 index 0000000..3531f4f --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/ru.yaml @@ -0,0 +1,22 @@ +id: overlay.translation.hardcore.ru +kind: translation +translations: + adapter_ids: + crowdanki:guid: + 'ArmciP%a;a': ':0I7n0Gm6' + 'd~3m`ihA*b': 'js18~IX': 'DL(Ab~)_EX' + mvNnHkGnVl: 'L]+nu>Yt>A' + 'nNs18~IX': 'A^!89R}DC*' + mvNnHkGnVl: 'M`X9g|b#NS' + 'nNW`j0' + 'rHh8*,y.yc': 'AH%Sos=Q%/' + 'rNv|0qxR]K': 'vFxoO]fhlN' + '~)7-62ytBX': 'f_}$_(^i4x' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/zh.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/zh.yaml new file mode 100644 index 0000000..385c4e8 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/zh.yaml @@ -0,0 +1,22 @@ +id: overlay.translation.hardcore.zh +kind: translation +translations: + adapter_ids: + crowdanki:guid: + 'ArmciP%a;a': MYBD5zZ93Q + 'd~3m`ihA*b': 's_TLGy3(#c' + 'ga97y4Zy0{': MEXI8oC8fw + 'i;=jdbhl(w': 'lLgJB4-Rl&' + 'iq7o#n0juD': 'K6gAD,Ys$-' + 'j=,1:f39$]': 'Lk811V}xOV' + 'k3]]2^DP1R': 'L$*!I-x1[H' + 'khTp#J9C!!': 'OY97X~F2>M' + 'mCh>s18~IX': 'x;K^MIlxB~' + mvNnHkGnVl: vluaMTZoBr + 'nN65' + 'E3u*2.<9#`': 'kn6vhJtt!Y' + 'EWXNcy5(sRng' + 'Lol+XIvY21': 'P4d|2~&E#h' + 'M$bc0[jYf0': 'Ngx+p%Tn$u' + 'O~pXkAbd3': 'IxLAH)+-_V' + 'QSq%~rFaP-': 'pi]:s4:9TG' + 'Qlib4)q2Hk': 'x;/B{evQ?m' + 'SlOEXfq#|': 'BU*(=#&k=b' + 'W5?6yQZD2': 'd`,;^d_nt5' + '_]DH+g9!$': 'urEGY*T)mW' + 'b@M[XTc?}5': 'hV^z0k.t6x' + 'bD?R}i8_[c': 'wsH3aZT9(O' + 'bW)ch:vs1P': 'k5Zby(Llz+' + 'b[(2`4t&?J': 'QP7}%SW/px' + 'b^Try&me1A': 't6K(T;};3}' + 'b`5Q_Z=HQT': 'pq=[Hn+x(t' + 'bd~Ght5tLR': 'z^#eqWYO-D' + 'bk2ByZIeU+': 'B&Pa`TK.W@' + 'bnEK#)[;qT': 'i:L[HB&@F[' + 'c(,2{yXI#E': 'G4t&;Hch6N' + 'c,5(mUGzYT': 'j.J_!d]H{c' + 'cAlyc~G.y%': 'MfwiBI+|=9' + 'cEO^y)Md[G': 'yKo`$PaHGg' + 'cL?8!xP#Wj': 's|EEBYpJ*s' + 'cPO|Cru8n<': 't2Q}Uw:@3V' + 'cV4idO^r8W': 'r]11B;H~[z' + 'cYKvF#ptF7': 'p2-()%uZj7' + 'c^,~EC6Pb2': 'ebha1T}R8+' + 'ck!6;xK$/^': 'D!9j=Ss[H)' + 'cv+Nq9:ab>D;vQoW' + 'd3c`&/1Y^D': 'wOmUda6!>t' + 'd5m8%D6,;u': 'LXl!1P9Nfp' + 'dC:e5G4J/p': 'x`*~=TA+FK' + 'dUZ^=SE5BG': 'u2@(1}y4a{' + 'dWA49p}F{k': 'b;o' + 'dY96`C-f(_': 'A7Cj*hqC7[' + 'd``B*6:eCx': 's{g7@QD$qr' + 'dbP/Cj}sB+': 'IToJAz{P9K' + 'de=9c:,@j9': 'e_%R[08+Z' + 'dlU7VG|3Gd': 'O_++P3q,lg' + 'd}R:qHj1,7': 'nsv]+(L(N^' + 'e!KSebP}pt': 'homhic06#&' + 'e+/O]%*qfk': 'C{]JF%%mr*' + 'e.t$Vi5,>f': 'BA4?dToa9]' + 'e42BpV,P}C': 'x(aw,hH&Y[' + 'e5P*D#I{m{': 'Qe#D4C[<+m' + 'eHH?U': 'v]C@D/(-+g' + 'euW0jm=wfR': 'Pmp+_CoZ`T' + 'e{e8vi@^PY': 'b:(mUH@(`.' + 'f#4-L[mp5*': 'IoHWaA78;3' + 'f*2@qiHh_1': 'g.N+uMm,{6' + 'f*LO~>!0PN': 'l[/(,BJdiJ' + 'f21=-A~W;~': 'k[^`V_WQ^I' + 'f>c7x2E]Q6': 'PSEKL{AHnb' + 'fRSM{K+;[w': 'uWjhG1h]m' + 'fT!w8R#>dg': 'Ja&@${-ll#' + 'fV`&p81%d(': 'Fx%kVt2%kj' + fkq7lhLkdH: 'Pv4nPI$/4$' + 'fxAN),FAc$': 'zWj%QH!yMnZjBl' + 'gFOojQO(MH': 'vI7@NGK_Rr' + 'gG6SWfM>H0': 'f#ZCPMD62V' + 'gRVZ]qJ#>O': pvm.S00zT/ + 'gYIoR`|AxW': 'm-W32Cjt+v' + 'gePC:Xt_nW': dt/qfm,mMR + 'gp[h@]y13l': 'nLS!c^B.u5' + 'gsS{jR,]|Q': 'v8pkKiW5;U' + 'gx:;za!?C9': 'uJWy?`0JRW' + 'gz9${p}er*': 'noQ|9=PqcG' + 'g~%pF`(x{u': 'v0.(wt0=5,' + 'h&`>n98~V1': 'qjIBt_WC^1' + 'h9Jr(H[=1~': 'qq/hAV~,t/' + 'h?a*UKfT_M': 'Hfw[5^1FXT' + 'hE5j*e,Q?[': 'lJGFm$U&>?' + 'hNE9}>;Q&-': 'zT0L2L~T>H' + 'hXNTVF<:SH': 'y*>>K_Rv{P' + 'hZ/V/D&rO`': 'j1e,tg|Z&b' + 'h_9=mkZXAH': 'Lh8)I$R;q>' + hoHandbYAy: 'L>tNacmJP#' + 'hrI_uiW>E]': 'o-iAF(Oe;d' + 'h}lnSN_f$_': 'H,;72`%LJX' + 'h~5xz+=ke~': 'CzhyKE{%zy' + 'i)]+09JUR&': 'tOc]hT~n}*' + 'i2]v|D3@:j': 'wO[~:de]T' + 'iB-8Br~onq': 'G4|N9mDxa&' + 'iIS#Nw]tr]': 'j}.YHgP+1)' + 'iR;Um(L/~c=': 'juCeYT&eX9' + 'i^J8&$(1]|': 'vg&(+g]]yT' + 'ia8as9sCY|': 'p|~)@{)+S*' + 'igiK,ff[eO': 'bfu8gGy]?r' + 'ijP>aJBT!9': 'Hb~5J(.Fg~' + 'ilGtw#=asM': 'l]+1ZOhWt<' + 'j%*8gN)q>d': 'JJzRb!:b#h' + 'j)i)pB*HJ,': 'Kx0E[9ibDy' + 'j2IQ=f=w3@': 'DBo3+cT-[:' + j7eEZkzsCZ: 'BCT|*-aN8t' + 'j9:K,~DQ2v': 'MY>?+-m40S' + 'jg|}': 'za4sz~iaR*' + 'jCd]`-=k,:': 'uv?Z2p<%;s' + 'jDA-V?/hVj': 'wM29>6E7w2' + jI9P-f6r3M: 'DE`CrM74%0' + 'jKIDq|}J&c': 'i9-RjgM[_,' + 'jKO,9y0M;#': 'AnTl}_8m=/' + 'jN^Vc%9OQ5': 'L!)340&$>j' + 'jN|NAUP*h}': 'rNr_y>SmaNv-o' + 'j|z@PMgdx,': 'vt!WOLxZl$' + 'k$1_yaF?9#': 'Aq@4iLUo[t' + 'k/u1:B%DJH': 'tcsX~cQjI2' + 'k0{O[6l]bH': LYe,4PfG4a + 'k1]FH+s8j@': 'B>y*UU741f' + 'k9E.p^UY=~g;v' + 'kX]hN=_.c-': 'G5M^3c_/X|' + 'kZ52zV#[Iv': 'HqC57OPq[I' + 'kopwzzB``A': sv5mAkY3Kf + 'k}4/oj#$~s': 'P,t][$~Axv' + 'k}A9]O:0xw': 'Q`OBBA(IqJ' + 'l$2O|9w`F~': 'M*;rH]4K.>' + 'l$zM~ihPFE': 'D8N9aGT(YL' + 'lI)Ah/E': 'p2,K;bOB~T' + 'l*j*EE:!Y?': 'dET^[R~4O3' + 'l+>Ki!j#v+': 'C%r3!}*o9-' + 'l3Ly8PBxt(': 'j^9KkW@1V^' + 'lAxH.CfJB@': 'stl,m~cAO&' + 'lH^2D]w;[H': 'm~5*]PDD9k' + 'lU@C3' + 'm*#%sE.`.;': 'qd!suvo&1v' + 'm*PYE=#wr^': 'CO+b,zPO_I' + 'm2F|cUkKhv': 'ddf7bzEG~m' + 'm5j,b/y`:m' + 'mk1VltJl<]': 'c@Ms0SL)sn' + 'mk20`,N=`=': 'Byv;->|tVn' + 'mkgcU7[bA%': 'dr(`ZC7T|g>': 'bhv8b|]kY9' + 'mz[>>AxIr4': 'Pqi/=VQrpE' + 'm{noqE29%I': 'p;&<%/w5b|' + 'm{oM,jr>C#': 'h~hx$TrJ9L' + 'n&>e`2FO/S': jDvPRSN7UT + 'n+t>2`:;P/': 'nG#=Zxp5Qn' + 'n:7&>YT5A=': 'xMDjNS>?hL' + 'nIfRncS>' + oV/JG7fwZq: 'Hm4bJ/yz': 'sg,bj=%n1w' + 'oa87&PB&Bm': 'y9[[$e%qC`' + 'onMKID2d;D': 'm.mLjg-{lM' + 'o~}KVA:VbI': 'BAz%}9{UE' + 'p#R$-mG/L': 'f[E%u}3F%e' + 'p.yv>BQ470': 'Nz_#ah$j&d' + 'p:SPZ:e/*[': 'C]DrFhd%u': 'G4?YxB/g': 'nlr&VdfBJA' + 'q2#Yv[O0:|': 'uq)KM}CVsF' + 'q7do-wLx%E': 'GJT]wWpqv6' + 'q8][)Ri}=q': 'm|F4][J.!m' + 'q8fF$4,c6_': 'Qt7ILf!6y2' + 'q:XyGf#>Wg': 'c#$?0jnW0K' + 'q=w[iu~' + 'q[}7hk(+=e': 'k`{m~8Eb`.' + 'qiv/4tOlf=': 'k~[RV@fB$d+$': 'vlkjlV^U~T' + 'r(h9&#wc~B': 'h:!&t0Lvqc' + 'r/B|ra%L,.': 'bbW|f)4_>u' + 'r5C646H*+*': 'B*Me@Zh_[S' + 'r6BV%|..*5': 'Cob1W^QJ3F' + 'raI-$$|wT': 'pbX.~.6OJO' + 'r>g<|,FWi%': 'Rh_*(xz;/]' + 'r?E$4f-d0/': 'fPO>]fFure' + 'rPekx@n9:L': 'K4#|XaqO:g' + 'rSneU!3}bn': 'c!B%9S]zNK' + 'r]*Ioa%$bK': 'I[3;:2u9!5' + 's#:aIK$9^x': 'g-3%G(QyEf' + 's4E?ZF[E<>': 'dL;ASbD>B}' + 's9-GL*@AXD': 'uMj[iKx*FT' + 's<_WLSB3I/': 'l]B_A,?ph!' + 's>f40rn,O]': 'HXMK8^/pUt' + 'sB7rZQsR<1FFUo' + sM0GPt1HS1: 'I_$^q=J15;' + 'sRw-ik:I$v': 'v6t>$@9w~i^p': 'iR6^@_]dvR' + 'skOM(?n$GG': 'kC{ALA>P%a' + spKoFRM4if: 'sLc+1O/t|!' + 'ssbyA9}]QP': 'Jf_>OgL@4?' + 'stRa/S(0$F': 'wC(@I#Qj=_' + 'sxOMs+bE8@': 'vFrU{gPXOA' + 'sx|l+io`B@': 'z.;^Kjw?w-' + 's|K&2S,UP|': 'mH(F;!A-^i' + 't$y1C2R-Av': 'gsF]bu>P': 'P*^/<)(KjW' + 't2&ri|[Un:': 'o!L7Ln:)+/' + t3aZXtpNyi: '^@~8x/eFp' + 't50y/[AEhF': 'FQm}Y9aG^*' + 'tMMtBUpb$&': 'CP|BiC2[I' + 't`cagaWApo': 'bu.JnK@qu%' + 'tcneuK7v%`': 'c0H>DK:hY.' + tfCY4j6KEZ: 'O_`cb-?a@m': 'KM9;RF{k!2cXc': 'wsB.ZRe68:' + x2d4n4ADcP: 'Rf#Y$PY2hD' + 'xb&P*pH(Q': 'FUCfa4kb{|K' + 'zSc]-^U=0=': 'l1uc5E8j;X' + crowdanki:uuid: + 43e2586a-9a65-11e8-a777-a0481cc15658: 47d13c81-b471-4471-8e87-ebad53fa7307 diff --git a/fixtures/ultimate-geography/overlays/languages/da.yaml b/fixtures/ultimate-geography/overlays/languages/da.yaml new file mode 100644 index 0000000..88a7315 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/da.yaml @@ -0,0 +1,703 @@ +id: overlay.translation.da +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': 'Der eksisterer en navnekonflikt mellem landene ved havet, hvor Sydkorea især støtter navnet Østhavet.' + Abkhazia: Abkhasien + Addis Ababa: Addis Abeba + Adriatic Sea: Adriaterhavet + Aegean Sea: 'Ægæiske Hav' + Africa: Afrika + Akrotiri and Dhekelia: Akrotiri og Dhekelia + Albania: Albanien + Algeria: Algeriet + Algiers: Algier + Also known as Burma.: 'Også kendt som Burma.' + Also known as Cabo Verde.: '' + Also known as Czechia.: '' + Also known as East Timor.: 'Også kendt som Timor-Leste.' + Also known as Kiev.: 'Også kendt som Kiev.' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: 'Også kendt som Siambugten.' + Also known as the Sea of Cortez.: '' + 'Also spelled as Sana''a.': '' + American Samoa: Amerikansk Samoa + 'Andorra (narrower, coat of arms with motto)': 'Andora (smallere, våbenskjold med motto)' + Antarctica: Antarktis + Antigua and Barbuda: Antigua og Barbuda + Arabian Sea: Det Arabiske Hav + Aral Sea: 'Aralsøen' + Arctic Ocean: Det Nordlige Ishav + Armenia: Armenien + Ashgabat: Asjkhabad + Asia: Asien + Athens: Athen + Atlantic Ocean: Atlanterhavet + Australia: Australien + 'Australia (white stars, two more stars)': 'Australien (hvide stjerner, to stjerner mere)' + Austria: 'Østrig' + 'Austria (brighter red, wider white band)': 'Østrig (lysere rød, bredere hvid stribe)' + Autonomous community of Spain.: Selvstyrende region af Spanien. + Autonomous province of South Korea.: Selvstyrende provins i Sydkorea. + Autonomous region of Finland.: Selvstyrende region i Finland. + Autonomous region of Italy.: Selvstyrende region i Italien. + Autonomous region of Papua New Guinea.: Selvstyrende region i Papua New Guinea. + Autonomous region of Portugal.: + notes.note.azores.fields.field.country-info: 'Selvstændig region Autonomous region i Portugal.' + notes.note.madeira.fields.field.country-info: 'Selvstændig region i Portugal.' + Azerbaijan: Aserbajdsjan + Azores: Azorerne + Baghdad: Bagdad + 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrain (smallere, færre takkede kanter, rød)' + Balkan Peninsula: 'Balkanhalvøen' + Baltic Sea: 'Østersøen' + Banda Sea: Bandahavet + Barents Sea: Barentshavet + Bay of Bengal: Bengalske Bugt + Bay of Biscay: Biscayabugten + Belgium: Belgien + Belgrade: Beograd + Bering Strait: 'Beringstrædet' + Bishkek: Bisjkek + Black Sea: Sortehavet + 'Bolivia (coat of arms instead of star)': 'Bolivia (våbenskjold i stedet for stjerne)' + Bosnia and Herzegovina: Bosnien-Hercegovina + 'Brasília': Brasilia + Brazil: Brasilien + British Virgin Islands: 'De Britiske Jomfruøer' + Brussels: Bruxelles + Bucharest: Bukarest + Bulgaria: Bulgarien + Cambodia: Cambodja + Cameroon: Cameroun + 'Cameroon (green/red/yellow, yellow star)': 'Cameroun (grøn/rød/gul, gul stjerne)' + Canary Islands: 'De Kanariske Øer' + Cape Verde: Kap Verde + Caribbean Sea: Caribiske Hav + Caspian Sea: Kaspiske Hav + Cayman Islands: 'Caymanøerne' + Celebes Sea: Celebeshavet + Celtic Sea: Det Keltiske hav + Central African Republic: Den Centralafrikanske Republik + Cetinje is an honorary capital.: Cetinje har status som Montenegros gamle royale hovedstad. + Chad: Tchad + 'Chad (slightly darker blue)': 'Tchad (lidt mørkere blå)' + China: Kina + 'Chișinău': Chisinau + City of San Marino: San Marino + Claimed and controlled: 'Gjort krav på og kontrolleret' + Claimed but not controlled: 'Gjort krav på, men ikke kontrolleret' + 'Colombia (no coat of arms)': 'Colombia (intet våbenskjold)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Colombo bliver ofte benævnt som hovedstad, men Sri Jayawardenapura Kotte, en forstad til Colombo, er den officielle hovedstad.' + Comoros: Comorerne + Constituent country in the Kingdom of Denmark.: 'Selvstyrende nation i Det Danske Rigsfællesskab.' + Constituent country of the Kingdom of the Netherlands.: 'Selvstændig stat i Kongeriget Nederlandene.' + Constituent country of the United Kingdom.: Delvis selvstyrende nation i Storbritannien. + Cook Islands: 'Cookøerne' + Copenhagen: 'København' + Coral Sea: Koralhavet + Corsica: Korsika + Croatia: Kroatien + Crown dependency of the United Kingdom.: Britisk kronbesiddelse. + 'Cuba (red triangle, blue stripes)': 'Cuba (rød trekant, blå striber)' + 'Curaçao (two stars in top-left corner)': 'Curaçao (to stjerne i øverste venstre hjørne)' + Cyprus: Cypern + Czech Republic: Tjekkiet + Damascus: Damaskus + Dead Sea: 'Dødehavet' + Democratic Republic of the Congo: Den Demokratiske Republik Congo + Denmark: Danmark + Denmark Strait: 'Danmarksstrædet' + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': 'Omtvistet; gjort krav på af Israel; Ramallah er det administrative centrum.' + 'Disputed; claimed by Palestine.': 'Omtvistet; gjort krav på af Palæstina.' + Dominican Republic: Den Dominikanske Republik + Dushanbe: Dusjanbe + East China Sea: 'Det Østkinesiske Hav' + East Siberian Sea: 'Det Østsibiriske Hav' + 'Ecuador (with coat of arms)': 'Ecuador (med våbenskjold)' + Egypt: 'Ægypten' + 'Egypt (emblem instead of text), Yemen (no text)': 'Ægypten (emblem i stedet for tekst), Yemen (ingen tekst)' + 'Egypt (with emblem), Iraq (with text)': 'Ægypten (med emblem), Irak (med tekst)' + 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (forskelligt våbenskjold, lidt mørkere blå)' + English Channel: Den Engelske Kanal + Equatorial Guinea: 'Ækvatorialguinea' + Estonia: Estland + Ethiopia: Etiopien + Europe: Europa + European Union: 'Den Europæiske Union' + Falkland Islands: 'Falklandsøerne' + Faroe Islands: 'Færøerne' + Federated States of Micronesia: Mikronesien + Formerly Zaire.: Tidligere kendt som Zaire. + Formerly known as Macedonia.: Tidligere kendt som Makedonien. + France: Frankrig + French Guiana: Fransk Guyana + French Polynesia: Fransk Polynesien + Georgia: Georgien + Germany: Tyskland + 'Ghana (star instead of coat of arms)': 'Ghana (stjerner i stedet for våbenskjold)' + Greece: 'Grækenland' + Greenland: 'Grønland' + 'Guinea (green and red flipped, slightly darker green)': 'Guinea (grøn og rød byttet rundt, lidt mørkere grøn)' + Gulf of Alaska: Alaskagolfen + Gulf of California: Den Californiske Havbugt + Gulf of Carpentaria: Carpentariabugten + Gulf of Guinea: Guineabugten + Gulf of Mexico: Den Mexicanske Golf + Gulf of Thailand: Thailandbugten + 'Hagåtña': Agana + Havana: Havanna + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: Historisk og kulturel region i Nordeuropa som indeholder landene Danmark, Norge og Sverige, og nogle gange Finland og Island. + Hong Kong: HongKong + Hudson Bay: Hudsonbugten + Hungary: Ungarn + Iceland: Island + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Island (blå baggrund, rødt og hvidt kors), Norge (rød baggrund, blåt og hvidt kors)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Island (blå baggrun, rødt kors), Færøerne (hvid baggrund, rødt og blåt kors)' + Independent state claimed by Georgia.: 'Uafhængig stat som Georgien gør krav på.' + Independent state claimed by Moldova.: 'Uafhængig stat som Moldova gør krav på.' + Independent state claimed by Somalia.: 'Uafhængig stat som Somalia gør krav på.' + India: Indien + Indian Ocean: Det Indiske Ocean + Indonesia: Indonesien + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesien (hvid og rød byttet rundt, lysere rød), Monaco (hvid og rød byttet rundt, smallere)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesien (bredere, lysere rød), Polen (rød og hvid byttet rundt, bredere)' + Iraq: Irak + 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (tekst i stedet for emblem), Yemen (intet emblem)' + Ireland: Irland + 'Ireland (orange and green flipped, wider)': 'Irland (orange og grøn byttet rundt, bredere)' + Island of Indonesia.: 'Ø i Indonesien.' + Italy: Italien + Ivory Coast: Elfenbenskysten + 'Ivory Coast (green and orange flipped, narrower)': 'Elfenbenskysten (grøn og orange byttet rundt, smallere)' + Jakarta: Djakarta + Jeju: Cheju-do + Kathmandu: Katmandu + Kazakhstan: Kasakhstan + Known as Nur-Sultan between 2019 and 2022: Kendt som Nur-Sultan mellem 2019 og 2022 + Known as Swaziland until 2018.: Kendt som Swaziland til 2018. + Kuwait City: Kuwait + Kyrgyzstan: Kirgisistan + Laayoune: El Aaiun + Labrador Sea: Labradorhavet + Latvia: Letland + 'Latvia (darker red, narrower white band)': 'Letland (mørkere rød, smallere hvid stribe)' + Lebanon: Libanon + Libya: Libyen + Lisbon: Lissabon + Lithuania: Litauen + 'Luxembourg (lighter blue)': 'Luxembourg (lysere blå)' + Luxembourg City: Luxembourg + Macau: Macao + Madagascar: Madagaskar + Maldives: Maldiverne + 'Mali (red and green flipped, slightly brighter green)': 'Mali (rød og grøn byttet rundt, lidt lysere grøn)' + Marshall Islands: 'Marshalløerne' + Mauritania: Mauretanien + Mediterranean Sea: Middelhavet + Melanesia: Melanesien + Micronesia: Mikronesien + 'Moldova (wider, coat of arms with eagle)': 'Moldova (bredere, våbenskjold med ørn)' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (smallere, mørkere rød), Polen (rød og hvid byttet rundt, mørkere rød)' + Mongolia: Mongoliet + Morocco: Marokko + Moscow: Moskva + 'N''Djamena': Ndjamena + 'Nauru (single star below yellow band)': 'Nauru (enkelt stjerne under gul stribe)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru har ingen officiel hovedstad; Yaren distriktet er de facto hovedstad.' + Netherlands: Nederlandene + 'Netherlands (darker blue)': 'Nederlandene (mørkere blå)' + New Caledonia: Ny Kaledonien + 'New Zealand (red stars, two fewer stars)': 'New Zealand (røde stjerner, to færre stjerner)' + 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (forskelligt våbenskjold, lidt lysere blå)' + North America: Nordamerika + North Korea: Nordkorea + North Macedonia: Nordmakedonien + North Nicosia: Nordnicosia + North Sea: 'Nordsøen' + Northern Cyprus: Nordcypern + Northern Ireland: Nordirland + Northern Mariana Islands: Nordmarianerne + Norway: Norge + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norge (rød baggrund, blåt og hvidt kors), Færøerne (hvid baggrund, rødt og blåt kors)' + Norwegian Sea: Norskehavet + Not a sovereign country: 'Ikke et selvstændigt land' + 'Nukuʻalofa': Nukualofa + 'Oblast (administrative region) of the Russian Federation.': Administrationsenhed i Rusland. + Oceania: Oceanien + Official capital was moved from Bujumbura to Gitega in 2019.: Den officielle hovedstad blev flyttet fra Bujumbura til Gitega i 2019. + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: Den officielle hovedstad blev flyttet fra Malabo til Ciudad de la Paz i 2026. + 'Officially Côte d''Ivoire.': 'Officielt Cote d’Ivoire.' + Officially Luxembourg.: '' + Overseas department of France.: 'Fransk oversøisk departement.' + Overseas territory of France.: + notes.note.french-polynesia.fields.field.country-info: 'Fransk oversøisk territorie.' + notes.note.new-caledonia.fields.field.country-info: 'Fransk oversøisk departement.' + notes.note.saint-martin.fields.field.country-info: 'Fransk oversøisk departement.' + notes.note.wallis-and-futuna.fields.field.country-info: 'Fransk oversøisk departement.' + Overseas territory of the United Kingdom.: + notes.note.akrotiri-and-dhekelia.fields.field.country-info: 'Britisk oversøisk territorie.' + notes.note.anguilla.fields.field.country-info: 'Oversøisk territorie i Storbritannien.' + notes.note.bermuda.fields.field.country-info: Delvis selvstyrende nation i Storbritannien. + notes.note.british-virgin-islands.fields.field.country-info: 'Oversøisk territorie i Storbritannien.' + notes.note.cayman-islands.fields.field.country-info: 'Oversøisk territorie i Storbritannien.' + notes.note.falkland-islands.fields.field.country-info: 'Britisk oversøisk territorie.' + notes.note.gibraltar.fields.field.country-info: 'Oversøisk territorie i Storbritannien.' + notes.note.turks-and-caicos-islands.fields.field.country-info: 'Oversøisk territorie i Storbritannien.' + Pacific Ocean: Stillehavet + Palestine: 'Palæstina' + 'Palestine (black/white/green, red arrow)': 'Palæstina (sort/hvid/grøn, rød pil)' + 'Palestine (no symbol)': 'Palæstina (intet symbol)' + Papua New Guinea: Papua Ny Guinea + Partially recognised state claimed by China.: 'Delvist anerkendt stat som Kina gør krav på.' + Partially recognised state claimed by Morocco. Also known as Western Sahara.: 'Delvist anerkendt stat som Marokko gør krav på. Også kendt som Saharawi Arabiske Demokratiske Republik.' + Partially recognised state claimed by Serbia.: 'Delvist anerkendt stat som Serbien gør krav på.' + Persian Gulf: Persiske Bugt + Philippine Sea: Filippinske Hav + Philippines: Filippinerne + Poland: Polen + Polynesia: Polynesien + Porto-Novo: Porto Novo + Prague: Prag + 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blå trekant, røde striber)' + 'Qatar (wider, more serrated edges, maroon)': 'Qatar (bredere, flere takkede kanter, kastanjabrun)' + Red Sea: 'Det Røde Hav' + Region of France.: Region i Frankrig. + Republic of the Congo: Republikken Congo + Romania: 'Rumænien' + 'Romania (slightly lighter blue)': 'Rumænien (lidt lysere blå)' + Rome: Rom + Russia: Rusland + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rusland (intet våbenskjold), Slovenien (bredere, mindre våbenskjold)' + Sahrawi Arab Democratic Republic: Vestsahara + Saint Kitts and Nevis: Saint Kitts og Nevis + Saint Martin: Saint-Martin + Saint Vincent and the Grenadines: Saint Vincent og Grenadinerne + Sanaa: Sana + Sardinia: Sardinien + Saudi Arabia: Saudi-Arabien + Scandinavia: Skandinavien + Scotland: Skotland + Sea of Galilee: 'Genesaret Sø' + Sea of Japan: Japanske Hav + Sea of Okhotsk: Det Okhotske Hav + Semi-autonomous region of Tanzania.: Semi-autonom del af Tanzania. + 'Senegal (green/yellow/red, green star)': 'Senegal (grøn/gul/rød, grøn stjerne)' + Serbia: Serbien + Seychelles: Seychellerne + Sicily: Sicilien + Sint Maarten: Sint-Maarten + Slovakia: Slovakiet + 'Slovakia (narrower, bigger coat of arms)': 'Slovakiet (smallere, større våbenskjold)' + 'Slovakia (with coat of arms)': 'Slovakiet (med våbenskjold)' + Slovenia: Slovenien + Solomon Islands: 'Salomonøerne' + South Africa: Sydafrika + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'Sydafrika har ingen juridisk defineret hovedstad: regeringens grene er delt i tre byer: Pretoria (udøvende), Cape Town (lovgivende) og Bloemfontein (dømmende).' + South America: Sydamerika + South China Sea: Det Sydkinesiske Hav + South Korea: Sydkorea + South Ossetia: Sydossetien + South Sudan: Sydsudan + South Tarawa: Sydtarawa + Southern Ocean: Det Sydlige Ishav + Sovereign country: 'Selvstændigt land' + Spain: Spanien + Special Administrative Region of China.: 'Særlig administrativ region i Kina.' + Sri Jayawardenepura Kotte: Sri Jayawardenapura Kotte + 'St. George''s': 'St. George’s' + 'St. John''s': 'St. John’s' + State of the United States.: Amerikansk stat. + State recognised only by Turkey and claimed by Cyprus.: 'Stat som kun bliver anerkendt af Tyrkiet og som Cypern gør krav på.' + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Område af Oceanien som indeholder tusindvis af små øer i det centrale og sydlige Stillehav.' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Område af Oceanien der indeholder tusindvis af små øer i det vestlige Stillehav.' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Område af Oceanien som indeholder Vanuatu, Salomonøerne, Fiji og Papua Ny Guinea.' + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (rød/hvid/sort, grøn pil), Vestsahara (med stjerne og halvmåne)' + Suriname: Surinam + Sweden: Sverige + Switzerland: Schweiz + 'Switzerland has no official capital; Bern is the de facto capital.': 'Schweiz har ingen officiel hovedstad; Bern er de facto hovedstad.' + Syria: Syrien + 'São Tomé and Príncipe': 'São Tomé og Príncipe' + Tajikistan: Tadsjikistan + Tashkent: Tasjkent + Tasman Sea: Tasmanske Hav + Tehran: Teheran + The Bahamas: Bahamas + The Gambia: Gambia + Thimphu: Timphu + Timor Sea: Timorhavet + Timor-Leste: 'Østtimor' + Transnistria: Transnistrien + Trinidad and Tobago: Trinidad og Tobago + Tunisia: Tunesien + Turkey: Tyrkiet + Turks and Caicos Islands: 'Turks- og Caicosøerne' + 'Tórshavn': Thorshavn + Ulaanbaatar: Ulan Bator + Unincorporated internal area of Norway.: 'Norsk øgruppe.' + Unincorporated territory of the United States.: 'Et såkaldt Unincorporated Territory under USA.' + United Arab Emirates: De Forenede Arabiske Emirater + United Kingdom: Storbritannien + United States Virgin Islands: 'De Amerikanske Jomfruøer' + United States of America: USA + Uzbekistan: Usbekistan + Vatican City: Vatikanstaten + Vienna: Wien + Wallis and Futuna: Wallis og Futuna + Warsaw: Warszawa + Washington, D.C.: Washington + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: 'Selvom Amsterdam er den forfatningsmæssige hovedstad, ligger regeringen i Haag.' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: 'Selvom Dodoma er den officielle hovedstad er Dar es Salaam de facto sæde for regeringen government.' + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': 'Selvom El Aaiun er den erklærede hovedstad er Tifariti de facto sæde for regeringen.' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: 'Selvom Mbabane er den officielle og udøvende hovedstad er Lobamba den traditionelle, sprituelle og lovgivende hovedstad.' + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: 'Selvom Porto-Novo er den officielle hovedstad, er Cotonou de facto sæde for regeringen.' + While Sucre is the constitutional capital, La Paz is the seat of government.: 'Sucre er officiel hovedstad, mens La Paz er regeringssædet.' + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: 'Selvom Yamoussoukro er den officielle hovedstad er Abidjan de facto sæde for regeringen.' + White Sea: Hvidehavet + World region covering the Australian continent and most of the islands in the Pacific Ocean.: 'Verdensdel som dækker det Australske kontinent og de fleste af øerne i Stillehavet.' + Yellow Sea: Gule Hav + Yerevan: Jerevan + 'Åland Islands': 'Åland' + additions: + notes.note.arctic-ocean.fields.field.country-info: 'Også kendt som Det Arktiske Ocean.' + notes.note.balkan-peninsula.fields.field.country-info: 'Også kendt som Balkan.' + notes.note.baltic-sea.fields.field.country-info: 'Også kaldt Baltiske Hav.' + notes.note.belarus.fields.field.country-info: 'Også kendt som Hviderusland. Udenrigsministeriet er begyndt at bruge betegnelsen Belarus i marts 2021.' + notes.note.netherlands.fields.field.country-info: 'Også kendt som Holland.' + notes.note.united-kingdom.fields.field.country-info: 'Officielt benævnt Det Forenede Kongerige Storbritannien og Nordirland.' + notes.note.united-states-of-america.fields.field.country-info: Formelt Amerikas Forenede Stater. + variables: + label.capital: + Capital: Hovedstad + label.capital-hint.answer: + 'Hint: {{Capital hint}}': 'Ledetråd: {{Capital hint}}' + label.location: + Location: Placering + note-type.name: + Ultimate Geography: 'Ultimate Geography [DA]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': 'Flaget ligner {{Flag similarity}}.' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': 'Kb<43(~@]3' + '5JJ2]i)p4': 'K2q=PpJ[C>' + '5S(f7%O-$': 'lO0wObq%O:' + '?n!)c/h=Q': 'l`9rE:$x+g' + 'AES],s:8=': 'x@&HED.U@#' + 'B:x`Z' + 'E3u*2.<9#`': 'sg.X:y`!db' + 'EWXNcTLrI' + 'G/^_##fcPN': 'OD>z~q-UNy' + 'Gn;q2Y)Xe4': 'D].(J{*+wj' + 'H*c[ML+`s=': 'q/Q9$4A[.0' + 'He!e=h?_2V': 'EOL]eWfA}#' + 'J%,fuysl*&': 'xp$uSWv4N4' + 'JOT:U4ayh@': 'h,wvQiW}f.' + 'K*b}' + 'L5M1*yG7eH': 'INxYDi%#ji' + LPF4AfaHKR: 'J+[lvz&;-,' + 'Lol+XIvY21': 'op{,HY,R^K' + 'M$bc0[jYf0': 'kIX7j=SKtG' + 'O~pXkAbd3': 'vzrC7`Rysy' + 'QSq%~rFaP-': 'F#aFYkho+R' + 'Qlib4)q2Hk': 'Oa[MIj/dzh' + 'SlOEXfq#|': 's&2|=WcRuB' + 'W5?6yQZD2': 'x!QK:aR+Kd' + '_]DH+g9!$': 'e6LM|~Rxa(' + 'b@M[XTc?}5': 'I|/.Z?idjd' + 'bD?R}i8_[c': 'GvAa_Oi{c}' + 'bW)ch:vs1P': Ql49tXZ51x + 'b[(2`4t&?J': 'JA26u,2`hg' + 'b^Try&me1A': 'ICS}K]:uYU' + 'b`5Q_Z=HQT': 'kwXL1I+,Te' + 'bd~Ght5tLR': 'soL3XG}20w' + 'bk2ByZIeU+': /E5Rk.RSJ + 'bnEK#)[;qT': 'm&XT%bsGw(' + 'c(,2{yXI#E': rt5LkWRaLo + 'c,5(mUGzYT': 'Ou]hx.]gl>' + 'cAlyc~G.y%': 'fuUqIYrwq`' + 'cEO^y)Md[G': 'jGWB8^WXiP' + 'cL?8!xP#Wj': 'rPc:I:)OP!' + 'cPO|Cru8n<': 'mspp{o}BqI' + 'cV4idO^r8W': 'i.e%#9&x' + 'dUZ^=SE5BG': 'vv!U3<)YQd' + 'dWA49p}F{k': 'CdND;!8x4h' + 'dY96`C-f(_': 'd.8!=y^r?T' + 'd``B*6:eCx': 'DZ4H}xv.@0' + 'dbP/Cj}sB+': 2DE5ZZSpp + 'de=9c:,@j9': 'qu7$T~h$aQ' + 'dlU7VG|3Gd': fuKjK4-6uB + 'd}R:qHj1,7': 'NUU&F_n.K*' + 'e!KSebP}pt': 'mc06W]r~in' + 'e+/O]%*qfk': 'HhCAt{`FkH' + 'e.t$Vi5,>f': 'm/#`_nWJ8r' + 'e42BpV,P}C': 'd[f%5t;UH5' + 'e5P*D#I{m{': y/GSUYA4Uz + 'eHH?U': 'bs8M8s)6X|' + 'euW0jm=wfR': 'vE8R)ABG0b' + 'e{e8vi@^PY': 'M(G<3?R+|c' + 'f#4-L[mp5*': 'E$0$>x=Zhj' + 'f*2@qiHh_1': t27nr9YA-z + 'f*LO~>!0PN': 'yO`O$6WvVk' + 'f21=-A~W;~': 'h{>w_]xn^~' + 'f>c7x2E]Q6': 'o860?{U!P?' + 'fRSM{K+;[w': mn3Fwwz5Ll + 'fT!w8R#>dg': 'HL![RP|Urq' + 'fV`&p81%d(': 'oaC6@Yele]' + fkq7lhLkdH: 'O:h(.Em}Y@' + 'fxAN),FAc$': 'OsLM>,b7vo' + 'g-]bb&[.E!': 'M&|@(/.of4' + 'g9v~y/;=z(': 'yyX`4Po,+5' + 'g<[f89~U)A': 'G:Du!hF%=;' + 'gBCR?!O*;E': 'ALw.(aKml7' + 'gB|K:+r?1V': 'm!,_BMop/#' + 'gDsLPj(9S#': 'i+DaDVG`8F' + 'gFOojQO(MH': 'G0Zq}|J@Hf' + 'gG6SWfM>H0': 'ofIXVVnbk+' + 'gRVZ]qJ#>O': 'z+R(l-qu:w' + 'gYIoR`|AxW': 'p>4`R>@ts5' + 'gePC:Xt_nW': 'j,M#?Q{BE<' + 'gp[h@]y13l': wxybJM,rY + 'gsS{jR,]|Q': 'I5Xpi}2t{j' + 'gx:;za!?C9': 'grAkl2|g`7' + 'gz9${p}er*': 'z2[w)b%6;V' + 'g~%pF`(x{u': 'A345Xn98~V1': 'uv4>|I(g|3' + 'h9Jr(H[=1~': 'u>1xt-=|mM' + 'h?a*UKfT_M': 'v3Q(-Lz6-w' + 'hE5j*e,Q?[': 'luR#M^0uhv' + 'hNE9}>;Q&-': JPnLHNC0x3 + 'hXNTVF<:SH': 'mf=T5@S@B$' + 'hZ/V/D&rO`': 'xJoB:Z/33$' + 'h_9=mkZXAH': 'HrvW&[P0V6' + hoHandbYAy: DE8jGf4.p_ + 'hrI_uiW>E]': 'y$a<]#=[kh' + 'h}lnSN_f$_': 'va7a~Jl^Dx' + 'h~5xz+=ke~': 'v%l&+z)c=t' + 'i)]+09JUR&': 'QtP;fVY}?{' + 'i2]v|D3@:j': 'X[]sE;=s@' + 'iB-8Br~onq': 'J:QDHRvbY^' + 'iIS#Nw]tr]': dHQ3bH/E/s + 'iR;Um(L/~c=': 'y!zI/vjcP(' + 'i^J8&$(1]|': 'nMZd2^,fLG' + 'ia8as9sCY|': 'uWMPk~=Q<>' + 'igiK,ff[eO': 'q.,&c}-x,G' + 'ijP>aJBT!9': 'OUD.,!OrcL' + 'ilGtw#=asM': 'qL#}r|5Mt`' + 'j%*8gN)q>d': x6sS60yxJ, + 'j)i)pB*HJ,': 'E&c?sL:0${' + 'j2IQ=f=w3@': 'smO=&Woj34' + j7eEZkzsCZ: 'v?p{P_T(=Y' + 'j9:K,~DQ2v': 'N5^^fmYEhO' + 'jg|}': 'M~CY8#w%,9' + 'jCd]`-=k,:': 'HS19wR2TJ{' + 'jDA-V?/hVj': 'cZjPi}s{>f' + jI9P-f6r3M: 's=~NbDpTw@' + 'jKIDq|}J&c': 'e(}%?$1n1Z' + 'jKO,9y0M;#': 'Pp{]{fO0V3' + 'jN^Vc%9OQ5': 'k0&Sa)F$sm' + 'jN|NAUP*h}': 'n#ItI:gZyE' + 'jTNoKo}Bu+': k1U3Xpt_Fz + 'jX=#G8wu#(': 'hL&<+w5j^&' + 'jYz[ibrr9m': 'm)CY|PH/iq' + 'jcu!gLw/&r': 'F`QTam?)B[' + 'jrM28*HbyG': 'b|sXd=z![^' + 'j|z@PMgdx,': 'Mv>trkGo{S' + 'k$1_yaF?9#': 'o>?99Fvx%/' + 'k/u1:B%DJH': 'L,t09Ht&6I' + 'k0{O[6l]bH': 'G?.zu_GD}*' + 'k1]FH+s8j@': kdIst1pWxd + 'k9E.p^Yhx5' + 'kS)hDm%w}R': 'r;&9#reAq>' + 'kX]hN=_.c-': P0cb3EIuka + 'kZ52zV#[Iv': 'hQ61{_HFBB' + 'k[)#L4.[v3': 'Jb!k3Zk952' + 'khB~(m^@Z5': 'p/zP}McYF(' + 'kkbYQXw5b;': 'K:KG8,Ch+~' + 'koI(}OyJ:@': 'v(zX&lYI;&' + 'kopwzzB``A': 'vTW7o0@Cbi' + 'k}4/oj#$~s': 'C}IA~L{U6@' + 'k}A9]O:0xw': 'k/--0=CQ24' + 'l$2O|9w`F~': 'kxy_[68Eh$' + 'l$zM~ihPFE': 'cK+qJN(Y0Z' + 'lI)Ah/E': 'K6KK5O7|e$' + 'l*j*EE:!Y?': 'o1s-Ke1!}|' + 'l+>Ki!j#v+': IvS7NktDMU + 'l3Ly8PBxt(': 'v%__gCc&;=' + 'lAxH.CfJB@': 'OIp9T`E.zP' + 'lH^2D]w;[H': 'I#a1r}a^ox' + 'lU@C3G/=c~s' + 'm2F|cUkKhv': 'gOMsqm#1v:' + 'm5j,b/yg%WMX2DQ' + 'mcc&0Kq^xE': 'DP|7nmfMGQ' + 'mi+kf0LTW.': 'qtG?nh5zIU' + 'mk1VltJl<]': 'Rgv*IZqSmF' + 'mk20`,N=`=': 'Fohes67)Oz' + 'mkgcU7[bA%': 'k~VOx&I}t{' + 'mq)8`hi{hb': wcmCaTm14V + 'mtd;>7T|g>': 'qzrPU{F1.#' + 'mz[>>AxIr4': 'mf?[qcKLQN' + 'm{noqE29%I': 'EsG3PP1%gq' + 'm{oM,jr>C#': 'N-VDo?Rx@P' + 'n&>e`2FO/S': 'qAd]|6O,7Z' + 'n+t>2`:;P/': 'iYgSvir%)V' + 'n:7&>YT5A=': iGsrHmA9Bc + 'nIfRn/yz': 'zgf^l#Fr_!' + 'oa87&PB&Bm': 'mOz:A8Cv+c' + 'onMKID2d;D': 'GrBHv<@H+7' + 'o~}KVA:VbI': 'C.u~aYFs^y' + 'p#R$-mG/L': 'qs1#;Ke~GW' + 'p.yv>BQ470': 'yA.vuGT<.b' + 'p:SPZ:e/*[': 'P]Er5fA%dB' + 'pDDrFhd%u': 'oC.ex]c%ci' + 'pYRw[=6;JI': 'KbHb&_S;-v' + 'p]t8:Tka7q': 'j$(6)I{3Mv' + pkj3RPBUrF: 'Iap>xuM=!p' + 'pwwZu{7f~(': 'tmUABORc%v' + 'pz(G*]A' + 'p}Qa|!7PpR': 'g}l,l&fs(:' + 'q1Q3Do8S>g': 'q@B#VXN;Jf' + 'q2#Yv[O0:|': '6uQ8zE+bN' + 'q7do-wLx%E': 'CTevYb-Dt]' + 'q8][)Ri}=q': 'so#V7SThWD' + 'q8fF$4,c6_': 'G~F&>Mc><4' + 'q:XyGf#>Wg': 'xW`%t:aC:q' + 'q=w[$qo8FD' + 'qFuGdDx^Yv': 'qObv%Y4Tz' + 'qVi;t%/`F|': 'z[&.Zoz9Wr' + 'q[S4`,F0D=': 'P6uJ_GAwG&' + 'q[}7hk(+=e': 'jnY+~6&jpG' + 'qiv/4tOlf=': 'm^=fB$d+$': 'baI-$$|wT': 'G1Q&LOe!Nv' + 'r>g<|,FWi%': 'd$r4uW9udC' + 'r?E$4f-d0/': 'hn&pw!0': 'LJ7~]dZ,pS' + 's9-GL*@AXD': 'd{V%m8RM^K' + 's<_WLSB3I/': 'M%2Z1/~Y[S' + 's>f40rn,O]': 'HM/uONn&+7' + 'sB7rZQsR@9w~i^p': 'Ecs;]/<;`%' + 'skOM(?n$GG': 'zn;(Mg+s.+' + spKoFRM4if: 'w6BvfhT&-R' + 'ssbyA9}]QP': '>v6oM{`%o' + 'stRa/S(0$F': 'Fh61;335zk' + 'sxOMs+bE8@': 'im}0hozT&<' + 'sx|l+io`B@': 'D]4yXjvC:9' + 's|K&2S,UP|': 'tmF}dZ1EH!' + 't$y1C2R-Av': JUjb1XwjuC + 't+v9b--,qs': 'BXXqj;W#6T' + 't-0>F]bu>P': 'dDaj)OD&@~' + 't2&ri|[Un:': 'O.$J.A)V?N' + t3aZXtpNyi: 'j&&6JU`wC`' + 't50y/[AEhF': 'cV/[QK2H^,' + 'tMMtBUpb$&': 'i!?4Hc^[WK' + 'tTnCoO/*tk': 'wmUbr/A' + 'tcneuK7v%`': 'y(>fAw?M7h' + tfCY4j6KEZ: mFQvXkOFXY + 'tj&bK}%P!F': cPwvsMSC1N + 'twrhzJ[k:Z': 'M4*OI7|Ubj' + 't|h3|;E4g{': 'qB3J]6{#j4' + 'u-`cspdp)D': 'pA0=t$_>hR' + 'u4^lH.xem5': 'D=o(;#f23a' + 'u;|vR#mVMW': 'fV5iO9=}H,' + 'uH*Q(mcRt5': 'x5_%[P=#b-' + 'uRo^w#p5Be': 'J:E?C=p8O;' + 'uc61ToT%gt': 'jxv@,7idOy' + 'ui3>b-?a@m': 'HU`]%dF5Op' + upiX0g1quR: 'c=InO0-mt?' + 'uv1fGXHN@)': 'uSPUaH0];!' + 'u|I}oElaH)': 'i!E{?dzJj_' + 'vE[T$`n9T)': g0yTLOPs_3 + 'vKx<0Qa!aP': 'p1z;2bU$}H' + 'vNiD6A.v#s': 'pEjiMe.U`M' + 'va/ky=f5pK': 'it>eGSc0;R' + 'vf)G?[_?Pf': 'sF=7^K-Xpq' + 'vgdj#X?8dB': 'Fi~Du[Cg`F' + 'vpOTK6tMr@': 'vp[7n&^y0?' + 'vppEA|$@~g': 'c.?kH`a?8B' + 'vrn%j%6{nu': 'M]!aUbbgQ)' + 'vwc3}.=Z#&': 'qRrQ1!Q_|1' + 'wa>{k!2cXc': 'bfrzr8~u|(' + x2d4n4ADcP: 'slIL&6*m)J' + 'xb&P*pH(Q': 'uju+7hizrf' + 'xqrJ^d[.(O': 'sx6g!v8gw>' + 'yX:tmrd5;]': 'vTvl[[S&ba' + 'z:*d{z(~V2': 'O,O]lf&18]' + 'zSc]-^U=0=': 'kQ{}[-(!z,' + crowdanki:uuid: + 43e2586a-9a65-11e8-a777-a0481cc15658: 15daaec7-4097-4d97-ba94-9db44f8b8f51 diff --git a/fixtures/ultimate-geography/overlays/languages/de.yaml b/fixtures/ultimate-geography/overlays/languages/de.yaml new file mode 100644 index 0000000..c6dfff5 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/de.yaml @@ -0,0 +1,747 @@ +id: overlay.translation.de +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': 'Zwischen den Anrainerstaaten des Meeres besteht ein Namensstreit, wobei Südkorea den Namen Ostmeer besonders unterstützt.' + Abkhazia: Abchasien + Addis Ababa: Addis Abeba + Adriatic Sea: Adriatisches Meer + Aegean Sea: 'Ägäisches Meer' + Africa: Afrika + Akrotiri and Dhekelia: Akrotiri und Dekelia + Albania: Albanien + Algeria: Algerien + Algiers: Algier + Also known as Burma.: Auch Birma oder Burma. + Also known as Cabo Verde.: Auch Cabo Verde. + Also known as Czechia.: '' + Also known as East Timor.: Auch Timor-Leste. + Also known as Kiev.: '' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: Auch Golf von Siam. + Also known as the Sea of Cortez.: 'Auch Cortés-See.' + 'Also spelled as Sana''a.': '' + American Samoa: Amerikanisch-Samoa + 'Andorra (narrower, coat of arms with motto)': 'Andorra (schmaler, Wappen mit Wahlspruch)' + Antarctica: Antarktis + Antigua and Barbuda: Antigua und Barbuda + Arabian Sea: Arabisches Meer + Aral Sea: Aralsee + Arctic Ocean: Arktischer Ozean + Argentina: Argentinien + Armenia: Armenien + Ashgabat: 'Aşgabat' + Asia: Asien + Athens: Athen + Atlantic Ocean: Atlantischer Ozean + Australia: Australien + 'Australia (white stars, two more stars)': 'Australien (weiße Sterne, zwei weitere Sterne)' + Austria: 'Österreich' + 'Austria (brighter red, wider white band)': 'Österreich (helleres Rot, breiteres weißes Band)' + Autonomous community of Spain.: Autonome Gemeinschaft Spaniens. + Autonomous province of South Korea.: 'Autonome Provinz Südkoreas.' + Autonomous region of Finland.: 'Auch Ålandinseln. Autonome Region Finnlands.' + Autonomous region of Italy.: Autonome Region Italiens. + Autonomous region of Papua New Guinea.: Autonome Region Papua-Neuguineas. + Autonomous region of Portugal.: Autonome Region Portugals. + Azerbaijan: Aserbaidschan + Azores: 'Azoren (Habichtsinseln)' + Baghdad: Bagdad + 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrain (enger, weniger Zacken, rot)' + Balkan Peninsula: Balkanhalbinsel + Baltic Sea: Ostsee + Banda Sea: Bandasee + Bangladesh: Bangladesch + Barents Sea: Barentssee + Bay of Bengal: Golf von Bengalen + Bay of Biscay: Biskaya + Beijing: Peking + Belgium: Belgien + Belgrade: Belgrad + Bering Strait: 'Beringstraße' + Bishkek: Bischkek + Black Sea: Schwarzes Meer + Bolivia: Bolivien + 'Bolivia (coat of arms instead of star)': 'Bolivien (Wappen anstatt Stern)' + Bosnia and Herzegovina: Bosnien und Herzegowina + Brazil: Brasilien + British Virgin Islands: Britische Jungferninseln + Brussels: 'Brüssel' + Bucharest: Bukarest + Bulgaria: Bulgarien + Cairo: Kairo + Cambodia: Kambodscha + Cameroon: Kamerun + 'Cameroon (green/red/yellow, yellow star)': 'Kamerun (Grün/Rot/Gelb, gelber Stern)' + Canada: Kanada + Canary Islands: Kanarische Inseln + Cape Verde: Kap Verde + Caribbean Sea: Karibisches Meer + Caspian Sea: Kaspisches Meer + Cayman Islands: 'Cayman Islands (Kaimaninseln)' + Celebes Sea: Celebessee + Celtic Sea: Keltische See + Central African Republic: Zentralafrikanische Republik + Cetinje is an honorary capital.: Cetinje ist eine Ehrenhauptstadt. + Chad: Tschad + 'Chad (slightly darker blue)': 'Tschad (etwas dunkleres Blau)' + China: Volksrepublik China + City of San Marino: Stadt San Marino + Claimed and controlled: Behauptet und kontrolliert + Claimed but not controlled: Behauptet, aber nicht kontrolliert + Colombia: Kolumbien + 'Colombia (no coat of arms)': 'Kolumbien (kein Wappen)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: Colombo wird oft als die Hauptstadt bezeichnet, aber Sri Jayawardenepura Kotte, ein Vorort von Colombo, ist die offizielle, legislative Hauptstadt. + Comoros: Komoren + Constituent country in the Kingdom of Denmark.: 'Autonomer Bestandteil des Königreichs Dänemark.' + Constituent country of the Kingdom of the Netherlands.: 'Autonomes Land innerhalb des Königreichs der Niederlande.' + Constituent country of the United Kingdom.: 'Landesteil des Vereinigten Königreichs.' + Cook Islands: Cookinseln + Copenhagen: Kopenhagen + Coral Sea: Korallenmeer + Corsica: Korsika + Croatia: Kroatien + Crown dependency of the United Kingdom.: Kronbesitz der britischen Krone. + Cuba: Kuba + 'Cuba (red triangle, blue stripes)': 'Kuba (rotes Dreieck, blaue Streifen)' + 'Curaçao (two stars in top-left corner)': 'Curaçao (zwei Sterne in der oberen linken Ecke)' + Cyprus: Republik Zypern + Czech Republic: Tschechien + Damascus: Damaskus + Dead Sea: Totes Meer + Democratic Republic of the Congo: Demokratische Republik Kongo + Denmark: 'Dänemark' + Denmark Strait: 'Dänemarkstraße' + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': 'Umstritten; von Israel beansprucht; Ramallah ist das Verwaltungszentrum.' + 'Disputed; claimed by Palestine.': 'Umstritten; von Palästina beansprucht.' + Djibouti: Dschibuti + Dominican Republic: Dominikanische Republik + Dushanbe: Duschanbe + East China Sea: Ostchinesisches Meer + East Siberian Sea: Ostsibirische See + 'Ecuador (with coat of arms)': 'Ecuador (mit Wappen)' + Egypt: 'Ägypten' + 'Egypt (emblem instead of text), Yemen (no text)': 'Ägypten (Emblem statt Text), Jemen (kein Text)' + 'Egypt (with emblem), Iraq (with text)': 'Ägypten (mit Emblem), Irak (mit Text)' + 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (anderes Wappen, etwas dunkleres Blau)' + English Channel: 'Ärmelkanal' + Equatorial Guinea: 'Äquatorialguinea' + Estonia: Estland + Ethiopia: 'Äthiopien' + Europe: Europa + European Union: 'Europäische Union' + Falkland Islands: Falklandinseln + Faroe Islands: 'Färöer' + Federated States of Micronesia: 'Föderierte Staaten von Mikronesien' + Fiji: Fidschi + Finland: Finnland + Formerly Zaire.: 'Früher Zaire.' + Formerly known as Macedonia.: 'Früher bekannt als Mazedonien.' + France: Frankreich + French Guiana: 'Französisch-Guayana' + French Polynesia: 'Französisch-Polynesien' + Gabon: Gabun + Georgia: Georgien + Germany: Deutschland + 'Ghana (star instead of coat of arms)': 'Ghana (Stern anstatt Wappen)' + Greece: Griechenland + Greenland: 'Grönland' + Guatemala City: Guatemala-Stadt + 'Guinea (green and red flipped, slightly darker green)': 'Guinea (Grün und Rot vertauscht, etwas dunkleres Grün)' + Gulf of Alaska: Golf von Alaska + Gulf of California: Golf von Kalifornien + Gulf of Carpentaria: Golf von Carpentaria + Gulf of Guinea: Golf von Guinea + Gulf of Mexico: Golf von Mexiko + Gulf of Thailand: Golf von Thailand + Hargeisa: Hargeysa + Havana: Havanna + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: 'Historische und kulturelle Region in Nordeuropa, die die Länder Dänemark, Norwegen und Schweden und manchmal Finnland und Island umfasst.' + Hong Kong: Hongkong + Hungary: Ungarn + Iceland: Island + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Island (blauer Hintergrund, rotes und weißes Kreuz), Norwegen (roter Hintergrund, blaues und weißes Kreuz)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Island (blauer Hintergrund, rotes Kreuz), Färöer (weißer Hintergrund, rotes und blaues Kreuz)' + Independent state claimed by Georgia.: 'Unabhängiger Staat, von Georgien beansprucht.' + Independent state claimed by Moldova.: 'Unabhängiger Staat, von Republik Moldau (Moldawien) beansprucht.' + Independent state claimed by Somalia.: 'Unabhängiger Staat, von Somalia beansprucht.' + India: Indien + Indian Ocean: Indischer Ozean + Indonesia: Indonesien + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesien (Weiß und Rot vertauscht, helleres Rot), Monaco (Weiß und Rot vertauscht, schmaler)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesien (breiter, helleres Rot), Polen (Rot und Weiß vertauscht, breiter)' + Iraq: Irak + 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (Text anstatt Emblem), Jemen (kein Emblem)' + Ireland: Irland + 'Ireland (orange and green flipped, wider)': 'Irland (Orange und Grün vertauscht, breiter)' + Island of Indonesia.: + notes.note.bali.fields.field.country-info: Insel von Indonesien. + notes.note.java.fields.field.country-info: Insel von Indonesien. + notes.note.sumatra.fields.field.country-info: Indonesische Insel. + Italy: Italien + Ivory Coast: 'Elfenbeinküste' + 'Ivory Coast (green and orange flipped, narrower)': 'Elfenbeinküste (Grün und Orange vertauscht, schmaler)' + Jamaica: Jamaika + Jeju: Jeju-do + Jordan: Jordanien + Kaliningrad Oblast: Oblast Kaliningrad + Kazakhstan: Kasachstan + Kenya: Kenia + Khartoum: Khartum + Known as Nur-Sultan between 2019 and 2022: Bekannt als Nur-Sultan zwischen 2019 und 2022 + Known as Swaziland until 2018.: 'Bis 2018: Swasiland.' + Kuwait City: Kuwait-Stadt + Kyiv: Kiew + Kyrgyzstan: Kirgisistan + Laayoune: 'El Aaiún' + Labrador Sea: Labradorsee + Latvia: Lettland + 'Latvia (darker red, narrower white band)': 'Lettland (dunkleres rot, dünneres weißes Band)' + Lebanon: Libanon + Libya: Libyen + Lisbon: Lissabon + Lithuania: Litauen + Luxembourg: Luxemburg + 'Luxembourg (lighter blue)': 'Luxemburg (helleres Blau)' + Luxembourg City: Luxemburg + Madagascar: Madagaskar + Madeira: Autonome Region Madeira + Maldives: Malediven + 'Mali (red and green flipped, slightly brighter green)': 'Mali (Rot und Grün vertauscht, etwas helleres Grün)' + Marshall Islands: Marshallinseln + Mauritania: Mauretanien + Mediterranean Sea: Mittelmeer + Melanesia: Melanesien + Mexico: Mexiko + Mexico City: Mexiko-Stadt + Micronesia: Mikronesien + Mogadishu: Mogadischu + Moldova: Republik Moldau + 'Moldova (wider, coat of arms with eagle)': 'Republik Moldau/Moldawien (breiter, Wappen mit Adler)' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (schmaler, dunkleres rot), Polen (Rot und Weiß vertauscht, dunkleres rot)' + Mongolia: Mongolei + Morocco: Marokko + Moscow: Moskau + Mozambique: Mosambik + Muscat: Maskat + 'N''Djamena': 'N’Djamena' + 'Nauru (single star below yellow band)': 'Nauru (einzelner Stern unter gelbem Band)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru hat keine offizielle Hauptstadt; der Yaren District ist der Regierungssitz.' + Netherlands: Niederlande + 'Netherlands (darker blue)': 'Niederlande (dunkleres Blau)' + New Caledonia: Neukaledonien + New Delhi: Neu-Delhi + New Zealand: Neuseeland + 'New Zealand (red stars, two fewer stars)': 'Neuseeland (rote Sterne, zwei Sterne weniger)' + 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (anderes Wappen, etwas helleres Blau)' + Nicosia: Nikosia + North America: Nordamerika + North Korea: Nordkorea + North Macedonia: Nordmazedonien + North Nicosia: Nord-Nikosia + North Sea: Nordsee + Northern Cyprus: Nordzypern + Northern Ireland: Nordirland + Northern Mariana Islands: 'Nördliche Marianen' + Norway: Norwegen + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norwegen (roter Hintergrund, blaues Kreuz), Färöer (weißer Hintergrund, rotes und blaues Kreuz)' + Norwegian Sea: 'Europäisches Nordmeer' + Not a sovereign country: 'Kein souveräner Staat' + 'Oblast (administrative region) of the Russian Federation.': 'Oblast (administratives Gebiet) der Russischen Föderation.' + Oceania: Ozeanien + Official capital was moved from Bujumbura to Gitega in 2019.: Die offizielle Hauptstadt wurde 2019 von Bujumbura nach Gitega verlegt. + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: Die offizielle Hauptstadt wurde 2026 von Malabo nach Ciudad de la Paz verlegt. + 'Officially Côte d''Ivoire.': 'Offiziell Côte d''Ivoire.' + Officially Luxembourg.: '' + Overseas department of France.: 'Übersee-Département Frankreichs.' + Overseas territory of France.: 'Französisches Überseegebiet.' + Overseas territory of the United Kingdom.: + notes.note.akrotiri-and-dhekelia.fields.field.country-info: 'Britisches Überseegebiet.' + notes.note.anguilla.fields.field.country-info: 'Britisches Überseegebiet.' + notes.note.bermuda.fields.field.country-info: 'Britisches Überseegebiet.' + notes.note.british-virgin-islands.fields.field.country-info: 'Britisches Überseegebiet.' + notes.note.cayman-islands.fields.field.country-info: 'Britisches Überseegebiet.' + notes.note.falkland-islands.fields.field.country-info: 'Britisches Überseegebiet.' + notes.note.gibraltar.fields.field.country-info: 'Britisches Überseegebiet.' + notes.note.turks-and-caicos-islands.fields.field.country-info: 'Auch Turks & Caicos. Britisches Überseegebiet.' + Pacific Ocean: Pazifischer Ozean + Palestine: 'Palästina' + 'Palestine (black/white/green, red arrow)': 'Palästina (Schwarz/Weiß/Grün, roter Pfeil)' + 'Palestine (no symbol)': 'Palästina (kein Symbol)' + Panama City: Panama-Stadt + Papua New Guinea: Papua-Neuguinea + Partially recognised state claimed by China.: Teilweise anerkannter Staat, von China beansprucht. + Partially recognised state claimed by Morocco. Also known as Western Sahara.: Teilweise anerkannter Staat, von Marokko beansprucht. + Partially recognised state claimed by Serbia.: Teilweise anerkannter Staat, von Serbien beansprucht. + Persian Gulf: Persischer Golf + Philippine Sea: Philippinensee + Philippines: Philippinen + Poland: Polen + Polynesia: Polynesien + Prague: Prag + Pretoria, Cape Town, Bloemfontein: Pretoria, Kapstadt, Bloemfontein + 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blaues Dreieck, rote Streifen)' + Pyongyang: 'Pjöngjang' + Qatar: Katar + 'Qatar (wider, more serrated edges, maroon)': 'Katar (breiter, mehr kleinere Zacken, rotbraun)' + Red Sea: Rotes Meer + Region of France.: 'Gebietskörperschaft Frankreichs.' + Republic of the Congo: Republik Kongo + Riyadh: Riad + Romania: 'Rumänien' + 'Romania (slightly lighter blue)': 'Rumänien (etwas helleres Blau)' + Rome: Rom + Russia: Russland + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Russland (kein Wappen), Slowenien (breiter, kleineres Wappen)' + Rwanda: Ruanda + 'Réunion': 'La Réunion' + Sahrawi Arab Democratic Republic: Demokratische Arabische Republik Sahara + Saint Kitts and Nevis: St. Kitts und Nevis + Saint Lucia: St. Lucia + Saint Martin: Saint-Martin + Saint Vincent and the Grenadines: St. Vincent und die Grenadinen + Santiago: Santiago de Chile + Sardinia: Sardinien + Saudi Arabia: Saudi-Arabien + Scandinavia: Skandinavien + Scotland: Schottland + Sea of Galilee: See Genezareth + Sea of Japan: Japanisches Meer + Sea of Okhotsk: Ochotskisches Meer + Semi-autonomous region of Tanzania.: Halbautonomer Teilstaat Tansanias. + 'Senegal (green/yellow/red, green star)': 'Senegal (Grün/Gelb/Rot, grüner Stern)' + Serbia: Serbien + Seychelles: Seychellen + Sicily: Sizilien + Singapore: Singapur + Slovakia: Slowakei + 'Slovakia (narrower, bigger coat of arms)': 'Slowakei (schmaler, größeres Wappen)' + 'Slovakia (with coat of arms)': 'Slowakei (mit Wappen)' + Slovenia: Slowenien + Solomon Islands: Salomonen + South Africa: 'Südafrika' + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'Die Regierung sitzt in Pretoria (Exekutive), das Parlament in Kapstadt (Legislative) und das Oberste Berufungsgericht in Bloemfontein (Judikative).' + South America: 'Südamerika' + South China Sea: 'Südchinesisches Meer' + South Korea: 'Südkorea' + South Ossetia: 'Südossetien' + South Sudan: 'Südsudan' + Southern Ocean: 'Südlicher Ozean' + Sovereign country: 'Souveräner Staat' + Spain: Spanien + Special Administrative Region of China.: Sonderverwaltungszone Chinas. + Sri Jayawardenepura Kotte: Sri Jayewardenepura Kotte + 'St. John''s': 'Saint John’s' + State of the United States.: Bundesstaat der Vereinigten Staaten. + State recognised only by Turkey and claimed by Cyprus.: 'Nur von der Türkei anerkannter Staat, von Zypern beansprucht.' + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Größte Inselregion Ozeaniens.' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Teilregion von Ozeanien bestehend über 2000 tropischen Inseln und Atollen im westlichen Pazifik.' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Teilregion von Ozeanien, zu der Vanuatu, die Salomonen, Fidschi und Papua-Neuguinea gehören.' + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (Rot/Weiß/Schwarz, grüner Pfeil), Demokratische Arabische Republik Sahara (mit Stern und Halbmond)' + Sukhumi: Sochumi + Svalbard: Spitzbergen + Sweden: Schweden + Switzerland: Schweiz + 'Switzerland has no official capital; Bern is the de facto capital.': 'Die Schweiz hat keine offizielle Hauptstadt; Bern ist der Regierungssitz.' + Syria: Syrien + 'São Tomé and Príncipe': 'São Tomé und Príncipe' + Taipei: Taipeh + Tajikistan: Tadschikistan + Tanzania: Tansania + Tashkent: Taschkent + Tasman Sea: Tasmansee + Tbilisi: Tiflis + Tehran: Teheran + The Bahamas: Bahamas + The Gambia: Gambia + Timor Sea: Timorsee + Timor-Leste: Osttimor + Tokyo: Tokio + Transnistria: Transnistrien + Trinidad and Tobago: Trinidad und Tobago + Tripoli: Tripolis + Tskhinvali: Zchinwali + Tunisia: Tunesien + Turkey: 'Türkei' + Turks and Caicos Islands: Turks- und Caicosinseln + Unincorporated internal area of Norway.: Gemeindefreies Gebiet Norwegens. + Unincorporated territory of the United States.: 'Nichtinkorporiertes US-amerikanisches Außengebiet.' + United Arab Emirates: Vereinigte Arabische Emirate + United Kingdom: 'Vereinigtes Königreich' + United States Virgin Islands: Amerikanische Jungferninseln + United States of America: Vereinigte Staaten von Amerika + Uzbekistan: Usbekistan + Vatican City: Vatikanstadt + Vienna: Wien + Wallis and Futuna: Wallis und Futuna + Warsaw: Warschau + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: 'Amsterdam ist die offizielle Hauptstadt, während Den Haag Parlaments- und Regierungssitz ist.' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: Dodoma ist die offizielle Hauptstadt, aber Dar es Salaam ist der Regierungssitz. + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': 'El Aaiún ist die erklärte Hauptstadt, aber Tifariti ist der Regierungssitz.' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: Mbabane ist die offizielle Haupstadt, aber Lobamba ist die traditionelle Haupstadt und Sitz des nationalen Parlaments. + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: Porto-Novo ist die offizielle Hauptstadt, aber Cotonou ist der Regierungssitz. + While Sucre is the constitutional capital, La Paz is the seat of government.: Sucre ist die offizielle Hauptstadt, aber La Paz ist der Regierungssitz. + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: Yamoussoukro ist die offizielle Hauptstadt, aber Abidjan ist der Regierungssitz. + White Sea: 'Weißes Meer' + World region covering the Australian continent and most of the islands in the Pacific Ocean.: Weltregion, die den australischen Kontinent und die meisten Inseln im Pazifischen Ozean umfasst. + Yellow Sea: Gelbes Meer + Yemen: Jemen + Yerevan: Jerewan + Zambia: Sambia + Zanzibar: Sansibar + Zimbabwe: Simbabwe + 'Åland Islands': 'Åland' + additions: + notes.note.aegean-sea.fields.field.country-info: 'Auch Ägäis.' + notes.note.arctic-ocean.fields.field.country-info: 'Auch Nordpolarmeer, nördliches Eismeer, Arktische See oder Arktik.' + notes.note.atlantic-ocean.fields.field.country-info: Auch Atlantik. + notes.note.balkan-peninsula.fields.field.country-info: Auch Balkan. + notes.note.baltic-sea.fields.field.country-info: Auch Baltisches Meer. + notes.note.bay-of-biscay.fields.field.country-info: Auch Golf von Biskaya. + notes.note.belarus.fields.field.country-info: 'Auch Weißrussland.' + notes.note.burkina-faso.fields.field.capital-info: Auch Wagadugu. + notes.note.caspian-sea.fields.field.country-info: Auch Kaspisee. + notes.note.celebes-sea.fields.field.country-info: Auch Sulawesisee. + notes.note.moldova.fields.field.country-info: Auch Moldawien. + notes.note.norwegian-sea.fields.field.country-info: Auch Norwegische See oder Norwegisches Meer. + notes.note.pacific-ocean.fields.field.country-info: Auch Pazifik. + notes.note.persian-gulf.fields.field.country-info: Auch Arabischer Golf. + notes.note.solomon-islands.fields.field.country-info: Auch Salomoninseln. + notes.note.suriname.fields.field.country-info: Auch Surinam. + notes.note.white-sea.fields.field.country-info: 'Auch Weißmeer.' + variables: + label.capital: + Capital: Hauptstadt + label.capital-hint.answer: + 'Hint: {{Capital hint}}': 'Hinweis: {{Capital hint}}' + label.flag: + Flag: Flagge + label.location: + Location: Lage + note-type.name: + Ultimate Geography: 'Ultimate Geography [DE]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': 'Flagge ähnlich wie {{Flag similarity}}.' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': '9|1g!uc+D' + '5JJ2]i)p4': 'cYpd' + 'Gn;q2Y)Xe4': '{Zx2bRpQQu' + 'H*c[ML+`s=': '|qOD;Er>4Q' + 'He!e=h?_2V': '|QjQzaB=bl' + 'J%,fuysl*&': '~msR6r4eqE' + 'JOT:U4ayh@': '~=]w^XMrTT' + 'K*b}*d$}MA/h' + 'Lol+XIvY21': 'b0Xr{B7Rbr' + 'M$bc0[jYf0': 'clNO~:VRRq' + 'O~pXkAbd3': 'HL+nW$NPW' + 'QSq%~rFaP-': 'g[2mLk*^>K' + 'Qlib4)q2Hk': 'gXUNd92V,%' + 'SlOEXfq#|': 'LXe_{R2k@' + 'W5?6yQZD2': 'PeSw!?}(V' + '_]DH+g9!$': '=E^|rSij5' + 'b@M[XTc?}5': '4C;D{MO.Kv' + 'bD?R}i8_[c': '4(B@Kbh=D5' + 'bW)ch:vs1P': '4`pOT)7laf' + 'b[(2`4t&?J': '4DobHX57B~' + 'b^Try&me1A': '4F]3!7Y|a@' + 'b`5Q_Z=HQT': 'o9MT>Zr^kl' + 'bd~Ght5tLR': '4PL+Tmem:h' + 'bk2ByZIeU+': '4Wb%!S-|^I' + 'bnEK#)[;qT': '4Z)/k9D*2j' + 'c(,2{yXI#E': '5osbIr{Bk_' + 'c,5(mUGzYT': '5seoYN+s|j' + 'cAlyc~G.y%': '5$X!O]+&!D' + 'cEO^y)Md[G': '5)=F!9;{D{' + 'cL?8!xP#Wj': '5:Bhjq>4`$' + 'cPO|Cru8n<': '5>=J&k61ZP' + 'cV4idO^r8W': 5_dUPHFkhm + 'cYKvF#ptF7': '5|/7*41m*x' + 'c^,~EC6Pb2': '5FsL)vfINs' + 'ck!6;xK$/^': 5Wjfxq/5vW + 'cv+Nq9:af': '7u5l_be$A8' + 'e42BpV,P}C': '7db%1OsIK]' + 'e5P*D#I{m{': '7e>q(4-?YZ' + 'eHH?U': 7175c-,ABk + 'euW0jm=wfR': '76`~VfzpRh' + 'e{e8vi@^PY': '7IQh7bC<>o' + 'f#4-L[mp5*': '8kdt::YieH' + 'f*2@qiHh_1': 8qbC2b,aGr + 'f*LO~>!0PN': '8q:=L-jT>d' + 'f21=-A~W;~': 8bazttLPx2 + 'f>c7x2E]Q6': '8AOg9V);?w' + 'fRSM{K+;[w': '8@[;IDr*D<' + 'fT!w8R#>dg': '8]j8hKk-P9' + 'fV`&p81%d(': 8_Hn11a6PF + fkq7lhLkdH: 'b,nf]GF[[?' + 'fxAN),FAc$': '89$co2[B' + 'gFOojQO(MH': '9*=0VJ=8;|' + 'gG6SWfM>H0': '9+f[`};-,q' + 'gRVZ]qJ#>O': '9@_}Ej.4Ae' + 'gYIoR`|AxW': '9|-0@>Jt9m' + 'gePC:Xt_nW': '9Q>&wQ5=Zm' + 'gp[h@]y13l': '91DTC;!Uc&' + 'gsS{jR,]|Q': '94[IVKs;Jg' + 'gx:;za!?C9': '99wx#^j.&z' + 'gz9${p}er*': '9#ilIiK|3H' + 'g~%pF`(x{u': '9Lm1*>oqI:' + 'h&`>n98~V1': '!nHAZ2h]_r' + 'h9Jr(H[=1~': 'hrp$LcsG^>' + 'h?a*UKfT_M': '!BMq^DRMGc' + 'hE5j*e,Q?[': '!)eVq|sJBU' + 'hNE9}>;Q&-': '!<)iK-xJnK' + 'hXNTVF<:SH': '!{<]_yy)[|' + 'hZ/V/D&rO`': '!}v_vwnk=Y' + 'h_9=mkZXAH': '!GizYd}Q$|' + hoHandbYAy: '!0,MZ{NR$>' + 'hrI_uiW>E]': '!3-G6b`-)V' + 'h}lnSN_f$_': '!KXZ[GG}lX' + 'h~5xz+=ke~': '!Le9##zdQ2' + 'i)]+09JUR&': '#pEr~2.N@E' + 'i2]v|D3@:j': '#bE7Jwc/w$' + 'iB-8Br~onq': '#%th%kLhZ,' + 'iIS#Nw]tr]': '#-[km(L/~c=': '#`AYoEv]OQ' + 'i^J8&$(1]|': '#F.hn5oUE0' + 'ia8as9sCY|': '#MhM424v|0' + 'igiK,ff[eO': 'MO*aWaoK4b' + 'ijP>aJBT!9': '#V>AMC%Mjz' + 'ilGtw#=asM': '#X+584z^4c' + 'j%*8gN)q>d': '$mqhSGpjA6' + 'j)i)pB*HJ,': '$pUp1uqA.J' + 'j2IQ=f=w3@': '$b-?z}zpcT' + j7eEZkzsCZ: '$gQ)}d#l&p' + 'j9:K,~DQ2v': '$iw/s](Jb;' + 'jg|}': '$%PC$.A~J1' + 'jCd]`-=k,:': '$&PEH%zdsN' + 'jDA-V?/hVj': '$($t_.va_$' + jI9P-f6r3M: '$-i>t}fkcc' + 'jKIDq|}J&c': '$/-(2@KCn5' + 'jKO,9y0M;#': '$/=sir~FxB' + 'jN^Vc%9OQ5': '$!T1' + 'jTNoKo}Bu+': '$]<0/hKu6I' + 'jX=#G8wu#(': '${zk+18nkF' + 'jYz[ibrr9m': '$|#DU_3ki(' + 'jcu!gLw/&r': '$O6jSE8(n-' + 'jrM28*HbyG': '$3;bh!,_!{' + 'j|z@PMgdx,': '$J#C>FS{9J' + 'k$1_yaF?9#': '%laG!^*.iB' + 'k/u1:B%DJH': '%v6awumw.|' + 'k0{O[6l]bH': '%~I=DZX;N|' + 'k1]FH+s8j@': '%aE*,#41VT' + 'k9E.p^*2' + 'l$zM~ihPFE': '&l#;LbTI*_' + 'lI)Ah/E': '&nkg-9$av_' + 'l*j*EE:!Y?': '&qVq)xw3|S' + 'l+>Ki!j#v+': '&rA/U3V47I' + 'l3Ly8PBxt(': '&c:!hI%q5F' + 'lAxH.CfJB@': '&$9,uvRC%T' + 'lH^2D]w;[H': '&,Fb(;8*D|' + 'lU@C3,E' + 'm*#%sE.`.;': '(qkm4xu>uO' + 'm*PYE=#wr^': '(q>|),kp3W' + 'm2F|cUkKhv': '(b*JONWDT;' + 'm5j,b/yyuK' + 'm[96fa+HDS': '(DifR^rA(i' + mbGXc0y3Pe: '(N+{OT!W>7' + 'mbz=7n[0v9': '(N#zggDT7z' + 'mcc&0Kq^xE': '(OOn~D2<9_' + 'mi+kf0LTW.': '(UrWRT:M`L' + 'mk1VltJl<]': '(Wa_Xm.eyV' + 'mk20`,N=`=': '(Wb~H$<,HQ' + 'mkgcU7[bA%': '(WSO^0D_$D' + 'mq)8`hi{hb': '(2phHaU?T4' + 'mtd;>7T|g>': '(5PxA0]@SR' + 'mz[>>AxIr4': '(#DAAt9B3u' + 'm{noqE29%I': '(IZ02xb2m}' + 'm{oM,jr>C#': '(I0;sc3-&B' + 'n&>e`2FO/S': ')nAQHV*Hvi' + 'n+t>2`:;P/': ')r5Ab>w*>M' + 'n:7&>YT5A=': ')wgnAR]Y$Q' + 'nIfRn/yz': '*HHwBTA(!?' + 'oa87&PB&Bm': '*MhgnI%7%(' + 'onMKID2d;D': '*Z;/-wb{x^' + 'o~}KVA:VbI': '*LK/_twON}' + 'p#R$-mG/L': 'ikhCtY+vE' + 'p.yv>BQ470': '+u!7Au?Xgq' + 'p:SPZ:e/*[': '+w[>})Q(qU' + 'pDP5$~C~' + 'pQpX%(EVOH': '+?1{m8)O=|' + 'pS>DrFhd%u': '+[A(3yT{m:' + 'pYRw[=6;JI': 'tk,=<`H?)!' + 'p]t8:Tka7q': '+E5hwMW^g,' + pkj3RPBUrF: '+WVc@I%N3`' + 'pwwZu{7f~(': '+88}6?g}LF' + 'pz(G*g': ',a?c(hhLA9' + 'q2#Yv[O0:|': ',bk|7:=Tw0' + 'q7do-wLx%E': ',gP0tp:qm_' + 'q8][)Ri}=q': ',hEDpKU[z,' + 'q8fF$4,c6_': ',hR*lXs`fX' + 'q:XyGf#>Wg': ',w{!+}k-`9' + 'q=w[*0' + 'q[S4`,F0D=': ',D[dH$*T(Q' + 'q[}7hk(+=e': ',DKgTdo#z7' + 'qiv/4tOlf=': ',U7vdm=eRQ' + qvITjNxkLN: ',7-]VG9d:d' + 'qx%>fB$d+$': ',9mARul{rC' + 'r(h9&#wc~B': '-oTin48`L[' + 'r/B|ra%L,.': '-v%J3^mEsL' + 'r5C646H*+*': '-e&fdZ,!rH' + 'r6BV%|..*5': '-f%_m@u&qv' + 'raI-$$|wT': '-AM-t5l@8j' + 'r>g<|,FWi%': '-ASyJ$*PUD' + 'r?E$4f-d0/': '-B)ld}t{~M' + 'rPekx@n9:L': '->QW9/Z2wb' + 'rSneU!3}bn': '-[ZQ^3c[N)' + 'r]*Ioa%$bK': '-Eq-0^m5Na' + 's#:aIK$9^x': '.kwM-Dl2F=' + 's4E?ZF[E<>': '.d)B}yDxyR' + 's9-GL*@AXD': '.it+:!Ct{^' + 's<_WLSB3I/': '.yG`:L%W-M' + 's>f40rn,O]': 'x<|U~!Rw@;' + 'sB7rZQsRmaA[r' + 'sRw-ik:I$v': '.@8tUdwBl;' + 'sSW/?WMy17': '.[`vBP;rax' + 'sT5=H|@KU|': '.]ez,@CD^0' + sXzvT.g31p: '.{#7]&SWa+' + 'sY>@9w~i^p': '.|ACipLbF+' + 'skOM(?n$GG': '.W=;o.Z5+{' + spKoFRM4if: '.1/0*K;XU8' + 'ssbyA9}]QP': 'wTi%T' + 's|K&2S,UP|': '.J/nbLsN>0' + 't$y1C2R-Av': '/l!a&V@%$;' + 't+v9b--,qs': '/r7iN%t$2.' + 't-0>F]bu>P': '/t~A*;NnAf' + 't2&ri|[Un:': '/bn3U@DNZN' + t3aZXtpNyi: '/cM}{m1G!#' + 't50y/[AEhF': '/e~!v:$xT`' + 'tMMtBUpb$&': '/;;5%N1_lE' + 'tTnCoO/*tk': '/]Z&0Hv!5%' + 't_h*fbN#:(': '/GTqR_<4wF' + 't`cagaWApo': '/HOMS^`t1*' + 'tcneuK7v%`': /OZQ6DgomY + tfCY4j6KEZ: 'Du}tm|Ku-b' + 'tj&bK}%P!F': '/VnN/[mIj`' + 'twrhzJ[k:Z': '/83T#CDdwp' + 't|h3|;E4g{': '/JTcJ*)XSZ' + 'u-`cspdp)D': ':tHO4iPip^' + 'u4^lH.xem5': ':dFX,&9|Yv' + 'u;|vR#mVMW': ':xJ7@4YO;m' + 'uH*Q(mcRt5': ':,q?ofOK5v' + 'uRo^w#p5Be': ':@0F841Y%7' + 'uc61ToT%gt': ':Ofa]h]6S/' + 'ui3>b-?a@m': ':UcAN%B^C(' + upiX0g1quR: ':1U{~~aj6h' + 'uv1fGXHN@)': ':7aR+Q,GCG' + 'u|I}oElaH)': ':J-K0xX^,G' + 'vE[T$`n9T)': ';)D]l>Z2]G' + 'vKx<0Qa!aP': 'o*$5]rH:r[' + 'vNiD6A.v#s': ';8' + 'vgdj#X?8dB': ';SPVkQB1P[' + 'vpOTK6tMr@': ';1=]/Z5F3T' + 'vppEA|$@~g': 'OD{?KH{=z3' + 'vrn%j%6{nu': ';3ZmV6f?Z:' + 'vwc3}.=Z#&': ';8OcK&zSkE' + 'wa>{k!2cXc': NnxZS,Tdmh + x2d4n4ADcP: '=bPdZX$wOf' + 'xb&P*pH(Q': qNEfq1,oJ + 'xqrJ^d[.(O': '=23.F{D&oe' + 'yX:tmrd5;]': '>{w5YkPYxV' + 'z:*d{z(~V2': '?wqPIso]_s' + 'zSc]-^U=0=': '?[OEt<^,~Q' + crowdanki:uuid: + 43e2586a-9a65-11e8-a777-a0481cc15658: cb3de16f-7c9c-4944-994a-e17ec2018c28 diff --git a/fixtures/ultimate-geography/overlays/languages/es.yaml b/fixtures/ultimate-geography/overlays/languages/es.yaml new file mode 100644 index 0000000..6e7ae61 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/es.yaml @@ -0,0 +1,832 @@ +id: overlay.translation.es +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': 'Existe una disputa sobre el nombre entre los países que lo rodean, con Corea del Sur apoyando notablemente el nombre de mar del Este.' + Abkhazia: Abjasia + Abu Dhabi: Abu Dabi + Abuja: Abuya + Accra: Acra + Addis Ababa: 'Adís Abeba' + Adriatic Sea: 'Mar Adriático' + Aegean Sea: Mar Egeo + Afghanistan: 'Afganistán' + Africa: 'África' + Akrotiri and Dhekelia: Acrotiri y Dhekelia + Algeria: Argelia + Algiers: Argel + Also known as Burma.: 'También conocida como Myanmar.' + Also known as Cabo Verde.: '' + Also known as Czechia.: 'También conocida como Chequia.' + Also known as East Timor.: '' + Also known as Kiev.: '' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: 'También conocido como golfo de Siam.' + Also known as the Sea of Cortez.: 'También conocido como mar de Cortés.' + 'Also spelled as Sana''a.': '' + American Samoa: Samoa Americana + Amman: 'Amán' + Amsterdam: 'Ámsterdam' + 'Andorra (narrower, coat of arms with motto)': 'Andorra (más estrecha, escudo con lema)' + Andorra la Vella: Andorra la Vieja + Anguilla: Anguila + Antarctica: 'Antártida' + Antigua and Barbuda: Antigua y Barbuda + Arabian Sea: 'Mar arábigo' + Aral Sea: Mar de Aral + Arctic Ocean: 'Océano Ártico' + Ashgabat: Asjabad + Astana: 'Astaná' + Athens: Atenas + Atlantic Ocean: 'Océano Atlántico' + 'Australia (white stars, two more stars)': 'Australia (estrellas blancas, dos estrellas más)' + 'Austria (brighter red, wider white band)': 'Austria (rojo más vivo, banda blanca más ancha)' + Autonomous community of Spain.: 'Comunidad autónoma de España.' + Autonomous province of South Korea.: 'Provincia autónoma de Corea del Sur.' + Autonomous region of Finland.: 'Región autónoma de Finlandia.' + Autonomous region of Italy.: 'Región autónoma de Italia.' + Autonomous region of Papua New Guinea.: 'Región autónoma de Papúa Nueva Guinea.' + Autonomous region of Portugal.: 'Región autónoma de Portugal.' + Azerbaijan: 'Azerbaiyán' + Baghdad: Bagdad + Bahrain: 'Baréin' + 'Bahrain (narrower, fewer serrated edges, red)': 'Baréin (más estrecha, menos dientes de sierra, roja)' + Baku: 'Bakú' + Balkan Peninsula: 'Península balcánica' + Baltic Sea: 'Mar Báltico' + Banda Sea: Mar de Banda + Bangladesh: 'Bangladés' + Barents Sea: Mar de Barents + Bay of Bengal: 'Bahía de Bengala' + Bay of Biscay: Golfo de Vizcaya + Beijing: 'Pekín' + Belarus: Bielorrusia + Belgium: 'Bélgica' + Belgrade: Belgrado + Belize: Belice + Belmopan: 'Belmopán' + Benin: 'Benín' + Bering Strait: Estrecho de Bering + Berlin: 'Berlín' + Bermuda: Bermudas + Bern: Berna + Bhutan: 'Bután' + Bishkek: Biskek + Bissau: 'Bisáu' + Black Sea: Mar Negro + 'Bolivia (coat of arms instead of star)': 'Bolivia (escudo en lugar de la estrella)' + Bosnia and Herzegovina: Bosnia y Herzegovina + Botswana: Botsuana + 'Brasília': Brasilia + Brazil: Brasil + British Virgin Islands: 'Islas Vírgenes Británicas' + Brunei: 'Brunéi' + Brussels: Bruselas + Bucharest: Bucarest + Cairo: El Cairo + Cambodia: Camboya + Cameroon: 'Camerún' + 'Cameroon (green/red/yellow, yellow star)': 'Camerún (verde/rojo/amarillo, estrella amarilla)' + Canada: 'Canadá' + Canary Islands: Canarias + Cape Verde: Cabo Verde + Caribbean Sea: Mar Caribe + Caspian Sea: Mar Caspio + Cayman Islands: 'Islas Caimán' + Celebes Sea: 'Mar de Célebes' + Celtic Sea: 'Mar Céltico' + Central African Republic: 'República Centroafricana' + Cetinje is an honorary capital.: 'Cetiña es una capital honoraria.' + 'Chad (slightly darker blue)': 'Chad (azul ligeramente más oscuro)' + 'Chișinău': 'Chisináu' + City of San Marino: San Marino + Claimed and controlled: Reclamada y controlada + Claimed but not controlled: Reclamada pero no controlada + 'Colombia (no coat of arms)': 'Colombia (sin escudo)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: Colombo es a menudo referida como la capital, pero Sri Jayawardenapura Kotte, una ciudad a las afueras de Colombo, es la capital oficial y legislativa. + Comoros: Comoras + Conakry: Conakri + Constituent country in the Kingdom of Denmark.: 'Nación constitutiva del Reino de Dinamarca.' + Constituent country of the Kingdom of the Netherlands.: 'Nación constitutiva del Reino de los Países Bajos.' + Constituent country of the United Kingdom.: 'Nación constitutiva del Reino Unido.' + Cook Islands: Islas Cook + Copenhagen: Copenhague + Coral Sea: Mar del Coral + Corsica: 'Córcega' + Croatia: Croacia + Crown dependency of the United Kingdom.: 'Dependencia de la Corona británica.' + 'Cuba (red triangle, blue stripes)': 'Cuba (triángulo rojo, bandas azules)' + 'Curaçao': Curazao + 'Curaçao (two stars in top-left corner)': 'Curazao (dos estrellas en la esquina de arriba a la izquierda)' + Cyprus: Chipre + Czech Republic: 'República Checa' + Damascus: Damasco + Dead Sea: Mar Muerto + Democratic Republic of the Congo: 'República Democrática del Congo' + Denmark: Dinamarca + Denmark Strait: Estrecho de Dinamarca + Dhaka: Daca + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': 'Disputada; reclamada por Israel; Ramala es el centro administrativo.' + 'Disputed; claimed by Palestine.': 'Disputada; reclamada por Palestina.' + Djibouti: Yibuti + Dominican Republic: 'República Dominicana' + Dublin: 'Dublín' + Dushanbe: 'Dusambé' + East China Sea: Mar de la China Oriental + East Siberian Sea: Mar de Siberia Oriental + 'Ecuador (with coat of arms)': 'Ecuador (con escudo)' + Edinburgh: Edimburgo + Egypt: Egipto + 'Egypt (emblem instead of text), Yemen (no text)': 'Egipto (escudo en lugar del texto), Yemen (sin texto)' + 'Egypt (with emblem), Iraq (with text)': 'Egipto (con escudo), Irak (con texto)' + 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (escudo diferente, azul ligeramente más oscuro)' + England: Inglaterra + English Channel: Canal de la Mancha + Equatorial Guinea: Guinea Ecuatorial + Eswatini: Suazilandia + Ethiopia: 'Etiopía' + Europe: Europa + European Union: 'Unión Europea' + Falkland Islands: Islas Malvinas + Faroe Islands: Islas Feroe + Federated States of Micronesia: Estados Federados de Micronesia + Fiji: Fiyi + Finland: Finlandia + Formerly Zaire.: Anteriormente Zaire. + Formerly known as Macedonia.: Anteriormente conocida como Macedonia. + France: Francia + French Guiana: Guayana Francesa + French Polynesia: Polinesia Francesa + Gabon: 'Gabón' + Germany: Alemania + 'Ghana (star instead of coat of arms)': 'Ghana (estrella en lugar del escudo)' + Gitega: Guitega + Greece: Grecia + Greenland: Groenlandia + Grenada: Granada + Guadeloupe: Guadalupe + Guatemala City: Ciudad de Guatemala + 'Guinea (green and red flipped, slightly darker green)': 'Guinea (verde y rojo intercambiados, verde ligeramente más oscuro)' + Guinea-Bissau: 'Guinea-Bisáu' + Gulf of Alaska: Golfo de Alaska + Gulf of California: Golfo de California + Gulf of Carpentaria: Golfo de Carpentaria + Gulf of Guinea: Golfo de Guinea + Gulf of Mexico: 'Golfo de México' + Gulf of Thailand: Golfo de Tailandia + 'Hagåtña': 'Agaña' + Haiti: 'Haití' + Hanoi: 'Hanói' + Havana: La Habana + Hawaii: 'Hawái' + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: 'Región histórica y cultural del norte de la Europa Septentrional, que incluye los países de Dinamarca, Noruega y Suecia, y a veces Islandia y Finlandia.' + Hudson Bay: 'Bahía de Hudson' + Hungary: 'Hungría' + Iceland: Islandia + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Islandia (fondo azul, cruz roja y blanca), Noruega (fondo rojo, cruz azul y blanca)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Islandia (fondo azul, cruz roja), Islas Feroe (fondo blanco, cruz roja y azul)' + Independent state claimed by Georgia.: Estado independiente reclamado por Georgia. + Independent state claimed by Moldova.: Estado independiente reclamado por Moldavia. + Independent state claimed by Somalia.: Estado independiente reclamado por Somalia. + Indian Ocean: 'Océano Índico' + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesia (rojo y blanco intercambiados, rojo más vivo), Mónaco (rojo y blanco intercambiados, más estrecha)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesia (más ancha, rojo más vivo), Polonia (rojo y blanco intercambiados, más ancha)' + Iran: 'Irán' + Iraq: Irak + 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (texto en lugar de escudo), Yemen (sin escudo)' + Ireland: Irlanda + 'Ireland (orange and green flipped, wider)': 'Irlanda (naranja y verde intercambiados, más ancha)' + Island of Indonesia.: Isla de Indonesia. + Isle of Man: Isla de Man + Italy: Italia + Ivory Coast: Costa de Marfil + 'Ivory Coast (green and orange flipped, narrower)': 'Costa de Marfil (verde y naranja intercambiados, más estrecha)' + Jakarta: Yakarta + Japan: 'Japón' + Jerusalem: 'Jerusalén' + Jordan: Jordania + Juba: Yuba + Kaliningrad Oblast: 'Óblast de Kaliningrado' + Kathmandu: 'Katmandú' + Kazakhstan: 'Kazajistán' + Kenya: Kenia + Khartoum: Jartum + Kinshasa: Kinsasa + Known as Nur-Sultan between 2019 and 2022: 'Conocida como Nursultán entre 2019 y 2022' + Known as Swaziland until 2018.: Oficialmente Esuatini desde 2018. + Kuwait City: Kuwait + Kyiv: Kiev + Kyrgyzstan: 'Kirguistán' + Laayoune: 'El Aaiún' + Labrador Sea: Mar de Labrador + Latvia: Letonia + 'Latvia (darker red, narrower white band)': 'Letonia (rojo más oscuro, banda blanca más estrecha)' + Lebanon: 'Líbano' + Lesotho: Lesoto + Libya: Libia + Lilongwe: 'Lilongüe' + Lisbon: Lisboa + Lithuania: Lituania + Ljubljana: Liubliana + London: Londres + Luxembourg: Luxemburgo + 'Luxembourg (lighter blue)': 'Luxemburgo (azul más claro)' + Luxembourg City: Luxemburgo + Macau: Macao + Malawi: Malaui + Malaysia: Malasia + Maldives: Maldivas + Mali: 'Malí' + 'Mali (red and green flipped, slightly brighter green)': 'Malí (rojo y verde intercambiados, verde ligeramente más vivo)' + Marshall Islands: Islas Marshall + Martinique: Martinica + Mauritius: Mauricio + Mediterranean Sea: 'Mar Mediterráneo' + Mexico: 'México' + Mexico City: 'Ciudad de México' + Mogadishu: Mogadiscio + Moldova: Moldavia + 'Moldova (wider, coat of arms with eagle)': 'Moldavia (más ancha, escudo con águila)' + Monaco: 'Mónaco' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Mónaco (más estrecha, rojo más oscuro), Polonia (rojo y blanco intercambiados, rojo más oscuro)' + Morocco: Marruecos + Moscow: 'Moscú' + Muscat: Mascate + Myanmar: Birmania + 'N''Djamena': Yamena + Nassau: 'Nasáu' + 'Nauru (single star below yellow band)': 'Nauru (una sola estrella debajo de la banda amarilla)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru no tiene capital oficial; el distrito de Yaren es la capital de facto.' + Naypyidaw: 'Naipyidó' + Netherlands: 'Países Bajos' + 'Netherlands (darker blue)': 'Países Bajos (azul más oscuro)' + New Caledonia: Nueva Caledonia + New Delhi: Nueva Delhi + New Zealand: Nueva Zelanda + 'New Zealand (red stars, two fewer stars)': 'Nueva Zelanda (estrellas rojas, dos estrellas menos)' + 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (escudo diferente, azul ligeramente más claro)' + Niger: 'Níger' + North America: 'América del Norte' + North Korea: Corea del Norte + North Macedonia: Macedonia del Norte + North Nicosia: Nicosia del Norte + North Sea: Mar del Norte + Northern Cyprus: Chipre del Norte + Northern Ireland: Irlanda del Norte + Northern Mariana Islands: Islas Marianas del Norte + Norway: Noruega + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Noruega (fondo rojo, cruz azul), Islas Feroe (fondo blanco, cruz roja y azul)' + Norwegian Sea: Mar de Noruega + Not a sovereign country: 'No es un país soberano' + Nouakchott: Nuakchot + 'Nouméa': Numea + 'Nukuʻalofa': Nukualofa + 'Oblast (administrative region) of the Russian Federation.': 'Óblast (región administrativa) de la Federación de Rusia.' + Oceania: 'Oceanía' + Official capital was moved from Bujumbura to Gitega in 2019.: 'La capital oficial se cambió de Buyumbura a Guitega en 2019.' + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: 'La capital oficial se cambió de Malabo a Ciudad de la Paz en 2026.' + 'Officially Côte d''Ivoire.': 'Oficialmente Côte d''Ivoire.' + Officially Luxembourg.: '' + Oman: 'Omán' + Ouagadougou: 'Uagadugú' + Overseas department of France.: Departamento de ultramar de Francia. + Overseas territory of France.: Territorio de ultramar de Francia. + Overseas territory of the United Kingdom.: Territorio de ultramar del Reino Unido. + Pacific Ocean: 'Océano Pacífico' + Pakistan: 'Pakistán' + Palau: Palaos + Palestine: Palestina + 'Palestine (black/white/green, red arrow)': 'Palestina (negro/blanco/verde, triángulo rojo)' + 'Palestine (no symbol)': 'Palestina (sin símbolo)' + Panama: 'Panamá' + Panama City: 'Ciudad de Panamá' + Papua New Guinea: 'Papúa Nueva Guinea' + Paris: 'París' + Partially recognised state claimed by China.: Estado parcialmente reconocido reclamado por China. + Partially recognised state claimed by Morocco. Also known as Western Sahara.: Estado parcialmente reconocido reclamado por Marruecos. + Partially recognised state claimed by Serbia.: Estado parcialmente reconocido reclamado por Serbia. + Persian Gulf: 'Golfo Pérsico' + Peru: 'Perú' + Philippine Sea: Mar de Filipinas + Philippines: Filipinas + Phnom Penh: Nom Pen + Poland: Polonia + Polynesia: Polinesia + Port Moresby: Puerto Moresby + Port of Spain: 'Puerto España' + Port-au-Prince: 'Puerto Príncipe' + Porto-Novo: Porto Novo + Prague: Praga + Pretoria, Cape Town, Bloemfontein: Pretoria, Ciudad del Cabo, Bloemfontein + 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (triángulo azul, bandas rojas)' + Pyongyang: Pionyang + Qatar: Catar + 'Qatar (wider, more serrated edges, maroon)': 'Catar (más ancha, dientes más serrados, granate)' + Red Sea: Mar Rojo + Region of France.: 'Región de Francia.' + Republic of the Congo: 'República del Congo' + 'Reykjavík': Reikiavik + Riyadh: Riad + Romania: Rumania + 'Romania (slightly lighter blue)': 'Rumania (azul ligeramente más claro)' + Rome: Roma + Russia: Rusia + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rusia (sin escudo), Eslovenia (más ancha, escudo más pequeño)' + Rwanda: Ruanda + 'Réunion': 'Reunión' + Sahrawi Arab Democratic Republic: 'República Árabe Saharaui Democrática' + Saint Kitts and Nevis: 'San Cristóbal y Nieves' + Saint Lucia: 'Santa Lucía' + Saint Martin: 'San Martín (Francia)' + Saint Vincent and the Grenadines: San Vicente y las Granadinas + Sanaa: 'Saná' + Santiago: Santiago de Chile + Sardinia: 'Cerdeña' + Saudi Arabia: Arabia Saudita + Scandinavia: Escandinavia + Scotland: Escocia + Sea of Galilee: Mar de Galilea + Sea of Japan: 'Mar del Japón' + Sea of Okhotsk: Mar de Ojotsk + Semi-autonomous region of Tanzania.: 'Región semiautónoma de Tanzania.' + 'Senegal (green/yellow/red, green star)': 'Senegal (verde/amarillo/rojo, estrella verde)' + Seoul: 'Seúl' + Sicily: Sicilia + Sierra Leone: Sierra Leona + Singapore: Singapur + Sint Maarten: 'San Martín (Países Bajos)' + Skopje: Skopie + Slovakia: Eslovaquia + 'Slovakia (narrower, bigger coat of arms)': 'Eslovaquia (más estrecha, escudo más grande)' + 'Slovakia (with coat of arms)': 'Eslovaquia (con escudo)' + Slovenia: Eslovenia + Sofia: 'Sofía' + Solomon Islands: 'Islas Salomón' + Somaliland: Somalilandia + South Africa: 'Sudáfrica' + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'Sudáfrica no tiene capital oficial: los poderes del Estado están repartidos en tres ciudades: Pretoria (ejecutivo), Ciudad del Cabo (legislativo) y Bloemfontein (judicial).' + South America: 'América del Sur' + South China Sea: Mar de la China Meridional + South Korea: Corea del Sur + South Ossetia: Osetia del Sur + South Sudan: 'Sudán del Sur' + South Tarawa: Tarawa Sur + Southern Ocean: 'Océano Antártico' + Sovereign country: 'Es un país soberano' + Spain: 'España' + Special Administrative Region of China.: 'Región administrativa especial de China.' + Sri Jayawardenepura Kotte: Sri Jayawardenapura Kotte + 'St. George''s': Saint George + 'St. John''s': Saint John + State of the United States.: Estado de los Estados Unidos. + State recognised only by Turkey and claimed by Cyprus.: 'Estado reconocido solo por Turquía y reclamado por Chipre.' + Stockholm: Estocolmo + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Subregión de Oceanía compuesta por miles de islas pequeñas en el centro y la parte sur del océano Pacífico.' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Subregión de Oceanía compuesta por miles de islas pequeñas en la parte oeste del océano Pacífico.' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Subregión de Oceanía que incluye Vanuatu, las Islas Salomón, Fiyi y Papúa Nueva Guinea.' + Sudan: 'Sudán' + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudán (rojo/blanco/negro, triángulo verde), República Árabe Saharaui Democrática (con creciente y estrella)' + Sukhumi: Sujumi + Suriname: Surinam + Sweden: Suecia + Switzerland: Suiza + 'Switzerland has no official capital; Bern is the de facto capital.': 'Suiza no tiene capital oficial; Berna es la capital de facto.' + Syria: Siria + 'São Tomé': 'Santo Tomé' + 'São Tomé and Príncipe': 'Santo Tomé y Príncipe' + Taipei: 'Taipéi' + Taiwan: 'Taiwán' + Tajikistan: 'Tayikistán' + Tallinn: Tallin + Tashkent: Taskent + Tasman Sea: Mar de Tasmania + Tbilisi: Tiflis + Tehran: 'Teherán' + Thailand: Tailandia + The Bahamas: Bahamas + The Gambia: Gambia + Thimphu: Timbu + Timor Sea: Mar de Timor + Timor-Leste: Timor Oriental + Tiraspol: 'Tiráspol' + Tokyo: Tokio + Trinidad and Tobago: Trinidad y Tobago + Tripoli: 'Trípoli' + Tskhinvali: Tsjinval + Tunis: 'Túnez' + Tunisia: 'Túnez' + Turkey: 'Turquía' + Turkmenistan: 'Turkmenistán' + Turks and Caicos Islands: Islas Turcas y Caicos + Ukraine: Ucrania + Ulaanbaatar: 'Ulán Bator' + Unincorporated internal area of Norway.: Territorio dependiente de Noruega. + Unincorporated territory of the United States.: Territorio no incorporado de los Estados Unidos. + United Arab Emirates: 'Emiratos Árabes Unidos' + United Kingdom: Reino Unido + United States Virgin Islands: 'Islas Vírgenes de los Estados Unidos' + United States of America: Estados Unidos + Uzbekistan: 'Uzbekistán' + Valletta: La Valeta + Vatican City: Ciudad del Vaticano + Vienna: Viena + Vientiane: 'Vientián' + Vilnius: Vilna + Wales: Gales + Wallis and Futuna: Wallis y Futuna + Warsaw: Varsovia + Washington, D.C.: Washington D. C. + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: 'Aunque Ámsterdam es la capital oficial, La Haya es la sede del gobierno y del poder legislativo.' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: Aunque Dodoma es la capital oficial, Dar es-Salam es la capital de facto. + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': 'Aunque El Aaiún es la capital proclamada, Tifariti es la capital de facto.' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: Aunque Mbabane es la capital oficial y ejecutiva, Lobamba es la capital tradicional, espiritual y legislativa. + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: 'Aunque Porto Novo es la capital oficial, Cotonú es la capital de facto.' + While Sucre is the constitutional capital, La Paz is the seat of government.: Aunque Sucre es la capital constitucional, La Paz es sede de gobierno. + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: 'Aunque Yamusukro es la capital oficial, Abiyán es la capital de facto.' + White Sea: Mar Blanco + World region covering the Australian continent and most of the islands in the Pacific Ocean.: 'Continente que abarca la plataforma continental de Australia y la mayoría de islas del océano Pacífico.' + Yamoussoukro: Yamusukro + 'Yaoundé': 'Yaundé' + Yellow Sea: Mar Amarillo + Yerevan: 'Ereván' + Zanzibar: 'Zanzíbar' + Zimbabwe: Zimbabue + 'Åland Islands': 'Åland' + additions: + notes.note.celebes-sea.fields.field.country-info: 'También conocido como mar de Sulawesi.' + variables: + label.capital-hint.answer: + 'Hint: {{Capital hint}}': 'Pista: {{Capital hint}}' + label.flag: + Flag: Bandera + label.location: + Location: 'Ubicación' + note-type.name: + Ultimate Geography: 'Ultimate Geography [ES]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': 'Bandera similar a {{Flag similarity}}.' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': '860fGtb*C' + '5JJ2]i)p4': 'f=r^TR.L0,' + '5S(f7%O-$': 'X0E7(l3vg+' + 'B:x+BF' + 'BJ,,^wMC%N': 'vch6I=6.I^' + Bszsaz0Iuo: '@A!3Lr}AC)' + 'Cf2@;p|&U' + '_]DH+g9!$': 'dnXM*r6i' + 'cAlyc~G.y%': '4IW9N[*%GC' + 'cEO^y)Md[G': '4Mw' + 'e!KSebP}pt': 'CR+!|lD[|f' + 'e+/O]%*qfk': '6=uf': '6@4k^ad#{7' + 'e42BpV,P}C': '6$a$0NrHg[' + 'e5P*D#I{m{': '6%=p&3,>uY' + 'e,' + 'e_;dzXj8de': '6cwO!PU0l6' + 'epvt3>HH?U': '6x64b,+z|j' + 'euW0jm=wfR': '6C_}Ueyong' + 'e{e8vi@^PY': '6ePg6aB;Xn' + 'f#4-L[mp5*': '7,cs//Xh%G' + 'f*2@qiHh_1': '7!0PN': '7c7x2E]Q6': '7{Nf8U(:Yv' + 'fRSM{K+;[w': '7Z@:HCq)~;' + 'fT!w8R#>dg': 71i7gJj,l8 + 'fV`&p81%d(': '73Gm00~5lE' + fkq7lhLkdH: 'r[gU./Cvk`' + 'fxAN),FAc$': '7F#;o#)skB' + 'g-]bb&[.E!': '8?DMM6C%Mz' + 'g9v~y/;=z(': '8*6K9&w+HE' + 'g<[f89~U)A': '8_CQg1KM;?' + 'gBCR?!O*;E': '8J%?A2<9^^' + 'gB|K:+r?1V': '8JI.v!2-9k' + 'gDsLPj(9S#': '8L3/=bn10A' + 'gFOojQO(MH': '8NH0': '8Oe@_|:,Pp' + 'gRVZ]qJ#>O': '8Z^|Di-3{d' + 'gYIoR`|AxW': '86,Z?=IsFl' + 'gePC:Xt_nW': '8m=%vP4n98~V1': '9/GzY1g[3q' + 'h9Jr(H[=1~': 'bP!a$TSZBB' + 'h?a*UKfT_M': '9|Lp]CQLcb' + 'hE5j*e,Q?[': '9MdUp{rI|T' + 'hNE9}>;Q&-': '9V(hJ,wI/J' + 'hXNTVF<:SH': '95;[^xx(0{' + 'hZ/V/D&rO`': '97u^uvmjWX' + 'h_9=mkZXAH': '9chyXc|PI{' + hoHandbYAy: '9w+LY`MQI=' + 'hrI_uiW>E]': 9z,F5a_,MU + 'h}lnSN_f$_': '9gWY@FF|-W' + 'h~5xz+=ke~': '9hd8!!ycm1' + 'i)]+09JUR&': '!;Dq}1-MZD' + 'i2]v|D3@:j': '!!D6Ivb.]#' + 'iB-8Br~onq': '!Jsg$jKgv+' + 'iIS#Nw]tr]': '!Q@j;oDlzU' + 'iR;Um(L/~c=': '!4zXnDu[kP' + 'i^J8&$(1]|': '!b-gm4nTaZ' + 'ia8as9sCY|': '!igL313u6Z' + 'igiK,ff[eO': 'EsrUZ!O{Q%' + 'ijP>aJBT!9': '!r=zLB$L+y' + 'ilGtw#=asM': '!t*473y]Ab' + 'j%*8gN)q>d': '#.pgRFoi{5' + 'j)i)pB*HJ,': '#;To0tpzRI' + 'j2IQ=f=w3@': '#!,>y|yo#S' + j7eEZkzsCZ: '#(P(|c!kKo' + 'j9:K,~DQ2v': '#*v.r[&I!:' + 'jg|}': '#JOB#-z}f0' + 'jCd]`-=k,:': '#KODG$yc>M' + 'jDA-V?/hVj': '#L#s^-u~3#' + jI9P-f6r3M: '#Qh=s|ej#b' + 'jKIDq|}J&c': '#S,&1?JB/4' + 'jKO,9y0M;#': '#SP7XjN' + 'koI(}OyJ:@': '$w,nJG9B]S' + 'kopwzzB``A': 'N`Q4k]7};1' + 'k}4/oj#$~s': '$gcuZbj4h-' + 'k}A9]O:0xw': '$g#hDGvSF;' + 'l$2O|9w`F~': '%-aKi!j#v+': '%=z.T2U3DH' + 'l3Ly8PBxt(': '%#/9gH$pBE' + 'lAxH.CfJB@': '%I8+tuQBJS' + 'lH^2D]w;[H': '%PEa&:7)~{' + 'lU@C3p3' + 'mtd;>7T|g>': '&BOwzZ[?oQ' + 'mz[>>AxIr4': '&HCzzs8Azt' + 'm{noqE29%I': '&eYZ1wa1.|' + 'm{oM,jr>C#': '&eZ:rb2,KA' + 'n&>e`2FO/S': '(/zPGU)G[h' + 'n+t>2`:;P/': '(=4za=v)XL' + 'n:7&>YT5A=': '(]fmzQ[XIP' + 'nIfRn/yz': ')dGvASz&G>' + 'oa87&PB&Bm': ')igfmH$6J&' + 'onMKID2d;D': ')v:.,va`^]' + 'o~}KVA:VbI': ')hJ.^svNj|' + 'p#R$-mG/L': 'h,gB?X*uD' + 'p.yv>BQ470': '*@96zt>W(p' + 'p:SPZ:e/*[': '*]@=|(P&DrFhd%u': '*0z&2xS`./' + 'pYRw[=6;JI': 'xJzDb{C(wd' + 'p]t8:Tka7q': '*a4gvLV](+' + pkj3RPBUrF: '*sUb?H$Mz_' + 'pwwZu{7f~(': '*E7|5>f|hE' + 'pz(G*LI2fHxg' + 'q1Q3Do8S>g': '+9>b&ggK{8' + 'q2#Yv[O0:|': '+!j{6/Wg': '+]`9*|j,48' + 'q=w[fB$d+$': '+FlzQtk`=B' + 'r(h9&#wc~B': ',:Shm37_h@' + 'r/B|ra%L,.': ',[$I2]lD>K' + 'r5C646H*+*': ',%%ecY+9=G' + 'r6BV%|..*5': ',&$^l?t%aI-$$|wT': ',{L,s4k?Ei' + 'r>g<|,FWi%': ',{RxI#)OqC' + 'r?E$4f-d0/': ',|(kc|s`8L' + 'rPekx@n9:L': ',XPV8.Y1]a' + 'rSneU!3}bn': ',0YP]2b@j(' + 'r]*Ioa%$bK': ',ap,Z]l4j~' + 's#:aIK$9^x': '-,vL,Ck1b<' + 's4E?ZF[E<>': '-$(A|xCw_Q' + 's9-GL*@AXD': '-*s*/9Bs5]' + 's<_WLSB3I/': '-_F_/K$VQL' + 's>f40rn,O]': 'yK@HKIZR,h' + 'sB7rZQsR@9w~i^p': '-6zBhoKab*' + 'skOM(?n$GG': '-s<:n-Y4O`' + spKoFRM4if: '-x.Z)J:Wq7' + 'ssbyA9}]QP': 'izUG=]G]=<' + 'stRa/S(0$F': '-B?LuKnS-_' + 'sxOMs+bE8@': '-F<:3!Mw)S' + 'sx|l+io`B@': '-FIWqaZ=JS' + 's|K&2S,UP|': '-f.maKrMXZ' + 't$y1C2R-Av': '.-9~%U?$I:' + 't+v9b--,qs': '.=6hM$s#y-' + 't-0>F]bu>P': '.?}z):Mm{e' + 't2&ri|[Un:': '.!m2T?CMvM' + t3aZXtpNyi: '.#L|`l0FG!' + 't50y/[AEhF': '.%}9u/#wp_' + 'tMMtBUpb$&': '.U:4$M0^-D' + 'tTnCoO/*tk': '.1Y%ZGu9B$' + 't_h*fbN#:(': '.cSpQ^;3]E' + 't`cagaWApo': '.dNLR]_sx)' + 'tcneuK7v%`': .kYP5Cfn.X + tfCY4j6KEZ: 'JaKw#SXuA.' + 'tj&bK}%P!F': '.rmM.@lH+_' + 'twrhzJ[k:Z': '.E2S!BCc]o' + 't|h3|;E4g{': '.fSbI)(WoY' + 'u-`cspdp)D': '/?GN3hOh;]' + 'u4^lH.xem5': '/$EW+%8{uu' + 'u;|vR#mVMW': '/^I6?3XNUl' + 'uH*Q(mcRt5': '/Pp>neNJBu' + 'uRo^w#p5Be': /ZZE730XJ6 + 'uc61ToT%gt': '/ke~[g[5o.' + 'ui3>b-?a@m': '/qbzM$A]}&' + upiX0g1quR: '/xT`}}~iCg' + 'uv1fGXHN@)': '/D~Q*P+F}F' + 'u|I}oElaH)': '/f,JZwW]PF' + 'vE[T$`n9T)': ':MC[k=Y11F' + 'vKx<0Qa!aP': 'k=,7DzSs>p' + 'vNiD6A.v#s': ':VT&estn,-' + 'va/ky=f5pK': ':iuV9+QXx~' + 'vf)G?[_?Pf': ':no*A/F-X7' + 'vgdj#X?8dB': ':oOUjPA0l@' + 'vpOTK6tMr@': ':x<[.Y4EzS' + 'vppEA|$@~g': 'gboIok!xh^' + 'vrn%j%6{nu': ':zYlU5e>v/' + 'vwc3}.=Z#&': ':ENbJ%yR,D' + 'wa>{k!2cXc': cv-,SC1HIr + x2d4n4ADcP: ']pOHrn[3r' + 'zSc]-^U=0=': '>0NDs;]+8P' + crowdanki:uuid: + 43c5ba66-9a65-11e8-90c9-a0481cc15658: cb4d32ee-12ed-9960-1841-28c09449ded0 + 43e2586a-9a65-11e8-a777-a0481cc15658: 0bacddfd-1e81-4e62-9152-bdff33db0374 +deck: + name: + intent: replace + value: 'Ultimate Geography [ES]' + expected_base: + value: Ultimate Geography + description: + intent: replace + value: | + DESCRIPCIÓN COMPLETA | NOTAS DE LANZAMIENTO | CÓMO CONTRIBUIR + + Características de Ultimate Geography v5.3: + + - Los 205 estados soberanos del mundo (820 cartas). + - 59 territorios, regiones del mundo, y otras entidades (103 cartas). + - 48 océanos y mares (48 cartas, solo mapas). + - 7 continentes (7 cartas, solo mapas). + - Un total de 319 notas diferentes, 978 cartas, 221 banderas y 319 mapas. + + Este mazo está disponible en inglés, alemán, español, francés, noruego, checo, ruso, neerlandés, sueco, portugués, chino (simplificado y tradicional), polaco, italiano y danesa. También está disponible una versión extendida en cada idioma. Para ayudar a la memorización y contextualizar mientras se aprende, algunas notas incluyen información extra como banderas similares, información sobre la forma de gobierno, nombres alternativos del país, etc. + + Puedes usar la función de Anki de mazos filtrados para centrarte en alguna parte del mazo en particular, como estados soberanos, un tipo de plantilla de nota (por ejemplo, mapa-país) o un continente en concreto (por ejemplo, Europa). + + Este mazo se desarrolla en Github. Si encuentras algún fallo, tienes alguna sugerencia o quieres ayudar, por favor, no dudes en abrir una incidencia. ¿Quieres mantenerte informado de las nuevas versiones? ¡Mira el repositorio de Github o sigue los lanzamientos! + expected_base: + value: | + FULL DESCRIPTION | RELEASE NOTES | CONTRIBUTING + + Ultimate Geography v5.3 features: + + - the world's 205 sovereign states (820 cards) + - 59 territories, world regions, and other entities (103 cards) + - 48 oceans and seas (48 cards, maps only) + - 7 continents (7 cards, maps only) + - for a total of 319 unique notes, 978 cards, 221 flags and 319 maps. + + The deck is available in English, German, Spanish, French, Norwegian, Czech, Russian, Dutch, Swedish, Portuguese, Chinese (simplified and traditional), Polish, Italian and Danish. An extended version is also available in each language. To help with memorisation and provide context while learning, some notes include extra information such as similar flags, governance information, alternative country names, etc. + + You can use Anki's filtered deck feature to focus your study on a subset of the deck, such as sovereign states, a single note template (e.g. map to country), or a specific continent (e.g. Europe). + + This deck is maintained on GitHub. If you spot a mistake, have a suggestion or want to help, please don't hesitate to open an issue. Want to stay informed of new releases? Watch the GitHub repository or subscribe to the releases feed! diff --git a/fixtures/ultimate-geography/overlays/languages/fr.yaml b/fixtures/ultimate-geography/overlays/languages/fr.yaml new file mode 100644 index 0000000..22752f1 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/fr.yaml @@ -0,0 +1,768 @@ +id: overlay.translation.fr +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': 'Il y a un contentieux sur le nom de cette mer entre les pays qui l''entourent, la Corée du Sud l''appelant notamment Mer de l''Est.' + Abkhazia: Abkhazie + Abu Dhabi: Abou Dabi + Addis Ababa: Addis-Abeba + Adriatic Sea: Mer Adriatique + Aegean Sea: 'Mer Égée' + Africa: Afrique + Akrotiri and Dhekelia: Akrotiri et Dhekelia + Albania: Albanie + Algeria: 'Algérie' + Algiers: Alger + Also known as Burma.: Aussi connue comme le Myanmar. + Also known as Cabo Verde.: '' + Also known as Czechia.: 'Aussi connue comme la République tchèque.' + Also known as East Timor.: Aussi connu comme le Timor-Leste. + Also known as Kiev.: '' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: Aussi connu comme le golfe du Siam. + Also known as the Sea of Cortez.: 'Aussi connu comme la mer de Cortés.' + 'Also spelled as Sana''a.': '' + American Samoa: 'Samoa américaines' + Andorra: Andorre + 'Andorra (narrower, coat of arms with motto)': 'Andorre (plus étroit, blason avec devise)' + Andorra la Vella: Andorre-la-Vieille + Antarctica: Antarctique + Antigua and Barbuda: Antigua-et-Barbuda + Arabian Sea: 'Mer d''Arabie' + Aral Sea: 'Mer d''Aral' + Arctic Ocean: 'Océan Arctique' + Argentina: Argentine + Armenia: 'Arménie' + Ashgabat: Achgabat + Asia: Asie + 'Asunción': Asuncion + Athens: 'Athènes' + Atlantic Ocean: 'Océan Atlantique' + Australia: Australie + 'Australia (white stars, two more stars)': 'Australie (étoiles blanches, deux étoiles de plus)' + Austria: Autriche + 'Austria (brighter red, wider white band)': 'Autriche (rouge plus clair, bande blanche plus large)' + Autonomous community of Spain.: 'Communauté autonome d''Espagne.' + Autonomous province of South Korea.: 'Province autonome de Corée du Sud.' + Autonomous region of Finland.: Province autonome de Finlande. + Autonomous region of Italy.: 'Région d''Italie à statut spécial.' + Autonomous region of Papua New Guinea.: 'Région autonome de Papouasie-Nouvelle-Guinée.' + Autonomous region of Portugal.: 'Région autonome du Portugal.' + Azerbaijan: 'Azerbaïdjan' + Azores: 'Açores' + Baghdad: Bagdad + Bahrain: 'Bahreïn' + 'Bahrain (narrower, fewer serrated edges, red)': 'Bahreïn (séparation plus étroite et moins dentelée, rouge)' + Baku: Bakou + Balkan Peninsula: Balkans + Baltic Sea: Mer Baltique + Banda Sea: Mer de Banda + Barbados: Barbade + Barents Sea: Mer de Barents + Bay of Bengal: Golfe du Bengale + Bay of Biscay: Golfe de Gascogne + Beijing: 'Pékin' + Beirut: Beyrouth + Belarus: 'Biélorussie' + Belgium: Belgique + Benin: 'Bénin' + Bering Strait: 'Détroit de Béring' + Bermuda: Bermudes + Bern: Berne + Bhutan: Bhoutan + Bishkek: Bichkek + Black Sea: Mer Noire + 'Bogotá': Bogota + Bolivia: Bolivie + 'Bolivia (coat of arms instead of star)': 'Bolivie (blason à la place de l''étoile)' + Bosnia and Herzegovina: 'Bosnie-Herzégovine' + 'Brasília': Brasilia + Brazil: 'Brésil' + British Virgin Islands: 'Îles Vierges britanniques' + Brussels: Bruxelles + Bucharest: Bucarest + Bulgaria: Bulgarie + Cairo: Le Caire + Cambodia: Cambodge + Cameroon: Cameroun + 'Cameroon (green/red/yellow, yellow star)': 'Cameroun (vert/rouge/jaune, étoile jaune)' + Canary Islands: 'Îles Canaries' + Cape Verde: Cap-Vert + Caribbean Sea: 'Mer des Caraïbes' + Caspian Sea: Mer Caspienne + Cayman Islands: 'Îles Caïmans' + Celebes Sea: 'Mer de Célèbes' + Celtic Sea: Mer Celtique + Central African Republic: 'République centrafricaine' + Cetinje is an honorary capital.: Cetinje est une capitale honoraire. + Chad: Tchad + 'Chad (slightly darker blue)': 'Tchad (bleu légèrement plus foncé)' + Charlotte Amalie: 'Charlotte-Amélie' + Chile: Chili + China: Chine + City of San Marino: Ville de Saint-Marin + Claimed and controlled: 'Revendiquée et contrôlée' + Claimed but not controlled: 'Revendiquée mais non contrôlée' + Colombia: Colombie + 'Colombia (no coat of arms)': 'Colombie (pas de blason)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'On fait souvent référence à Colombo comme capitale, mais Sri Jayawardenapura Kotte, une ville de la banlieue de Colombo, est la capitale législative officielle.' + Comoros: Comores + Constituent country in the Kingdom of Denmark.: Pays constitutif du royaume du Danemark. + Constituent country of the Kingdom of the Netherlands.: 'État autonome du royaume des Pays-Bas.' + Constituent country of the United Kingdom.: Nation constitutive du Royaume-Uni. + Cook Islands: 'Îles Cook' + Copenhagen: Copenhague + Coral Sea: Mer de Corail + Corsica: Corse + Croatia: Croatie + Crown dependency of the United Kingdom.: 'Dépendance de la Couronne britannique.' + 'Cuba (red triangle, blue stripes)': 'Cuba (triangle rouge, bandes bleues)' + 'Curaçao (two stars in top-left corner)': 'Curaçao (deux étoiles en haut à gauche)' + Cyprus: Chypre + Czech Republic: 'Tchéquie' + Damascus: Damas + Dead Sea: Mer Morte + Democratic Republic of the Congo: 'République démocratique du Congo' + Denmark: Danemark + Denmark Strait: 'Détroit de Danemark' + Dhaka: Dacca + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': 'Contestée; revendiquée par Israël; Ramallah est la capitale administrative de fait.' + 'Disputed; claimed by Palestine.': 'Contestée; revendiquée par la Palestine.' + Dominica: Dominique + Dominican Republic: 'République dominicaine' + Dushanbe: 'Douchanbé' + East China Sea: Mer de Chine orientale + East Siberian Sea: 'Mer de Sibérie orientale' + Ecuador: 'Équateur' + 'Ecuador (with coat of arms)': 'Équateur (avec blason)' + Edinburgh: 'Édimbourg' + Egypt: 'Égypte' + 'Egypt (emblem instead of text), Yemen (no text)': 'Égypte (emblème à la place du texte), Yémen (pas de texte)' + 'Egypt (with emblem), Iraq (with text)': 'Égypte (avec un emblème), Irak (avec du texte)' + El Salvador: Salvador + 'El Salvador (different coat of arms, slightly darker blue)': 'Salvador (blason différent, bleu légèrement plus foncé)' + England: Angleterre + English Channel: Manche + Equatorial Guinea: 'Guinée équatoriale' + Eritrea: 'Érythrée' + Estonia: Estonie + Ethiopia: 'Éthiopie' + European Union: 'Union européenne' + Falkland Islands: 'Îles Malouines' + Faroe Islands: 'Îles Féroé' + Federated States of Micronesia: 'États fédérés de Micronésie' + Fiji: Fidji + Finland: Finlande + Formerly Zaire.: 'Anciennement le Zaïre.' + Formerly known as Macedonia.: 'Anciennement connue comme la Macédoine.' + French Guiana: Guyane + French Polynesia: 'Polynésie française' + Georgia: 'Géorgie' + Germany: Allemagne + 'Ghana (star instead of coat of arms)': 'Ghana (étoile à la place du blason)' + Greece: 'Grèce' + Greenland: Groenland + Grenada: Grenade + Guatemala City: Guatemala + Guernsey: Guernesey + Guinea: 'Guinée' + 'Guinea (green and red flipped, slightly darker green)': 'Guinée (vert et rouge inversés, vert légèrement plus foncé)' + Guinea-Bissau: 'Guinée-Bissau' + Gulf of Alaska: 'Golfe d''Alaska' + Gulf of California: Golfe de Californie + Gulf of Carpentaria: Golfe de Carpentarie + Gulf of Guinea: 'Golfe de Guinée' + Gulf of Mexico: Golfe du Mexique + Gulf of Thailand: 'Golfe de Thaïlande' + Haiti: 'Haïti' + Hanoi: 'Hanoï' + Havana: La Havane + Hawaii: 'Hawaï' + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: 'Région historique et culturelle d''Europe du Nord constituée de trois monarchies constitutionnelles, le Danemark, la Norvège et la Suède, et parfois de façon incorrecte l''Islande et la Finlande.' + Hudson Bay: 'Baie d''Hudson' + Hungary: Hongrie + Iceland: Islande + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Islande (fond bleu, croix rouge et blanche), Norvège (fond rouge, croix bleue et blanche)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Islande (fond bleu, croix rouge), Îles Féroé (fond blanc, croix rouge et bleue)' + Independent state claimed by Georgia.: 'État indépendant revendiqué par la Géorgie.' + Independent state claimed by Moldova.: 'État indépendant revendiqué par la Moldavie.' + Independent state claimed by Somalia.: 'État indépendant revendiqué par la Somalie.' + India: Inde + Indian Ocean: 'Océan Indien' + Indonesia: 'Indonésie' + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonésie (blanc et rouge inversés, rouge plus clair), Monaco (blanc et rouge inversés, plus étroit)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonésie (plus large, rouge plus clair), Pologne (rouge et blanc inversés, plus large)' + Iraq: Irak + 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (texte à la place de l''emblème), Yémen (pas d''emblème)' + Ireland: Irlande + 'Ireland (orange and green flipped, wider)': 'Irlande (orange et vert inversés, plus large)' + Island of Indonesia.: + notes.note.bali.fields.field.country-info: 'Île d''Indonésie.' + notes.note.java.fields.field.country-info: 'Île d''Indonésie.' + notes.note.sumatra.fields.field.country-info: 'Île indonésienne.' + Isle of Man: 'Île de Man' + Israel: 'Israël' + Italy: Italie + Ivory Coast: 'Côte d''Ivoire' + 'Ivory Coast (green and orange flipped, narrower)': 'Côte d''Ivoire (vert et orange inversés, plus étroit)' + Jamaica: 'Jamaïque' + Japan: Japon + Jerusalem: 'Jérusalem' + Jordan: Jordanie + Juba: Djouba + Kabul: Kaboul + Kaliningrad Oblast: Oblast de Kaliningrad + Kathmandu: Katmandou + Known as Nur-Sultan between 2019 and 2022: Connue sous le nom de Noursoultan entre 2019 et 2022 + Known as Swaziland until 2018.: 'Connu comme le Swaziland jusqu''en 2018.' + Kuwait: 'Koweït' + Kuwait City: 'Koweït' + Kyiv: Kiev + Kyrgyzstan: Kirghizistan + Laayoune: 'Laâyoune' + Labrador Sea: Mer du Labrador + Latvia: Lettonie + 'Latvia (darker red, narrower white band)': 'Lettonie (rouge plus foncé, bande blanche plus étroite)' + Lebanon: Liban + Libya: Libye + Lisbon: Lisbonne + Lithuania: Lituanie + London: Londres + 'Luxembourg (lighter blue)': 'Luxembourg (bleu plus clair)' + Luxembourg City: Luxembourg + Macau: Macao + Madeira: 'Madère' + Malaysia: Malaisie + 'Mali (red and green flipped, slightly brighter green)': 'Mali (rouge et vert inversés, vert légèrement plus clair)' + Malta: Malte + Manila: Manille + Marshall Islands: 'Îles Marshall' + Mauritania: Mauritanie + Mauritius: Maurice + Mediterranean Sea: 'Mer Méditerranée' + Melanesia: 'Mélanésie' + Mexico: Mexique + Mexico City: Mexico + Micronesia: 'Micronésie' + Mogadishu: Mogadiscio + Moldova: Moldavie + 'Moldova (wider, coat of arms with eagle)': 'Moldavie (plus large, blason avec un aigle)' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (plus étroit, rouge plus foncé), Pologne (rouge et blanc inversés, rouge plus foncé)' + Mongolia: Mongolie + Montenegro: 'Monténégro' + Morocco: Maroc + Moscow: Moscou + Muscat: Mascate + Myanmar: Birmanie + 'N''Djamena': 'N''Djaména' + Namibia: Namibie + 'Nauru (single star below yellow band)': 'Nauru (une seule étoile sous la bande jaune)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru n''a pas de capitale officielle ; le district de Yaren est la capitale de fait.' + Nepal: 'Népal' + Netherlands: Pays-Bas + 'Netherlands (darker blue)': 'Pays-Bas (bleu plus foncé)' + New Caledonia: 'Nouvelle-Calédonie' + New Zealand: 'Nouvelle-Zélande' + 'New Zealand (red stars, two fewer stars)': 'Nouvelle-Zélande (étoiles rouges, deux étoiles de moins)' + 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (blason différent, bleu légèrement plus clair)' + Nicosia: Nicosie + North America: 'Amérique du Nord' + North Korea: 'Corée du Nord' + North Macedonia: 'Macédoine du Nord' + North Nicosia: Nicosie-Nord + North Sea: Mer du Nord + Northern Cyprus: Chypre du Nord + Northern Ireland: Irlande du Nord + Northern Mariana Islands: 'Îles Mariannes du Nord' + Norway: 'Norvège' + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norvège (fond rouge, croix bleue), Îles Féroé (fond blanc, croix rouge et bleue)' + Norwegian Sea: 'Mer de Norvège' + Not a sovereign country: Pas une nation souveraine + 'Oblast (administrative region) of the Russian Federation.': 'Oblast (région administrative) de la fédération de Russie.' + Oceania: 'Océanie' + Official capital was moved from Bujumbura to Gitega in 2019.: 'La capitale officielle a été déplacée de Bujumbura à Gitega en février 2019.' + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: 'La capitale officielle a été déplacée de Malabo à Ciudad de la Paz en 2026.' + 'Officially Côte d''Ivoire.': '' + Officially Luxembourg.: '' + Overseas department of France.: 'Département français d''outre-mer.' + Overseas territory of France.: + notes.note.french-polynesia.fields.field.country-info: 'Collectivité française d''outre-mer.' + notes.note.new-caledonia.fields.field.country-info: 'Territoire français d''outre-mer.' + notes.note.saint-martin.fields.field.country-info: 'Collectivité française d''outre-mer.' + notes.note.wallis-and-futuna.fields.field.country-info: 'Collectivité française d''outre-mer.' + Overseas territory of the United Kingdom.: 'Territoire britannique d''outre-mer.' + Pacific Ocean: 'Océan Pacifique' + Palau: Palaos + 'Palestine (black/white/green, red arrow)': 'Palestine (noir/blanc/vert, flèche rouge)' + 'Palestine (no symbol)': 'Palestine (pas de symbole)' + Panama City: Panama + Papua New Guinea: 'Papouasie-Nouvelle-Guinée' + Partially recognised state claimed by China.: 'État partiellement reconnu revendiqué par la Chine.' + Partially recognised state claimed by Morocco. Also known as Western Sahara.: 'État partiellement reconnu revendiqué par le Maroc.' + Partially recognised state claimed by Serbia.: 'État partiellement reconnu revendiqué par la Serbie.' + Persian Gulf: Golfe Persique + Peru: 'Pérou' + Philippine Sea: Mer des Philippines + Poland: Pologne + Polynesia: 'Polynésie' + Port Louis: Port-Louis + Port Vila: Port-Vila + Port of Spain: 'Port-d''Espagne' + Pretoria, Cape Town, Bloemfontein: Pretoria, Le Cap, Bloemfontein + Puerto Rico: Porto Rico + 'Puerto Rico (blue triangle, red stripes)': 'Porto Rico (triangle bleu, bandes rouges)' + 'Qatar (wider, more serrated edges, maroon)': 'Qatar (séparation plus large et dentelée, pourpre/marron)' + Red Sea: Mer Rouge + Region of France.: 'Région française.' + Republic of the Congo: 'République du Congo' + 'Reykjavík': Reykjavik + Riyadh: Riyad + Romania: Roumanie + 'Romania (slightly lighter blue)': 'Roumanie (bleu légèrement plus clair)' + Russia: Russie + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Russie (pas de blason), Slovénie (plus large, blason plus petit)' + 'Réunion': 'La Réunion' + Sahrawi Arab Democratic Republic: 'République arabe sahraouie démocratique' + Saint Kitts and Nevis: 'Saint-Christophe-et-Niévès' + Saint Lucia: Sainte-Lucie + Saint Martin: 'Saint-Martin (Antilles françaises)' + Saint Vincent and the Grenadines: Saint-Vincent-et-les-Grenadines + San Marino: Saint-Marin + Santo Domingo: Saint-Domingue + Sardinia: Sardaigne + Saudi Arabia: Arabie saoudite + Scandinavia: Scandinavie + Scotland: 'Écosse' + Sea of Galilee: 'Lac de Tibériade' + Sea of Japan: Mer du Japon + Sea of Okhotsk: 'Mer d''Okhotsk' + Semi-autonomous region of Tanzania.: 'Région autonome de Tanzanie.' + Senegal: 'Sénégal' + 'Senegal (green/yellow/red, green star)': 'Sénégal (vert/jaune/rouge, étoile verte)' + Seoul: 'Séoul' + Serbia: Serbie + Sicily: Sicile + Singapore: Singapour + Sint Maarten: 'Saint-Martin (royaume des Pays-Bas)' + Slovakia: Slovaquie + 'Slovakia (narrower, bigger coat of arms)': 'Slovaquie (plus étroit, blason plus grand)' + 'Slovakia (with coat of arms)': 'Slovaquie (avec blason)' + Slovenia: 'Slovénie' + Solomon Islands: 'Îles Salomon' + Somalia: Somalie + South Africa: Afrique du Sud + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'L''Afrique du Sud n''a pas de capitale officielle. Les branches du gouvernement sont réparties sur trois villes : Pretoria (exécutive), Le Cap (législative) et Bloemfontein (judiciaire).' + South America: 'Amérique du Sud' + South China Sea: 'Mer de Chine méridionale' + South Korea: 'Corée du Sud' + South Ossetia: 'Ossétie du Sud-Alanie' + South Sudan: Soudan du Sud + South Tarawa: Tarawa-Sud + Southern Ocean: 'Océan Austral' + Sovereign country: Nation souveraine + Spain: Espagne + Special Administrative Region of China.: 'Région administrative spéciale de Chine.' + Sri Jayawardenepura Kotte: Sri Jayawardenapura Kotte + 'St. George''s': Saint-Georges + 'St. John''s': 'Saint John''s' + State of the United States.: 'État des États-Unis.' + State recognised only by Turkey and claimed by Cyprus.: 'État reconnu uniquement par la Turquie et revendiqué par Chypre.' + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Région d''Océanie comprenant des milliers de petites îles dans les parties centrale et orientale de l''océan Pacifique.' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Région d''Océanie comprenant des milliers de petites îles dans l''ouest de l''océan Pacifique.' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Région d''Océanie qui inclut le Vanuatu, les Îles Salomon, les Fidji et la Papouasie-Nouvelle-Guinée.' + Sudan: Soudan + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Soudan (rouge/blanc/noir, flèche verte), République arabe sahraouie démocratique (avec étoile et croissant)' + Sukhumi: Soukhoumi + Sweden: 'Suède' + Switzerland: Suisse + 'Switzerland has no official capital; Bern is the de facto capital.': 'La Suisse n''a pas de capitale officielle ; Berne est la capitale de fait.' + Syria: Syrie + 'São Tomé and Príncipe': 'Sao Tomé-et-Principe' + Taiwan: 'Taïwan' + Tajikistan: Tadjikistan + Tanzania: Tanzanie + Tashkent: Tachkent + Tasman Sea: Mer de Tasman + Tbilisi: Tbilissi + Tehran: 'Téhéran' + Thailand: 'Thaïlande' + The Bahamas: Bahamas + The Gambia: Gambie + Thimphu: Thimphou + Timor Sea: Mer de Timor + Timor-Leste: Timor oriental + Transnistria: Transnistrie + Trinidad and Tobago: 'Trinité-et-Tobago' + Tunisia: Tunisie + Turkey: Turquie + Turkmenistan: 'Turkménistan' + Turks and Caicos Islands: 'Îles Turques-et-Caïques' + Uganda: Ouganda + Ulaanbaatar: Oulan-Bator + Unincorporated internal area of Norway.: 'Territoire sous souveraineté norvégienne.' + Unincorporated territory of the United States.: 'Territoire non incorporé des États-Unis.' + United Arab Emirates: 'Émirats arabes unis' + United Kingdom: Royaume-Uni + United States Virgin Islands: 'Îles Vierges des États-Unis' + United States of America: 'États-Unis' + Uzbekistan: 'Ouzbékistan' + Valletta: La Valette + Vatican City: Vatican + Vienna: Vienne + Vietnam: 'Viêt Nam' + Wales: Pays de Galles + Wallis and Futuna: Wallis-et-Futuna + Warsaw: Varsovie + Washington, D.C.: Washington + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: 'Bien qu''Amsterdam soit la capitale officielle, La Haye est le siège du gouvernement et du parlement.' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: 'Bien que Dodoma soit la capitale officielle, Dar es Salaam est le siège du gouvernement.' + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': 'Bien que Laâyoune soit la capitale déclarée, Tifariti est le siège du gouvernement.' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: 'Bien que Mbabane soit la capitale exécutive officielle, Lobamba est la capitale traditionnelle, spirituelle et législative.' + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: 'Bien que Porto-Novo soit la capitale officielle, Cotonou est le siège du gouvernement.' + While Sucre is the constitutional capital, La Paz is the seat of government.: 'Bien que Sucre soit la capitale constitutionnelle, La Paz est le siège du gouvernement.' + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: 'Bien que Yamoussoukro soit la capitale officielle, Abidjan est le siège du gouvernement.' + White Sea: Mer Blanche + World region covering the Australian continent and most of the islands in the Pacific Ocean.: 'Région du monde englobant le continent australien et la plupart des îles de l''océan Pacifique.' + Yellow Sea: Mer Jaune + Yemen: 'Yémen' + Yerevan: Erevan + Zambia: Zambie + 'Åland Islands': 'Åland' + additions: + notes.note.celebes-sea.fields.field.country-info: Aussi connu comme la mer de Sulawesi. + variables: + label.capital: + Capital: Capitale + label.capital-hint.answer: + 'Hint: {{Capital hint}}': 'Indice : {{Capital hint}}' + label.flag: + Flag: Drapeau + label.location: + Location: Emplacement + note-type.name: + Ultimate Geography: 'Ultimate Geography [FR]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': 'Drapeau similaire à : {{Flag similarity}}.' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': cYWtxb5I.n + '5JJ2]i)p4': 'uE!R:a{FS{' + '5S(f7%O-$': 'kbJZsU]Tm~' + '?n!)c/h=Q': 'QykP%i/vKj' + 'AES],s:8=': 'i_0H+>}|<.' + 'B:x' + 'SlOEXfq#|': 'FIQ,YV4(u' + 'W5?6yQZD2': 'N$AR]vQR?L' + '_]DH+g9!$': 'Jt*GgcbWd>&Z' + 'dY96`C-f(_': 'e?6[KX^Js+' + 'd``B*6:eCx': 'dC[x.]4VGu' + 'dbP/Cj}sB+': 'rVJ^|c*{=!' + 'de=9c:,@j9': 'ET[Oa;OLaK' + 'dlU7VG|3Gd': 'mC$X9cRfF4' + 'd}R:qHj1,7': 'IG)L^LIKi]' + 'e!KSebP}pt': 'Fck/PT^@wO' + 'e+/O]%*qfk': 'p);vRzMs+W' + 'e.t$Vi5,>f': 'DS~l]pq.y<' + 'e42BpV,P}C': 'bTrCSD^W4m' + 'e5P*D#I{m{': 'E2g&j~V_+i' + 'e9NjBH' + 'eHJ?)TAdTo': 'k]|`O*|sYC' + 'eN#^kPGv*L': 'c.@-5vbYSr' + 'eTQ&:;ekF' + 'epvt3>HH?U': 'HY(==0nL/5' + 'euW0jm=wfR': 'HQ$_4*oGf3' + 'e{e8vi@^PY': 'jFv`AX2j9Z' + 'f#4-L[mp5*': 'lW;xJ;HJv/' + 'f*2@qiHh_1': 'xs+r,A:Ybh' + 'f*LO~>!0PN': 'xj/*Nw.FyN' + 'f21=-A~W;~': 'n65[nUZiZc' + 'f>c7x2E]Q6': 'IkP885[1L6' + 'fRSM{K+;[w': 'ymuYBM5)ux' + 'fT!w8R#>dg': 'vVH0': 'l?;mPU|Vu$' + 'gRVZ]qJ#>O': QplodT,RlZ + 'gYIoR`|AxW': 'bP:bhgKryC' + 'gePC:Xt_nW': 'Ay%EM@L0b~' + 'gp[h@]y13l': 'cH+7jy4I,m' + 'gsS{jR,]|Q': 'oi2j6zc,T]' + 'gx:;za!?C9': 'uQ-6g&t&/t' + 'gz9${p}er*': 'dTNjd[5|Ge' + 'g~%pF`(x{u': 'uhYJm4N/!$' + 'h&`>n98~V1': 'P[t`X50n$K' + 'h9Jr(H[=1~': 'JDSADMst&0' + 'h?a*UKfT_M': 'm`:zwx?UCO' + 'hE5j*e,Q?[': 'oCa[}6b]{J' + 'hNE9}>;Q&-': 'i+%_VY]7bt' + 'hXNTVF<:SH': 'hE&?:>xB0v' + 'hZ/V/D&rO`': 'OXH#q-u9XF' + 'h_9=mkZXAH': 'Ft$h3i.|l.' + hoHandbYAy: 'fRSFWQK>W(' + 'hrI_uiW>E]': 'H38F4o,>An' + 'h}lnSN_f$_': 'z>%tQDyw7D' + 'h~5xz+=ke~': 'Ive-9<)?/' + 'i)]+09JUR&': 'G]3ZD2G$ww' + 'i2]v|D3@:j': 'w1FM?@j-tk' + 'iB-8Br~onq': 'dNo9L*#}F?' + 'iIS#Nw]tr]': 'x7GO_2-WY$' + 'iR;Um(L/~c=': 'CW+/?p:?BO' + 'i^J8&$(1]|': 'CY~?X/vp+j' + 'ia8as9sCY|': 'uO%6i;{0PZ' + 'igiK,ff[eO': 'p*1Gvs%}#V' + 'ijP>aJBT!9': 'N@X:d': 'Ddu%%IIGo*' + 'j)i)pB*HJ,': 'xwNS`|r~1x' + 'j2IQ=f=w3@': 'I_Q-5hX|5;' + j7eEZkzsCZ: 'IJG[L,X;A3' + 'j9:K,~DQ2v': 'HsOJP(RQoY' + 'jg|}': 'BxD0*Nx8HN' + 'jCd]`-=k,:': 'INr2(vFB[p' + 'jDA-V?/hVj': 'g[tAvcrR3^' + jI9P-f6r3M: 'Kc0|/i[KY@' + 'jKIDq|}J&c': 'd*RV!D6_/z' + 'jKO,9y0M;#': 'ny_8#ZQA/?' + 'jN^Vc%9OQ5': 'O#$FD?t_j)' + 'jN|NAUP*h}': vwLrgavleL + 'jTNoKo}Bu+': 'AtMq%)YA4~' + 'jX=#G8wu#(': 'dy>%OTh=dS' + 'jYz[ibrr9m': 'H5FPagvP>_' + 'jcu!gLw/&r': 'Qw(O&Fnkul' + 'jrM28*HbyG': 'Lng7&qhm~G' + 'j|z@PMgdx,': 'umk_Ql/M`8' + 'k$1_yaF?9#': 'r]by!Z{bH3' + 'k/u1:B%DJH': 'uNyx=*R.$~' + 'k0{O[6l]bH': 'p(t;=_z4`=' + 'k1]FH+s8j@': 'ns:xfr`B.`' + 'k9E.p^Ki!j#v+': 'DN*^.Q~HcI' + 'l3Ly8PBxt(': 'A_Vjy~^@?}' + 'lAxH.CfJB@': 'mSo]}$5c7a*' + 'm*#%sE.`.;': 'o.l+o.aIv#' + 'm*PYE=#wr^': 'z#:5{OdBN]' + 'm2F|cUkKhv': 'G~@ufSz/a;' + 'm5j,b/y_CA/' + 'mk1VltJl<]': 'y8u|!Om27i' + 'mk20`,N=`=': 'MV[vr/8+{>' + 'mkgcU7[bA%': ']#3u-c%5X' + 'mq)8`hi{hb': 'co2;shq(5S' + 'mtd;>7T|g>': 'f;],oZor+L' + 'mz[>>AxIr4': 'O0NI(S,j2o' + 'm{noqE29%I': 'y;mRF(PX>0' + 'm{oM,jr>C#': 'i3#3D+l`w$' + 'n&>e`2FO/S': 'A%emXRt~a]' + 'n+t>2`:;P/': 'F*@Hxe1UYX' + 'n:7&>YT5A=': 'FFMu>hPw;u' + 'nIfRn/yz': 'rJf@3vMORX' + 'oa87&PB&Bm': 'w?},I|jwcN' + 'onMKID2d;D': 'cHz7UCnS&i' + 'o~}KVA:VbI': 'u9/j`LQwrv' + 'p#R$-mG/L': 'xAxtN`17x^' + 'p.yv>BQ470': 'IThTxqnu;|' + 'p:SPZ:e/*[': umTKoLZSfc + 'pDDrFhd%u': 'Cu].QU#a&l' + 'pYRw[=6;JI': 'yF,n2/p[P>' + 'p]t8:Tka7q': 'w?1B]|VvAn' + pkj3RPBUrF: 'FHqW?gqMKx' + 'pwwZu{7f~(': 'E9Y{~}fJ|q' + 'pz(G*g': 'QGDo{u)NuE' + 'q2#Yv[O0:|': 'p`7>hfkNfW' + 'q7do-wLx%E': 'HF8bKY!Wia' + 'q8][)Ri}=q': iyxxWaZ8Wb + 'q8fF$4,c6_': 'wAQL>iJ/-]' + 'q:XyGf#>Wg': 'ja9thWdG~?' + 'q=w[e/' + 'qx%>fB$d+$': 'b-3J|=Nm69' + 'r(h9&#wc~B': 'L}~TYYHI[l' + 'r/B|ra%L,.': 'CuDj}XI),:' + 'r5C646H*+*': 'gPWIY|dqgC' + 'r6BV%|..*5': kfOOt3pPdZ + 'raI-$$|wT': 'CJb&~c&tZv' + 'r>g<|,FWi%': 'Gh36nI3h)B' + 'r?E$4f-d0/': 'L~g[-Y5a.V' + 'rPekx@n9:L': 'bb1Lx^zyVu' + 'rSneU!3}bn': 'KfB$YBpp#=' + 'r]*Ioa%$bK': 'ltf#^eLgH=' + 's#:aIK$9^x': 'Cdm[YJ2i`t' + 's4E?ZF[E<>': 'rnHajUXn]|' + 's9-GL*@AXD': 'o6puwHE{j4' + 's<_WLSB3I/': vaSXyU,d,a + 's>f40rn,O]': 'f4q6u$5n-L' + 'sB7rZQsRf[SfCo' + 'sRw-ik:I$v': 'u:[~?k7{9`' + 'sSW/?WMy17': q1DvZ4Qkh, + 'sT5=H|@KU|': 'K6g6$oK|jN' + sXzvT.g31p: '`ePFuGBDX' + 'sY>@9w~i^p': 'r{g3sXk]Cm' + 'skOM(?n$GG': svVKQZTWFU + spKoFRM4if: 'Osy[q7a%UV' + 'ssbyA9}]QP': 'dl_kl*?R;X' + 'stRa/S(0$F': 'q@wPIm/;#)' + 'sxOMs+bE8@': 'sgq|,rEUT>' + 'sx|l+io`B@': 'HPtF]bu>P': 'qJP9,I/X;?' + 't2&ri|[Un:': 'Ek~&z$tb-I' + t3aZXtpNyi: 'Mmy:MH3p-5' + 't50y/[AEhF': 'CaSU3w{HJO' + 'tMMtBUpb$&': 'JS/Mu$zWXF' + 'tTnCoO/*tk': ':?*,_d`iM' + 't_h*fbN#:(': 'd_{5}`KOeV' + 't`cagaWApo': 'm$gS*RQqQ1' + 'tcneuK7v%`': 'w{[+/hi?P7' + tfCY4j6KEZ: 'oFFV~|R]U_' + 'tj&bK}%P!F': 'QG_m`Y6OnU' + 'twrhzJ[k:Z': 'qMOzrk<+(:' + 't|h3|;E4g{': 'hX`.aRn$&{' + 'u-`cspdp)D': 'B|v&Z,u=T' + 'u4^lH.xem5': 'm]_=p1zwik' + 'u;|vR#mVMW': 'L{DDezoz^' + 'uH*Q(mcRt5': 'C}D/)CbE1E' + 'uRo^w#p5Be': 'nlkynXE' + 'uc61ToT%gt': 'xc)G{&Ta22' + 'ui3>b-?a@m': 'J#u?@$XnR$' + upiX0g1quR: 'cM;m6U=Dg5' + 'uv1fGXHN@)': L5qdsn2.Vt + 'u|I}oElaH)': 'LG(/,IHQi@' + 'vE[T$`n9T)': 'CmEf8|#B?{' + 'vKx<0Qa!aP': 'P!2gMb,SH>' + 'vNiD6A.v#s': 'bsVR4>6gl0' + 'va/ky=f5pK': 'dnCkHdCc(#' + 'vf)G?[_?Pf': 'Q=UkoGw%47' + 'vgdj#X?8dB': 'n,~jhT?[]W' + 'vpOTK6tMr@': 'P[nc3.TLD`' + 'vppEA|$@~g': 'zX(|Q|9+|q' + 'vrn%j%6{nu': 'n^,sUM_!k_' + 'vwc3}.=Z#&': 'PcUr6zy#ql' + 'wa>{k!2cXc': 'Pz{%n/nX,I' + x2d4n4ADcP: kfniDM68Fr + 'xb&P*pH(Q': 'B,.i%oyEc%' + 'xqrJ^d[.(O': Mie3UMRyst + 'yX:tmrd5;]': 'j7FyQaW,n{' + 'z:*d{z(~V2': 'H1L?;^]mY]' + 'zSc]-^U=0=': 'f^0aH]F%!V' + crowdanki:uuid: + 43e2586a-9a65-11e8-a777-a0481cc15658: fd72d808-58d4-43ea-97db-24196747f24c diff --git a/fixtures/ultimate-geography/overlays/languages/it.yaml b/fixtures/ultimate-geography/overlays/languages/it.yaml new file mode 100644 index 0000000..727e025 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/it.yaml @@ -0,0 +1,711 @@ +id: overlay.translation.it +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': Esiste una disputa sul nome del mare tra i paesi confinanti, con la Corea del Sud che in particolare sostiene il nome di Mare orientale. + Abkhazia: Abcasia + Addis Ababa: Addis Abeba + Adriatic Sea: Mare Adriatico + Aegean Sea: Mar Egeo + Akrotiri and Dhekelia: Akrotiri e Dhekelia + Algiers: Algeri + Also known as Burma.: Conosciuta anche come Myanmar. + Also known as Cabo Verde.: '' + Also known as Czechia.: Conosciuta anche come Cechia. + Also known as East Timor.: Conosciuto anche come Timor-Leste. + Also known as Kiev.: '' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: Conosciuto anche come Golfo della Thailandia. + Also known as the Sea of Cortez.: Conosciuto anche come Mare di Cortez. + 'Also spelled as Sana''a.': '' + American Samoa: Samoa Americane + 'Andorra (narrower, coat of arms with motto)': 'Andorra (più stretta, stemma con motto)' + Antarctica: Antartide + Antigua and Barbuda: Antigua e Barbuda + Arabian Sea: Mar Arabico + Aral Sea: 'Lago d''Aral' + Arctic Ocean: Mar Glaciale Artico + Ashgabat: 'Aşgabat' + Athens: Atene + Atlantic Ocean: Oceano Atlantico + 'Australia (white stars, two more stars)': 'Australia (stelle bianche, due stelle in più)' + 'Austria (brighter red, wider white band)': 'Austria (rosso più chiaro, banda bianca più larga)' + Autonomous community of Spain.: 'Comunità autonoma della Spagna.' + Autonomous province of South Korea.: Provincia autonoma della Corea del Sud. + Autonomous region of Finland.: Regione autonoma della Finlandia. + Autonomous region of Italy.: Regione italiana a statuto speciale. + Autonomous region of Papua New Guinea.: Regione autonoma della Papua Nuova Guinea. + Autonomous region of Portugal.: Regione autonoma del Portogallo. + Azerbaijan: Azerbaigian + Azores: Azzorre + Bahrain: Bahrein + 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrein (più stretta, meno bordi seghettati, rossa)' + Balkan Peninsula: Penisola balcanica + Baltic Sea: Mar Baltico + Banda Sea: Mar di Banda + Barents Sea: Mare di Barents + Bay of Bengal: Golfo del Bengala + Bay of Biscay: Golfo di Biscaglia + Beijing: Pechino + Belarus: Bielorussia + Belgium: Belgio + Belgrade: Belgrado + Bering Strait: Stretto di Bering + Berlin: Berlino + Bern: Berna + Bishkek: 'Biškek' + Black Sea: Mar Nero + 'Bogotá': 'Bogotà' + 'Bolivia (coat of arms instead of star)': 'Bolivia (stemma al posto della stella)' + Bosnia and Herzegovina: Bosnia ed Erzegovina + 'Brasília': Brasilia + Brazil: Brasile + British Virgin Islands: Isole Vergini britanniche + Brussels: Bruxelles + Bucharest: Bucarest + Cairo: Il Cairo + Cambodia: Cambogia + Cameroon: Camerun + 'Cameroon (green/red/yellow, yellow star)': 'Camerun (verde/rosso/giallo, stella gialla)' + Canary Islands: Isole Canarie + Cape Verde: Capo Verde + Caribbean Sea: Mare Caraibico + Caspian Sea: Mar Caspio + Cayman Islands: Isole Cayman + Celebes Sea: Mare di Celebes + Celtic Sea: Mare Celtico + Central African Republic: Repubblica Centrafricana + Cetinje is an honorary capital.: 'Cettigne è una capitale onoraria.' + Chad: Ciad + 'Chad (slightly darker blue)': 'Ciad (blu leggermente più scuro)' + Chile: Cile + China: Cina + City of San Marino: 'Città di San Marino' + Claimed and controlled: Rivendicata e controllata + Claimed but not controlled: Rivendicata ma non controllata + 'Colombia (no coat of arms)': 'Colombia (senza stemma)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Colombo è spesso considerata la capitale ma Sri Jayawardenepura Kotte, un sobborgo di Colombo, è la capitale ufficiale e legislativa.' + Comoros: Comore + Constituent country in the Kingdom of Denmark.: + notes.note.faroe-islands.fields.field.country-info: 'Nazione costitutiva del Regno di Danimarca; conosciute anche come Isole Faroe.' + notes.note.greenland.fields.field.country-info: Nazione costitutiva del Regno di Danimarca. + Constituent country of the Kingdom of the Netherlands.: Nazione costitutiva del Regno dei Paesi Bassi. + Constituent country of the United Kingdom.: Nazione costitutiva del Regno Unito. + Cook Islands: Isole Cook + Copenhagen: Copenaghen + Coral Sea: Mar dei Coralli + Croatia: Croazia + Crown dependency of the United Kingdom.: Dipendenza della Corona britannica. + 'Cuba (red triangle, blue stripes)': 'Cuba (triangolo rosso, strisce blu)' + 'Curaçao (two stars in top-left corner)': 'Curaçao (due stelle nell''angolo in alto a sinistra)' + Cyprus: Cipro + Czech Republic: Repubblica Ceca + Damascus: Damasco + Dead Sea: Mar Morto + Democratic Republic of the Congo: Repubblica Democratica del Congo + Denmark: Danimarca + Denmark Strait: Stretto di Danimarca + Dhaka: Dacca + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': 'Contesa; rivendicata da Israele; Ramallah è il centro amministrativo.' + 'Disputed; claimed by Palestine.': 'Contesa; rivendicata dalla Palestina.' + Djibouti: Gibuti + Dominican Republic: Repubblica Dominicana + Dublin: Dublino + Dushanbe: 'Dušanbe' + East China Sea: Mar Cinese Orientale + East Siberian Sea: Mar della Siberia Orientale + 'Ecuador (with coat of arms)': 'Ecuador (con lo stemma)' + Edinburgh: Edimburgo + Egypt: Egitto + 'Egypt (emblem instead of text), Yemen (no text)': 'Egitto (emblema al posto del testo), Yemen (senza testo)' + 'Egypt (with emblem), Iraq (with text)': 'Egitto (con l''emblema), Iraq (con il testo)' + 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (stemma diverso, blu leggermente più scuro)' + England: Inghilterra + English Channel: La Manica + Equatorial Guinea: Guinea Equatoriale + Eswatini: eSwatini + Ethiopia: Etiopia + Europe: Europa + European Union: Unione europea + Falkland Islands: Isole Falkland + Faroe Islands: 'Fær Øer' + Federated States of Micronesia: Stati Federati di Micronesia + Fiji: Figi + Finland: Finlandia + Formerly Zaire.: In precedenza Zaire. + Formerly known as Macedonia.: Precedentemente conosciuta come Macedonia. + France: Francia + French Guiana: Guyana francese + French Polynesia: Polinesia francese + Germany: Germania + 'Ghana (star instead of coat of arms)': 'Ghana (stella al posto dello stemma)' + Gibraltar: Gibilterra + Greece: Grecia + Greenland: Groenlandia + Guadeloupe: Guadalupa + Guatemala City: 'Città del Guatemala' + 'Guinea (green and red flipped, slightly darker green)': 'Guinea (verde e rosso invertiti, verde leggermente più scuro)' + Gulf of Alaska: 'Golfo dell''Alaska' + Gulf of California: Golfo di California + Gulf of Carpentaria: Golfo di Carpentaria + Gulf of Guinea: Golfo di Guinea + Gulf of Mexico: Golfo del Messico + Gulf of Thailand: Golfo del Siam + Hargeisa: Hargheisa + Havana: 'L''Avana' + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: 'Regione storica e culturale dell''Europa settentrionale che comprende gli Stati di Danimarca, Norvegia e Svezia e talvolta anche Finlandia e Islanda.' + Hudson Bay: Baia di Hudson + Hungary: Ungheria + Iceland: Islanda + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Islanda (sfondo blu, croce rossa e bianca), Norvegia (sfondo rosso, croce blu e bianca)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Islanda (sfondo blu, croce rossa), Fær Øer (sfondo bianco, croce rossa e blu)' + Independent state claimed by Georgia.: Stato indipendente rivendicato dalla Georgia. + Independent state claimed by Moldova.: Stato indipendente rivendicato dalla Moldavia. + Independent state claimed by Somalia.: Stato indipendente rivendicato dalla Somalia. + Indian Ocean: Oceano Indiano + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesia (bianco e rosso invertiti, rosso più chiaro), Monaco (bianco e rosso invertiti, più stretta)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesia (più larga, rosso più chiaro), Polonia (rosso e bianco invertiti, più larga)' + 'Iraq (text instead of emblem), Yemen (no emblem)': 'Iraq (testo al posto dell''emblema), Yemen (senza emblema)' + Ireland: Irlanda + 'Ireland (orange and green flipped, wider)': 'Irlanda (arancione e verde invertiti, più larga)' + Island of Indonesia.: 'Isola dell''Indonesia.' + Isle of Man: Isola di Man + Israel: Israele + Italy: Italia + Ivory Coast: 'Costa d''Avorio' + 'Ivory Coast (green and orange flipped, narrower)': 'Costa d''Avorio (verde e arancione invertiti, più stretta)' + Jakarta: Giacarta + Jamaica: Giamaica + Japan: Giappone + Java: Giava + Jerusalem: Gerusalemme + Jordan: Giordania + Juba: Giuba + Kaliningrad Oblast: 'Oblast'' di Kaliningrad' + Kathmandu: Katmandu + Kazakhstan: Kazakistan + Khartoum: Khartum + Known as Nur-Sultan between 2019 and 2022: Conosciuta come Nur-Sultan tra il 2019 e il 2022 + Known as Swaziland until 2018.: Conosciuto come Swaziland fino al 2018. + Kuwait City: Al Kuwait + Kyiv: Kiev + Kyrgyzstan: Kirghizistan + Laayoune: 'El Aaiún' + Labrador Sea: Mare del Labrador + Latvia: Lettonia + 'Latvia (darker red, narrower white band)': 'Lettonia (rosso più scuro, banda bianca più stretta)' + Lebanon: Libano + Libya: Libia + Lisbon: Lisbona + Lithuania: Lituania + Ljubljana: Lubiana + London: Londra + Luxembourg: Lussemburgo + 'Luxembourg (lighter blue)': 'Lussemburgo (blu più chiaro)' + Luxembourg City: Lussemburgo + Macau: Macao + Madeira: Madera + Maldives: Maldive + 'Mali (red and green flipped, slightly brighter green)': 'Mali (rosso e verde invertiti, verde leggermente più chiaro)' + Marshall Islands: Isole Marshall + Martinique: Martinica + Mediterranean Sea: Mar Mediterraneo + Mexico: Messico + Mexico City: 'Città del Messico' + Mogadishu: Mogadiscio + Moldova: Moldavia + 'Moldova (wider, coat of arms with eagle)': 'Moldavia (più larga, stemma con un''aquila)' + Monaco: + notes.note.monaco.fields.field.capital: Comune di Monaco + notes.note.monaco.fields.field.country: Principato di Monaco + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (più stretta, rosso più scuro), Polonia (rosso e bianco invertiti, rosso più scuro)' + Morocco: Marocco + Moscow: Mosca + Mozambique: Mozambico + Muscat: Mascate + Myanmar: Birmania + 'Nauru (single star below yellow band)': 'Nauru (una sola stella sotto la banda gialla)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru non ha una capitale ufficiale; il Distretto di Yaren è la capitale de facto.' + Netherlands: Paesi Bassi + 'Netherlands (darker blue)': 'Paesi Bassi (blu più scuro)' + New Caledonia: Nuova Caledonia + New Delhi: Nuova Delhi + New Zealand: Nuova Zelanda + 'New Zealand (red stars, two fewer stars)': 'Nuova Zelanda (stelle rosse, due stelle in meno)' + 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (stemma diverso, blu leggermente più chiaro)' + North America: America del Nord + North Korea: Corea del Nord + North Macedonia: Macedonia del Nord + North Nicosia: Nicosia Nord + North Sea: Mare del Nord + Northern Cyprus: Cipro del Nord + Northern Ireland: Irlanda del Nord + Northern Mariana Islands: Isole Marianne Settentrionali + Norway: Norvegia + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norvegia (sfondo rosso, croce blu), Fær Øer (sfondo bianco, croce rossa e blu)' + Norwegian Sea: Mare di Norvegia + Not a sovereign country: 'Non è uno Stato sovrano' + 'Nouméa': Numea + 'Oblast (administrative region) of the Russian Federation.': 'Oblast'' (regione amministrativa) della Federazione Russa.' + Official capital was moved from Bujumbura to Gitega in 2019.: 'La capitale ufficiale è stata spostata da Bujumbura a Gitega nel 2019.' + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: 'La capitale ufficiale è stata spostata da Malabo a Ciudad de la Paz nel 2026.' + 'Officially Côte d''Ivoire.': 'Ufficialmente Côte d''Ivoire.' + Officially Luxembourg.: '' + Overseas department of France.: 'Dipartimento d''oltremare della Francia.' + Overseas territory of France.: 'Territorio d''oltremare della Francia.' + Overseas territory of the United Kingdom.: 'Territorio d''oltremare del Regno Unito.' + Pacific Ocean: Oceano Pacifico + Palestine: Palestina + 'Palestine (black/white/green, red arrow)': 'Palestina (nero/bianco/verde, triangolo rosso)' + 'Palestine (no symbol)': 'Palestina (nessun simbolo)' + Panama City: Panama + Papua New Guinea: Papua Nuova Guinea + Paris: Parigi + Partially recognised state claimed by China.: Stato parzialmente riconosciuto rivendicato dalla Cina. + Partially recognised state claimed by Morocco. Also known as Western Sahara.: 'Stato parzialmente riconosciuto rivendicato dal Marocco; conosciuta anche come Sahara Occidentale.' + Partially recognised state claimed by Serbia.: Stato parzialmente riconosciuto rivendicato dalla Serbia. + Persian Gulf: Golfo Persico + Peru: 'Perù' + Philippine Sea: Mare delle Filippine + Philippines: Filippine + Poland: Polonia + Polynesia: Polinesia + Portugal: Portogallo + Prague: Praga + Pretoria, Cape Town, Bloemfontein: 'Pretoria, Città del Capo, Bloemfontein' + Puerto Rico: Porto Rico + 'Puerto Rico (blue triangle, red stripes)': 'Porto Rico (triangolo blu, strisce rosse)' + 'Qatar (wider, more serrated edges, maroon)': 'Qatar (più larga, più bordi seghettati, rosso granata)' + Red Sea: Mar Rosso + Region of France.: Regione francese. + Republic of the Congo: Repubblica del Congo + Riyadh: Riad + 'Romania (slightly lighter blue)': 'Romania (blu leggermente più chiaro)' + Rome: Roma + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Russia (senza stemma), Slovenia (più larga, stemma più piccolo)' + Rwanda: Ruanda + 'Réunion': La Riunione + Sahrawi Arab Democratic Republic: Repubblica Democratica Araba dei Sahrawi + Saint Kitts and Nevis: Saint Kitts e Nevis + Saint Martin: Saint-Martin + Saint Vincent and the Grenadines: Saint Vincent e Grenadine + Sanaa: 'Sana''a' + Santiago: Santiago del Cile + Sardinia: Sardegna + Saudi Arabia: Arabia Saudita + Scotland: Scozia + Sea of Galilee: Lago di Tiberiade + Sea of Japan: Mar del Giappone + Sea of Okhotsk: Mare di Ochotsk + Semi-autonomous region of Tanzania.: Regione semi-autonoma della Tanzania. + 'Senegal (green/yellow/red, green star)': 'Senegal (verde/giallo/rosso, stella verde)' + Seoul: Seul + Sicily: Sicilia + Slovakia: Slovacchia + 'Slovakia (narrower, bigger coat of arms)': 'Slovacchia (più stretta, stemma più grande)' + 'Slovakia (with coat of arms)': 'Slovacchia (con lo stemma)' + Solomon Islands: Isole Salomone + South Africa: Sudafrica + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'Il Sudafrica non ha una capitale ufficiale: i poteri dello Stato sono suddivisi fra tre città: Pretoria (esecutivo), Città del Capo (legislativo) e Bloemfontein (giudiziario).' + South America: America meridionale + South China Sea: Mar Cinese Meridionale + South Korea: Corea del Sud + South Ossetia: Ossezia del Sud + South Sudan: Sudan del Sud + South Tarawa: Tarawa Sud + Southern Ocean: Oceano Antartico + Sovereign country: 'È uno Stato sovrano' + Spain: Spagna + Special Administrative Region of China.: Regione amministrativa speciale della Cina. + Sri Jayawardenepura Kotte: Sri Jayawardenapura Kotte + 'St. George''s': 'Saint George''s' + 'St. John''s': 'Saint John''s' + State of the United States.: Stato degli Stati Uniti. + State recognised only by Turkey and claimed by Cyprus.: Stato riconosciuto solamente dalla Turchia e rivendicato da Cipro. + Stockholm: Stoccolma + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Macroregione dell''Oceania che comprende migliaia di piccole isole nell''Oceano Pacifico centrale e meridionale.' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Macroregione dell''Oceania che comprende migliaia di piccole isole nell''Oceano Pacifico occidentale.' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Macroregione dell''Oceania che comprende Vanuatu, le Isole Salomone, le Figi e la Papua Nuova Guinea.' + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (rosso/bianco/nero, triangolo verde), Repubblica Democratica Araba dei Sahrawi (con stella e mezzaluna)' + Sweden: Svezia + Switzerland: Svizzera + 'Switzerland has no official capital; Bern is the de facto capital.': 'La Svizzera non ha una capitale ufficiale; Berna è la capitale de facto.' + Syria: Siria + 'São Tomé and Príncipe': 'São Tomé e Príncipe' + Tajikistan: Tagikistan + Tasman Sea: Mar di Tasman + Tehran: Teheran + Thailand: Thailandia + The Bahamas: Bahamas + The Gambia: Gambia + Timor Sea: Mar di Timor + Timor-Leste: Timor Est + Trinidad and Tobago: Trinidad e Tobago + Tunis: Tunisi + Turkey: Turchia + Turks and Caicos Islands: Turks e Caicos + Ukraine: Ucraina + Ulaanbaatar: Ulan Bator + Unincorporated internal area of Norway.: Area interna non incorporata della Norvegia. + Unincorporated territory of the United States.: Territorio non incorporato degli Stati Uniti. + United Arab Emirates: Emirati Arabi Uniti + United Kingdom: Regno Unito + United States Virgin Islands: Isole Vergini americane + United States of America: 'Stati Uniti d''America' + Valletta: La Valletta + Vatican City: 'Città del Vaticano' + Wales: Galles + Wallis and Futuna: Wallis e Futuna + Warsaw: Varsavia + Washington, D.C.: Washington + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: 'Nonostante Amsterdam sia la capitale ufficiale, L''Aia è la sede del governo e del parlamento.' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: 'Nonostante Dodoma sia la capitale ufficiale, Dar es Salaam è la sede del governo.' + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': 'Nonostante Laâyoune, conosciuta anche come El Aaiún, sia la capitale proclamata, Tifariti è la sede del governo.' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: 'Nonostante Mbabane sia la capitale ufficiale ed esecutiva, Lobamba è la capitale tradizionale, spirituale e legislativa.' + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: 'Nonostante Porto-Novo sia la capitale ufficiale, Cotonou è la sede del governo.' + While Sucre is the constitutional capital, La Paz is the seat of government.: 'Nonostante Sucre sia la capitale costituzionale, La Paz è la sede del governo.' + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: 'Nonostante Yamoussoukro sia la capitale ufficiale, Abidjan è la sede del governo.' + White Sea: Mar Bianco + World region covering the Australian continent and most of the islands in the Pacific Ocean.: 'Regione del mondo che comprende il continente Australiano e la maggior parte delle isole dell''Oceano Pacifico.' + Yellow Sea: Mar Giallo + Yerevan: Erevan + Zagreb: Zagabria + 'Åland Islands': 'Isole Åland' + additions: + notes.note.arctic-ocean.fields.field.country-info: Conosciuto a livello internazionale come Oceano Artico. + variables: + label.capital: + Capital: Capitale + label.capital-hint.answer: + 'Hint: {{Capital hint}}': 'Suggerimento: {{Capital hint}}' + label.flag: + Flag: Bandiera + label.location: + Location: Posizione + note-type.name: + Ultimate Geography: 'Ultimate Geography [IT]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': 'Bandiera simile a: {{Flag similarity}}.' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': 'pizs$6f^OI' + '5JJ2]i)p4': 's0^' + 'BJ,,^wMC%N': jEL_Gc0QOM + Bszsaz0Iuo: 'D.m%ueXh:9' + 'Cf2@;pN$}' + 'EWXNc:d^*' + 'H*c[ML+`s=': 'GPy,`vg]}<' + 'He!e=h?_2V': '}3cleuN,!' + 'J%,fuysl*&': 'p)2VM{]#8o' + 'JOT:U4ayh@': 'w3~iPHCWi@' + 'K*b}ui' + 'b^Try&me1A': 'D6%hiXB5%&' + 'b`5Q_Z=HQT': 'gxGHx4#O&s' + 'bd~Ght5tLR': sJ9gIjH3hX + 'bk2ByZIeU+': 'iNgR%2Eok/' + 'bnEK#)[;qT': 'q|%vIOl1;T' + 'c(,2{yXI#E': 'd+@.MPK' + 'cAlyc~G.y%': 'lMSAB/cOm*' + 'cEO^y)Md[G': 'h-`Y2EhgFQ' + 'cL?8!xP#Wj': 'z1POD,VID}' + 'cPO|Cru8n<': 'eqF,s$' + 'c^,~EC6Pb2': 'kecv+!in:(' + 'ck!6;xK$/^': 'lP>2[v``&g' + 'cv+Nq9:a>h!/' + 'e.t$Vi5,>f': 'KC^nKc-2%j' + 'e42BpV,P}C': 'KiGYV{Y2Rj' + 'e5P*D#I{m{': 'h(R&U@*wb}' + 'emB6@' + 'e?Xq9gy=@[': 'KA}n?y(.t6' + 'e?gIaPc$Ox': 'f+0nvKD]uC' + 'e?h=Xhs+K&': 'hh^x7B;l`6' + 'eALZe^HCHq': 'me^]a_a06D' + 'eHJ?)TAdTo': '{]0Gn}=?<' + 'eN#^kPGv*L': 'z3@H1&nZSg' + 'eTHH?U': 's#' + 'euW0jm=wfR': 'BB/C2B[$C5' + 'e{e8vi@^PY': 'lt:FGWJBSN' + 'f#4-L[mp5*': 'QoEJ+5vO&k' + 'f*2@qiHh_1': 'A6$z`mA%oA' + 'f*LO~>!0PN': 'DCq[ofnQ_C' + 'f21=-A~W;~': 'Q+px.mLe^.' + 'f>c7x2E]Q6': 'KW@PCgn4^d' + 'fRSM{K+;[w': 'H2dg': 'FdxY7GZlp$' + 'fV`&p81%d(': bLz53PbV0b + fkq7lhLkdH: 'KP*o@q}lR9' + 'fxAN),FAc$': 'cBH?yB-k-M' + 'g-]bb&[.E!': 'z)=(lc>D]!' + 'g9v~y/;=z(': 'gj:]O6VKC/' + 'g<[f89~U)A': 'GzIN2wS!UM' + 'gBCR?!O*;E': 'KBoJH0': 'NBAc+DUlR.' + 'gRVZ]qJ#>O': 'dGv_,!sUnF' + 'gYIoR`|AxW': 'B%fQaJUVdo' + 'gePC:Xt_nW': 'HuK?9aC_zy' + 'gp[h@]y13l': 'pQkua|dss!' + 'gsS{jR,]|Q': 'pU3#tTpq!l' + 'gx:;za!?C9': 'E[oI~&d)7T' + 'gz9${p}er*': 'MIvjS4ep`{' + 'g~%pF`(x{u': 'Ou<^C1eL+h' + 'h&`>n98~V1': 'q8/fZLX~{C' + 'h9Jr(H[=1~': 'O+JPzn{42?' + 'h?a*UKfT_M': 'fx]Qj`$n!<' + 'hE5j*e,Q?[': 'k^{fiJOiQK' + 'hNE9}>;Q&-': 'z+j0z0e&+>' + 'hXNTVF<:SH': mFunHjqPtu + 'hZ/V/D&rO`': 'L[wNzP!?1/' + 'h_9=mkZXAH': 'e,SBM4?I@u' + hoHandbYAy: 'M;TS86pSVW' + 'hrI_uiW>E]': 'dT$I;zW!R9' + 'h}lnSN_f$_': '/p,aOe!~L!' + 'iIS#Nw]tr]': 'x?#<3m(L/~c=': 'KDTitL&0A8' + 'i^J8&$(1]|': 'cXBHyX>[V$' + 'ia8as9sCY|': 'eA>XgY>alM' + 'igiK,ff[eO': 'eOOtP^l,3r' + 'ijP>aJBT!9': 'ySS-%<:X@%' + 'ilGtw#=asM': 's/XSB=3<_M' + 'j%*8gN)q>d': 'L0|q,v?fBC' + 'j)i)pB*HJ,': 'oi:nl?uhDf' + 'j2IQ=f=w3@': 'GD53>x$4(V' + j7eEZkzsCZ: 'i~F@r{J+u8' + 'j9:K,~DQ2v': 'q7>B#{/;(S' + 'jg|}': 'e9~liiy5)l' + 'jCd]`-=k,:': 'B8.@ls$HTO' + 'jDA-V?/hVj': 'OUlTl+:*GN' + jI9P-f6r3M: 'A}ER~W:Cla' + 'jKIDq|}J&c': 'zO#CeF9xFv' + 'jKO,9y0M;#': 'e[tzT?l#H0' + 'jN^Vc%9OQ5': 'OTBx@>`j]`' + 'jN|NAUP*h}': 't!i[6HG>P?' + 'jTNoKo}Bu+': 'Ay9!s9h9E(' + 'jX=#G8wu#(': 'xt)sOuFtn:' + 'jYz[ibrr9m': 'M`1yM!Pg1n' + 'jcu!gLw/&r': 'hf`]@|j!$I' + 'jrM28*HbyG': 'qVkX-]bkG' + 'j|z@PMgdx,': yt8CNByOZs + 'k$1_yaF?9#': 'z&vP452~E' + 'k/u1:B%DJH': '.nc9{|Rx|' + 'k0{O[6l]bH': 'ooH#i2V^L_' + 'k1]FH+s8j@': 'Q=Ng#q?dN`' + 'k9E.p^ZT>/O' + 'kQ:NIhJy~E': 'ICOm,PnZ)=' + 'kS)hDm%w}R': 'tZ&S#fk9|7' + 'kX]hN=_.c-': 'uxB==Gt:p4' + 'kZ52zV#[Iv': 'oRR~+%e`Hw' + 'k[)#L4.[v3': 'd?;n+GrH-G' + 'khB~(m^@Z5': 'FQ6}Cm%3$7' + 'kkbYQXw5b;': 'Q>X$_K%>.W' + 'koI(}OyJ:@': 'R2-j`41bE' + 'kopwzzB``A': 'vR)/{dF_/0' + 'k}4/oj#$~s': 'yd77K)R=;f' + 'k}A9]O:0xw': 'E;ZJ)^[Z5>W' + 'l*j*EE:!Y?': 'E#Q1{m2H6g' + 'l+>Ki!j#v+': rox57e9L,5 + 'l3Ly8PBxt(': 'wJqs(`?jY+' + 'lAxH.CfJB@': 'Qn=_I47LA%' + 'lH^2D]w;[H': 'Jt!e.x.0Gt' + 'lU@C3v4kdjJx,' + 'leh^fmrt*#': Eq6mfOsni + 'lhWI;$JC9(': 'kr~RoN}rC.' + libliptkB4: 'kHP%(:~L$T8P<9' + 'lpbD0St&OU': 'dd8@*P;!_=' + 'lq`T^ZBF2Y': 'oLrC[o>Ja;' + 'l}[BG9.nQS': 'nt{?s*~;(+' + 'm)WUtJS`H&': 'MD`pyb>HGd' + 'm*#%sE.`.;': 'IN2Vuk#F;>' + 'm*PYE=#wr^': 'dloL=[(^e?' + 'm2F|cUkKhv': 't[lzZGY3.3' + 'm5j,b/yO|' + mbGXc0y3Pe: 'ExARs/N{}X' + 'mbz=7n[0v9': 'l9wW=#1zk%' + 'mcc&0Kq^xE': 'w}zN{&/Ro;' + 'mi+kf0LTW.': 'K|jjS6T)93' + 'mk1VltJl<]': 'GQ/vj@?u/X' + 'mk20`,N=`=': 'KqNpX' + 'mq)8`hi{hb': 'PBogQaAw&>' + 'mtd;>7T|g>': 'QUFXkU|R}l' + 'mz[>>AxIr4': 'f/j>XFf+#$' + 'm{noqE29%I': 'Ji5yp3yF&;' + 'm{oM,jr>C#': 'H84|~MXE6}' + 'n&>e`2FO/S': 's$MWIX]7N%' + 'n+t>2`:;P/': 'OhS%m6wjrH' + 'n:7&>YT5A=': 'iUutTx[~f0' + 'nIfRn' + 'niJUFZF$Y:': 'i<%1$y^@*#' + 'nsS59c`VL,': 'j4J&;WUUw1.>g$w]' + 'o3VS!KWb/@': 'cH~ahGG4r(' + 'o9{x/yz': 'I^>KA)+sWg' + 'oa87&PB&Bm': 'F/Isk.P.[(' + 'onMKID2d;D': 'O_?zkb_dc:' + 'o~}KVA:VbI': 'Q^*!}l47T' + 'p#R$-mG/L': 'A[UOwi4j~%' + 'p.yv>BQ470': 'LkXz{lFlv{' + 'p:SPZ:e/*[': 'KTW_5-~ZIq' + 'pDDrFhd%u': 'r?X!/#D/zg' + 'pYRw[=6;JI': 'm{,@;#IB7t' + 'p]t8:Tka7q': 'GV6Rn!dmr!' + pkj3RPBUrF: 'rQN0=^O+GS' + 'pwwZu{7f~(': 'bA(w~2O9Xg' + 'pz(G*g': 'y7bRprmOE`' + 'q2#Yv[O0:|': 'dx;Wg': 'PO6ZA^*;M]' + 'q=w[r$se/Z0G' + 'qFuGdDx^Yv': 'f=`AJn~T:/' + 'qVi;t%/`F|': 'ds^tTE_]dX' + 'q[S4`,F0D=': 'LIu0Jpt[SQ' + 'q[}7hk(+=e': 'f,}O1xREjN' + 'qiv/4tOlf=': 'n<[|af68t>' + qvITjNxkLN: 'wJ}.y$;-Li' + 'qx%>fB$d+$': 'jb8Eh65j#1' + 'r(h9&#wc~B': 'g}SfA07quk' + 'r/B|ra%L,.': qhzCnw7SeK + 'r5C646H*+*': 'd=ke{fof`2' + 'r6BV%|..*5': 'k`%nD6rx=:XNFmp' + 'r>aI-$$|wT': 'r6rNn|YxO@' + 'r>g<|,FWi%': 'ra9YU$GwJi' + 'r?E$4f-d0/': d2lqq9EhWX + 'rPekx@n9:L': 'hU(8=s;Cch' + 'rSneU!3}bn': 'L5fYa(HY?&' + 'r]*Ioa%$bK': 'Q%7E!2lJ`R' + 's#:aIK$9^x': 'oI!OKr8': 'I///E!tK}`' + 's9-GL*@AXD': 'PMI(q1BU:[' + 's<_WLSB3I/': 'vpptc>@ky4' + 's>f40rn,O]': 'HN`?Pad@9w~i^p': 'L_j4?3vmv+' + 'skOM(?n$GG': 'o04NUTyQV$' + spKoFRM4if: 'qs*eI8U&Zf' + 'ssbyA9}]QP': 'Cd~]=K9h[S' + 'stRa/S(0$F': JHZDiQMPfb + 'sxOMs+bE8@': 'OKxBG0?#dm' + 'sx|l+io`B@': 'xF]bu>P': 'L^_?P(1YL[' + 't2&ri|[Un:': 'tfPkw@0;~]' + t3aZXtpNyi: 'ivB,61}=>D' + 't50y/[AEhF': 'KS=C;d>$qo' + 'tMMtBUpb$&': hqjCaPaeEZ + 'tTnCoO/*tk': 'IY+SvWsts^' + 't_h*fbN#:(': 'h!A$6,]|rX' + 't`cagaWApo': 'qsp+/:!:!W' + 'tcneuK7v%`': 'C4}ih73A=d' + tfCY4j6KEZ: 'u+Y]9-g]K~' + 'tj&bK}%P!F': 't6b-?a@m': 'kLhI3EPC)M' + upiX0g1quR: 'vPw=^$(CkP' + 'uv1fGXHN@)': 'kRnWGJyI$s' + 'u|I}oElaH)': 'jS!kn/5S}x' + 'vE[T$`n9T)': 'J$z:rG4h]?' + 'vKx<0Qa!aP': 'p:m=&i}/($' + 'vNiD6A.v#s': 'uD_r`#FV:D' + 'va/ky=f5pK': 'v=h^~bGQ1l' + 'vf)G?[_?Pf': 'Bety@6RI`M' + 'vgdj#X?8dB': 'C<4zOv=yp)' + 'vpOTK6tMr@': 'rq12?AX9S2' + 'vppEA|$@~g': 'o$aLe8WbC%' + 'vrn%j%6{nu': 'B(z0k%05eT' + 'vwc3}.=Z#&': 'Kd6N5U=|)z' + 'wa>{k!2cXc': 'jNpSzO@(B8' + x2d4n4ADcP: 'Cm$CS&[*Rg' + 'xb&P*pH(Q': 'Fs[o{go+4R' + 'xqrJ^d[.(O': 'cX>BQ?Y;e_' + 'yX:tmrd5;]': 'oylp@hqALI' + 'z:*d{z(~V2': 'gch|[3#I:X' + 'zSc]-^U=0=': 'Hq>2Y[(Y]*' + crowdanki:uuid: + 43e2586a-9a65-11e8-a777-a0481cc15658: cd65a274-7341-4e72-8de5-5d3d83ea4537 diff --git a/fixtures/ultimate-geography/overlays/languages/nb.yaml b/fixtures/ultimate-geography/overlays/languages/nb.yaml new file mode 100644 index 0000000..1b6c147 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/nb.yaml @@ -0,0 +1,705 @@ +id: overlay.translation.nb +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': '' + Abkhazia: Abkhasia + Addis Ababa: Addis Abeba + Adriatic Sea: Adriaterhavet + Aegean Sea: Egeerhavet + Africa: Afrika + Akrotiri and Dhekelia: Akrotiri og Dekelia + Algeria: Algerie + Algiers: Alger + Also known as Burma.: 'Også kalt Burma.' + Also known as Cabo Verde.: Offisielt Republikken Cabo Verde. + Also known as Czechia.: '' + Also known as East Timor.: '' + Also known as Kiev.: 'Også kjent som Kyiv.' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: 'Også kalt Siambukta.' + Also known as the Sea of Cortez.: '' + 'Also spelled as Sana''a.': '' + American Samoa: Amerikansk Samoa + 'Andorra (narrower, coat of arms with motto)': 'Andorra (smalere, våpen med valgspråk)' + Antarctica: Antarktis + Antigua and Barbuda: Antigua og Barbuda + Arabian Sea: Det arabiske hav + Aral Sea: 'Aralsjøen' + Arctic Ocean: Nordishavet + Ashgabat: Asjkhabad + Athens: Aten + Atlantic Ocean: Atlanterhavet + 'Australia (white stars, two more stars)': 'Australia (hvite stjerner og to ekstra stjerner)' + Austria: 'Østerrike' + 'Austria (brighter red, wider white band)': 'Østerrike (lysere rød, brede hvit stripe)' + Autonomous community of Spain.: 'Selvstyrt område i Spania.' + Autonomous province of South Korea.: 'Selvstyrt provins i Sør-Korea.' + Autonomous region of Finland.: 'Selvstyrt og delimilitarisert øygruppe og landskap i Finland.' + Autonomous region of Italy.: Delvis selvstyrt region i Italia. + Autonomous region of Papua New Guinea.: Selvstyrt region i Papua Ny-Guinea. + Autonomous region of Portugal.: Selvstyrt region i Portugal. + Azerbaijan: Aserbajdsjan + Azores: Asorene + Baghdad: Bagdad + 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrain (smalere, færre tagger, rødt)' + Balkan Peninsula: Balkan + Baltic Sea: 'Østersjøen' + Banda Sea: 'Bandasjøen' + Barents Sea: Barentshavet + Bay of Bengal: Bengalbukta + Bay of Biscay: Biscayabukta + Belgium: Belgia + Belgrade: Beograd + Bering Strait: Beringstredet + Bishkek: Bisjkek + Black Sea: Svartehavet + 'Bolivia (coat of arms instead of star)': 'Bolivia (våpenskjold i stedet for stjerne)' + Bosnia and Herzegovina: Bosnia-Hercegovina + 'Brasília': Brasilia + Brazil: Brasil + British Virgin Islands: 'De britiske Jomfruøyer' + Brussels: Brussel + Bucharest: Bucuresti + Cairo: Kairo + Cambodia: Kambodsja + Cameroon: Kamerun + 'Cameroon (green/red/yellow, yellow star)': 'Kamerun (grønn/rød/gul, gul stjerne)' + Canary Islands: 'Kanariøyene' + Cape Verde: Kapp Verde + Caribbean Sea: Det karibiske hav + Caspian Sea: Det kaspiske hav + Cayman Islands: 'Caymanøyene' + Celebes Sea: 'Celebessjøen' + Celtic Sea: Det keltiske hav + Central African Republic: Den sentralafrikanske republikk + Cetinje is an honorary capital.: Administrativ hovedstad. Cetinje er historisk hovedstad og presidentens tilholdssted. + Chad: Tsjad + 'Chad (slightly darker blue)': 'Tsjad (noe mørkere blå)' + China: Kina + City of San Marino: San Marino by + Claimed and controlled: Krevd og kontrollert + Claimed but not controlled: Krevd, men ikke kontrollert + 'Colombia (no coat of arms)': 'Colombia (uten våpenskjold)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Regjeringen har sete i Colombo. Sri Jayawardenepura Kotte (kalt Kotte) er en forstad til Colombo der nasjonalforsamlingen har sete. Kotte er altså lovgivende hovedstad.' + Comoros: Komorene + Constituent country in the Kingdom of Denmark.: 'Land som utgjør en del av kongeriket Danmark.' + Constituent country of the Kingdom of the Netherlands.: + notes.note.aruba.fields.field.country-info: '«Land» som sammen med Nederland, Curaçao og Sint Maarten utgjør føderasjonen Kongeriket Nederlandene.' + notes.note.cura-ao.fields.field.country-info: '«Land» som sammen med Nederland, Aruba og Sint Maarten utgjør føderasjonen Kongeriket Nederlandene.' + notes.note.sint-maarten.fields.field.country-info: '«Land» som sammen med Nederland, Curaçao og Aruba utgjør føderasjonen Kongeriket Nederlandene.' + Constituent country of the United Kingdom.: 'Land som utgjør en del av Storbritannia.' + Cook Islands: 'Cookøyene' + Copenhagen: 'København' + Coral Sea: Korallhavet + Corsica: Korsika + Croatia: Kroatia + Crown dependency of the United Kingdom.: + notes.note.guernsey.fields.field.country-info: 'Kanaløy underlagt britisk kronbesittelse.' + notes.note.isle-of-man.fields.field.country-info: 'Også kalt Isle of Man. Kanaløy underlagt britisk kronbesittelse. Hverken del av Storbritannia eller EU.' + notes.note.jersey.fields.field.country-info: 'Kanaløy underlagt britisk kronbesittelse.' + 'Cuba (red triangle, blue stripes)': 'Cuba (rød trekant, blå striper)' + 'Curaçao (two stars in top-left corner)': 'Curaçao (to stjerner i øverste venstre hjørne)' + Cyprus: Kypros + Czech Republic: Tsjekkia + Damascus: Damaskus + Dead Sea: 'Dødehavet' + Democratic Republic of the Congo: Kongo + Denmark: Danmark + Denmark Strait: Danmarkstredet + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': 'Omstridt; hevdet av Israel; Ramallah er administrasjonssenteret.' + 'Disputed; claimed by Palestine.': 'Omstridt; hevdet av Palestina.' + Dominican Republic: Den dominikanske republikk + Dushanbe: Dusjanbe + East China Sea: 'Øst-Kina-havet' + East Siberian Sea: 'Øst-Sibirhavet' + 'Ecuador (with coat of arms)': 'Ecuador (med våpenskjold)' + 'Egypt (emblem instead of text), Yemen (no text)': 'Jemen (blank midstripe), Egypt (emblem)' + 'Egypt (with emblem), Iraq (with text)': 'Egypt (emblem), Irak (tekst)' + 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (annerledes våpenskjold, noe mørkere blå)' + English Channel: Den engelske kanal + Equatorial Guinea: Ekvatorial-Guinea + Estonia: Estland + Ethiopia: Etiopia + Europe: Europa + European Union: Den europeiske union + Falkland Islands: 'Falklandsøyene' + Faroe Islands: 'Færøyene' + Federated States of Micronesia: 'Mikronesiaføderasjonen' + Formerly Zaire.: 'Offisielt Den demokratiske republikken Kongo. Tidligere Zaïre.' + Formerly known as Macedonia.: Tidligere Makedonia. + France: Frankrike + French Guiana: Fransk Guyana + French Polynesia: Fransk Polynesia + Germany: Tyskland + 'Ghana (star instead of coat of arms)': 'Ghana (stjerne i stedet for våpenskjold)' + Greece: Hellas + Greenland: 'Grønland' + Guatemala City: Guatemala by + 'Guinea (green and red flipped, slightly darker green)': 'Guinea (byttet plass på grønt og rødt, noe mørkere grønnfarge)' + Gulf of Alaska: Alaskabukta + Gulf of California: Californiabukta + Gulf of Carpentaria: Carpentariabukta + Gulf of Guinea: Guineabukta + Gulf of Mexico: Mexicogolfen + Gulf of Thailand: Thailandbukta + Havana: Havanna + Helsinki: Helsingfors + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: Skandinavia betegner Sverige, Danmark og Norge. Norden inkluderer nabolandene Finland og Island. + Hong Kong: Hongkong + Hudson Bay: Hudsonbukta + Hungary: Ungarn + Iceland: Island + '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)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Island (blå bakgrunn, rødt kors), Færøyene (hvit bakgrunn, rødt og blått kors)' + Independent state claimed by Georgia.: Utbryterstat i Georgia. + Independent state claimed by Moldova.: 'Region i Moldova som er en selverklært, men ikke internasjonalt anerkjent, stat.' + Independent state claimed by Somalia.: Utbryterrepublikk i Somalia. Fungerer som selvstendig stat, men ikke anerkjent av andre. + Indian Ocean: Det indiske hav + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesia (rødt og hvitt byttet, lysere rød), Monaco (rødt og hvitt byttet, smalere)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesia (bredere, lysere rød), Polen (rødt og hvitt byttet, bredere)' + Iraq: Irak + 'Iraq (text instead of emblem), Yemen (no emblem)': 'Jemen (blank midstripe), Irak (tekst)' + Ireland: Irland + 'Ireland (orange and green flipped, wider)': 'Ireland (byttet plass på oransje og grønt, bredere)' + Island of Indonesia.: 'Øy i Indonesia.' + Isle of Man: Man + Italy: Italia + Ivory Coast: Elfenbenskysten + 'Ivory Coast (green and orange flipped, narrower)': 'Elfenbenskysten (grønn og oransj har byttet plass, smalere)' + Kaliningrad Oblast: Kaliningrad oblast + Kathmandu: Katmandu + Kazakhstan: Kasakhstan + Known as Nur-Sultan between 2019 and 2022: Kjent som Nursultan mellom 2019 og 2022 + Known as Swaziland until 2018.: 'Før 2018 kjent som Swaziland.' + Kuwait City: Kuwait by + Kyiv: Kiev + Kyrgyzstan: Kirgisistan + Laayoune: 'El Aaiún' + Labrador Sea: 'Labradorsjøen' + 'Latvia (darker red, narrower white band)': 'Latvia (mørkere rød, smalere hvitt bånd)' + Lebanon: Libanon + Lisbon: Lisboa + Lithuania: Litauen + 'Luxembourg (lighter blue)': 'Luxembourg (lysere blå)' + Luxembourg City: Luxembourg + Macau: Macao + Madagascar: Madagaskar + Maldives: Maldivene + 'Mali (red and green flipped, slightly brighter green)': 'Mali (byttet plass på rødt og grønt; noe lysere grønn)' + Marshall Islands: 'Marshalløyene' + Mediterranean Sea: Middelhavet + Mexico City: Mexico by + Micronesia: Mikronesia + 'Moldova (wider, coat of arms with eagle)': 'Moldova (bredere, våpenskjold med ørn)' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (smalere, mørkere rød), Polen (rødt og hvitt byttet, mørkere rød)' + Morocco: Marokko + Moscow: Moskva + Mozambique: Mosambik + 'N''Djamena': Ndjamena + 'Nauru (single star below yellow band)': 'Nauru (en enkelt stjerne under et gult bånd)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru har ingen offisiell hovedstad; Yaren er regjeringssete.' + Netherlands: Nederland + 'Netherlands (darker blue)': 'Nederland (mørkere blå)' + New Caledonia: Ny-Caledonia + 'New Zealand (red stars, two fewer stars)': 'New Zealand (røde stjerner)' + 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (annerledes våpenskjold, noe lysere blå)' + Nicosia: Nikosia + North America: Nord-Amerika + North Korea: Nord-Korea + North Macedonia: Nord-Makedonia + North Nicosia: Nord-Nikosia + North Sea: 'Nordsjøen' + Northern Cyprus: Nord-Kypros + Northern Ireland: Nord-Irland + Northern Mariana Islands: Nord-Marianene + Norway: Norge + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norge (rød bakgrunn, blått kors), Færøyene (hvit bakgrunn, rødt og blått kors)' + Norwegian Sea: Norskehavet + Not a sovereign country: Ikke selvstendig land + 'Nukuʻalofa': Nukualofa + 'Oblast (administrative region) of the Russian Federation.': 'Oblast (administrativt distrikt) i Russland.' + Oceania: Oseania + Official capital was moved from Bujumbura to Gitega in 2019.: 'Hovedstad flyttet i februar 2019. Bujumbura er fortsatt regjeringssete og økonomisk hovedstad.' + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: Hovedstad flyttet til Ciudad de la Paz fra Malabo i 2026. + 'Officially Côte d''Ivoire.': '' + Officially Luxembourg.: '' + Overseas department of France.: + notes.note.french-guiana.fields.field.country-info: 'Fransk oversjøisk departement og del av EU.' + notes.note.guadeloupe.fields.field.country-info: 'Fransk oversjøisk departement.' + notes.note.martinique.fields.field.country-info: 'Fransk oversjøisk departement.' + notes.note.mayotte.fields.field.country-info: 'Fransk oversjøisk territorium.' + notes.note.r-union.fields.field.country-info: 'Fransk oversjøisk departement.' + Overseas territory of France.: + notes.note.french-polynesia.fields.field.country-info: 'Fransk oversjøisk land.' + notes.note.new-caledonia.fields.field.country-info: 'Fransk oversjøisk territorium i det sørvestlige Stillehavet.' + notes.note.saint-martin.fields.field.country-info: 'Fransk oversjøisk land.' + notes.note.wallis-and-futuna.fields.field.country-info: 'Fransk oversjøisk land.' + Overseas territory of the United Kingdom.: + notes.note.akrotiri-and-dhekelia.fields.field.country-info: 'Britisk oversjøisk territorium.' + notes.note.anguilla.fields.field.country-info: 'Selvstyrt britisk oversjøisk territorium i Karibia.' + notes.note.bermuda.fields.field.country-info: 'Selvstyrt britisk oversjøisk territorium.' + notes.note.british-virgin-islands.fields.field.country-info: 'Selvstyrt britisk oversjøisk territorium i Karibia.' + notes.note.cayman-islands.fields.field.country-info: 'Selvstyrt britisk oversjøisk territorium i Karibia.' + notes.note.falkland-islands.fields.field.country-info: 'Britisk øygruppe.' + notes.note.gibraltar.fields.field.country-info: Britisk kronkoloni. + notes.note.turks-and-caicos-islands.fields.field.country-info: 'Selvstyrt britisk oversjøisk territorium i Karibia.' + Pacific Ocean: Stillehavet + Palestine: Palestina + 'Palestine (black/white/green, red arrow)': 'Palestina (svart/hvit/grønn, rød trekant)' + 'Palestine (no symbol)': 'Palestina (uten symbol)' + Panama City: Panama by + Papua New Guinea: Papua Ny-Guinea + Partially recognised state claimed by China.: 'Offisielt Republikken Kina. Folkerepublikken Kina gjør også krav på områdene.' + Partially recognised state claimed by Morocco. Also known as Western Sahara.: 'Delvis anerkjent stat som Marokko gjør krav på.' + Partially recognised state claimed by Serbia.: 'Delvis anerkjent stat som Serbia gjør krav på.' + Persian Gulf: Persiabukta + Philippine Sea: Filippinerhavet + Philippines: Filippinene + Poland: Polen + Port Vila: Vila + Prague: Praha + 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blå trekant, røde striper)' + 'Qatar (wider, more serrated edges, maroon)': 'Qatar (bredere, flere tagger og jordbrun farge)' + Red Sea: 'Rødehavet' + Region of France.: Fransk region. + Republic of the Congo: Kongo-Brazzaville + 'Romania (slightly lighter blue)': 'Romania (noe lysere blå)' + Rome: Roma + Russia: Russland + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Russland (uten våpenskjold), Slovenia (bredere, mindre våpenskjold)' + Sahrawi Arab Democratic Republic: Vest-Sahara + Saint Kitts and Nevis: Saint Kitts og Nevis + Saint Martin: Saint-Martin + Saint Vincent and the Grenadines: Saint Vincent og Grenadinene + Sanaa: Sana + Santiago: Santiago de Chile + Saudi Arabia: Saudi-Arabia + Scandinavia: Skandinavia og Norden + Scotland: Skottland + Sea of Galilee: 'Gennesaretsjøen' + Sea of Japan: Japanhavet + Sea of Okhotsk: Okhotskhavet + Semi-autonomous region of Tanzania.: Delvis selvstendig stat i Tanzania. + 'Senegal (green/yellow/red, green star)': 'Senegal (grønn/gul/rød, grønn stjerne)' + Seychelles: Seychellene + Sicily: Sicilia + 'Slovakia (narrower, bigger coat of arms)': 'Slovakia (smalere, større våpenskjold)' + 'Slovakia (with coat of arms)': 'Slovakia (med våpenskjold)' + Solomon Islands: 'Salomonøyene' + South Africa: 'Sør-Afrika' + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'Sør-Afrika har ingen fastsatt hovedstad; makten er fordelt på tre byer: Pretoria (utøvende), Cape Town (lovgivende) og Bloemfontein (dømmende).' + South America: 'Sør-Amerika' + South China Sea: 'Sør-Kina-havet' + South Korea: 'Sør-Korea' + South Ossetia: 'Sør-Ossetia' + South Sudan: 'Sør-Sudan' + Southern Ocean: 'Sørishavet' + Sovereign country: Selvstendig land + Spain: Spania + Special Administrative Region of China.: + notes.note.hong-kong.fields.field.country-info: 'Også kjent som Hong Kong. Spesiell administrativ region i Kina.' + notes.note.macau.fields.field.country-info: 'Også kalt Macau. Spesiell administrativ region i Kina.' + Sri Jayawardenepura Kotte: Colombo + 'St. John''s': 'Saint John’s' + State of the United States.: USAs delstat. + State recognised only by Turkey and claimed by Cyprus.: 'Selverklært øyrepublikk på Kypros som bare Tyrkia anerkjenner.' + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Område i det sentrale og sydlige Stillehavet som inneholder tusener av små øyer.' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Område i det vestlige Stillehavet som inneholder tusenvis av små øyer.' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Område i Stillehavet. Inneholder Vanuatu, Salomonøyene, Fiji og Papua Ny-Guinea.' + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (rød/hvit/svart, grønn trekant), Vest-Sahara (med stjerne og halvmåne)' + Suriname: Surinam + Sweden: Sverige + Switzerland: Sveits + 'Switzerland has no official capital; Bern is the de facto capital.': Sveits har ingen offisiell hovedstad. Bern er regjeringssete. + 'São Tomé and Príncipe': 'São Tomé og Príncipe' + Tajikistan: Tadsjikistan + Tashkent: Tasjkent + Tasman Sea: Tasmanhavet + Tehran: Teheran + The Bahamas: Bahamas + The Gambia: Gambia + Thimphu: Thimpu + Timor Sea: 'Timorsjøen' + Timor-Leste: 'Øst-Timor' + Trinidad and Tobago: Trinidad og Tobago + Turkey: Tyrkia + Turks and Caicos Islands: 'Turks- og Caicosøyene' + Ukraine: Ukraina + Unincorporated internal area of Norway.: 'Norsk øygruppe.' + Unincorporated territory of the United States.: Uinnlemmet territorium i USA. + United Arab Emirates: De forente arabiske emirater + United Kingdom: Storbritannia + United States Virgin Islands: 'De amerikanske Jomfruøyer' + United States of America: USA + Uzbekistan: Usbekistan + Vatican City: Vatikanstaten + Vienna: Wien + Wallis and Futuna: Wallis og Futuna + Warsaw: Warszawa + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: Nasjonalforsamling og regjering i Haag. + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: Dodoma er offisiell hovedstad, mens Dar es Salaam er regjeringssete. + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': '' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: 'Mbabane offisiell, utøvende hovedstad, mens Lobamba er tradisjonell, åndelig og lovgivende hovedstad.' + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: Porto-Novo er offisiell hovedstad, mens Cotonou er de facto regjeringssete. + While Sucre is the constitutional capital, La Paz is the seat of government.: Sucre er offisiell hovedstat, mens La Paz er regjeringssete. + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: Yamoussoukro er offisiell hovedstad, mens Abidjan er de facto regjeringssete. + White Sea: 'Kvitsjøen' + World region covering the Australian continent and most of the islands in the Pacific Ocean.: 'Verdensdel som omfatter det australske kontinentet og de fleste øyene i Stillehavet.' + Yellow Sea: Gulehavet + Yemen: Jemen + Yerevan: Jerevan + 'Åland Islands': 'Åland' + additions: + notes.note.arabian-sea.fields.field.country-info: 'Også kalt Arabiahavet.' + notes.note.balkan-peninsula.fields.field.country-info: 'Også kalt Balkanhalvøya.' + notes.note.baltic-sea.fields.field.country-info: 'Også kalt Det baltiske hav.' + notes.note.belarus.fields.field.country-info: 'Tidligere kjent som Hviterussland. Utenriksdepartementet begynte å bruke navnet Belarus 29. mai 2022.' + notes.note.china.fields.field.country-info: Offisielt Folkerepublikken Kina. + notes.note.finland.fields.field.capital-info: 'Det finske navnet er Helsinki, men Utenriksdepartementet i Norge anbefaler å bruke byens svenske navn.' + notes.note.greenland.fields.field.capital-info: 'På dansk Godthåb.' + notes.note.indian-ocean.fields.field.country-info: 'Også kalt Indiahavet.' + notes.note.kiribati.fields.field.capital-info: 'Mange regner Bairiki som Kiribatis hovedstad (også Utenriksdepartementet i Norge) fordi både parlamentet og presidenten en gang hadde sete der. Siden år 2000 har parlamentet og departementer blitt spredd utover Syd-Tarawa.' + notes.note.mongolia.fields.field.capital-info: 'Også kjent som Ulan Bator.' + notes.note.myanmar.fields.field.capital-info: 'Hovedstad flyttet til Naypyidaw (Nay Pyi Taw) fra Yangon (tidl. Rangoon) i 2005.' + notes.note.persian-gulf.fields.field.country-info: 'Også kalt Den persiske bukt, Persiagolfen, Den persiske gulf og Den arabiske bukt.' + notes.note.republic-of-the-congo.fields.field.country-info: Offisielt Republikken Kongo. + notes.note.sea-of-galilee.fields.field.country-info: 'Også kalt Galileasjøen, Tiberiassjøen eller Ginossarsjøen.' + notes.note.turkmenistan.fields.field.capital-info: 'Også kjent som Asjgabat.' + notes.note.united-kingdom.fields.field.country-info: 'Også kalt Det forente kongeriket Storbritannia og Nord-Irland.' + notes.note.united-states-of-america.fields.field.country-info: 'Også kjent som De forente stater eller Sambandsstatene.' + notes.note.white-sea.fields.field.country-info: 'Også kalt Hvitehavet.' + variables: + label.capital: + Capital: Hovedstad + label.flag: + Flag: Flagg + label.location: + Location: Beliggenhet + note-type.name: + Ultimate Geography: 'Ultimate Geography [NB]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': 'Flagg som ligner på {{Flag similarity}}.' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': 'G8%+Eg:Na<' + '5JJ2]i)p4': 'E([1*B$wFU' + '5S(f7%O-$': 'Dh89q.%Zv<' + '?n!)c/h=Q': 'e7w}$Wx7yb' + 'AES],s:8=': 'd55~I-Znw' + 'O~pXkAbd3': 'FCE{oF+@St' + 'QSq%~rFaP-': 'yLhZzEnGK&' + 'Qlib4)q2Hk': 'Cay~/DU6[Y' + 'SlOEXfq#|': 'F[7BR94HX^' + 'W5?6yQZD2': 'u#*HO9?|?]' + '_]DH+g9!$': 'jH=<{Pcl+D' + 'b@M[XTc?}5': 'k,Tmj~Pw.i' + 'bD?R}i8_[c': 'J[Z3=;n.`R' + 'bW)ch:vs1P': 'tt:.S?c/]j' + 'b[(2`4t&?J': 'FMj}H;knqq' + 'b^Try&me1A': 'Dd.ED,HOi&' + 'b`5Q_Z=HQT': 'Gf4fmYBN)X' + 'bd~Ght5tLR': 'DO-=AqS[Lk' + 'bk2ByZIeU+': 'n`P0w@?q1u' + 'bnEK#)[;qT': r8MKfCfg2E + 'c(,2{yXI#E': '>1ErI|!*U' + 'c,5(mUGzYT': 'G#LLy[.iS2' + 'cAlyc~G.y%': 'kQssZ~b%|u' + 'cEO^y)Md[G': 'IHD#^9>(oF' + 'cL?8!xP#Wj': 'm>7j+&89xI' + 'cPO|Cru8n<': 'rymWNfd[Q[' + 'cV4idO^r8W': 'G+c6-zDX5@' + 'cYKvF#ptF7': 'b<&&OM3fYy' + 'c^,~EC6Pb2': 'nJdfBSsZq=' + 'ck!6;xK$/^': 'E2:8?ZCW`=' + 'cv+Nq9:aV@}^.' + 'de=9c:,@j9': 'AJ/nnIR/[0' + 'dlU7VG|3Gd': 'i?3Cq]Y=6@' + 'd}R:qHj1,7': 'v/?czq&41s' + 'e!KSebP}pt': 't`w^xuFUVp' + 'e+/O]%*qfk': 'iR~,tG{Val' + 'e.t$Vi5,>f': 'i3+,TcHF,N' + 'e42BpV,P}C': 'D+L%7xa1|K' + 'e5P*D#I{m{': 'ml7O4E=o<>' + 'egv' + 'e?h=Xhs+K&': 'P}MIWKA{Xz' + 'eALZe^HCHq': 'vy^=B$6h^Y' + 'eHJ?)TAdTo': 'w[pneW3d#g' + 'eN#^kPGv*L': 'Bc^_k/M|{V' + 'eTHH?U': lQbPlI/gwj + 'euW0jm=wfR': 'H9A(m7f1)9' + 'e{e8vi@^PY': 'c^iV73&0V(' + 'f#4-L[mp5*': 'M}OReg^Eem' + 'f*2@qiHh_1': Fe.owPPLXj + 'f*LO~>!0PN': 'I{.D^{85=#' + 'f21=-A~W;~': 'neb-=f&6r^' + 'f>c7x2E]Q6': 'dn}H9%A|!j' + 'fRSM{K+;[w': 'z7wQ8s_Si[' + 'fT!w8R#>dg': 'zv;#VKpiyA' + 'fV`&p81%d(': 'hH0': 'msi&z%jb~L' + 'gRVZ]qJ#>O': 'GkU[4wRj6;' + 'gYIoR`|AxW': 'bKW#GK' + 'gx:;za!?C9': 'K>Dfm{ad5K' + 'gz9${p}er*': 'l$1j[S4Z_w' + 'g~%pF`(x{u': 'PQ6$bI??y6' + 'h&`>n98~V1': 'G5wA};7d`m' + 'h9Jr(H[=1~': KYSHLMcczM + 'h?a*UKfT_M': 'BqW}>:ZSdW' + 'hE5j*e,Q?[': 'Bq5H0+i]XW' + 'hNE9}>;Q&-': 'D_u6CIg+wo' + 'hXNTVF<:SH': 'fxvMT{|_3M' + 'hZ/V/D&rO`': 'l!=SCi?>l?' + 'h_9=mkZXAH': 'cJ%(LRs3)q' + hoHandbYAy: 'B4sHDjU:Pl' + 'hrI_uiW>E]': 'p]3f+m(L/~c=': 'jKTnJ!T6lc' + 'i^J8&$(1]|': 'q%;R^5+|lP' + 'ia8as9sCY|': 'vxq=QA:/`z' + 'igiK,ff[eO': 'l|kNJq}f(%' + 'ijP>aJBT!9': 'dV#R#|}uZH' + 'ilGtw#=asM': 'fWbYL=>/W&' + 'j%*8gN)q>d': 'N(,;S#&N9e' + 'j)i)pB*HJ,': 'l[H_#9Xj.5' + 'j2IQ=f=w3@': 'C18`2)zTm(' + j7eEZkzsCZ: 'rQ)Q-EabYy' + 'j9:K,~DQ2v': 'HbTQZU8t9?' + 'jg|}': 'cnUAu,2#H.@>i' + 'jN^Vc%9OQ5': 'orHoXjp' + 'k/u1:B%DJH': 'G$NEO(W.(v' + 'k0{O[6l]bH': 'xL^i{$Dd?}' + 'k1]FH+s8j@': 'k4o56u=}jw' + 'k9E.p^H' + 'khB~(m^@Z5': 'M,j]%f~qLY' + 'kkbYQXw5b;': 'w`{+uGys`~' + 'koI(}OyJ:@': 'zH)c,C{V#j' + 'kopwzzB``A': 'l,n6{OL+@0' + 'k}4/oj#$~s': 'efA[V?^$DO' + 'k}A9]O:0xw': 'n{xEq+_:c{' + 'l$2O|9w`F~': dmsIj2JJlB + 'l$zM~ihPFE': 'eX=4L.MZ/)' + 'lI)Ah/E': 'zv#Fyq9;?1' + 'l*j*EE:!Y?': 'n%Pw}}Ki!j#v+': 'fLs!R5^k7e' + 'l3Ly8PBxt(': 'm<$SD/W~so' + 'lAxH.CfJB@': 'DNXY<9~9OH' + 'lH^2D]w;[H': 'qpx1j<_-l5' + 'lU@C37T|g>': 'ya?+!BD>AxIr4': 'g3SU#SH>>e' + 'm{noqE29%I': 'oK)C745Je~' + 'm{oM,jr>C#': 'zJ%%Hz#s9&' + 'n&>e`2FO/S': 'svzWf$x!8;' + 'n+t>2`:;P/': 'dSwjDNnc7[' + 'n:7&>YT5A=': 'A|BG)32=10' + 'nIfRn(_#b' + 'o9{x/yz': 'J3)Ty17a?Q' + 'oa87&PB&Bm': 'Gc>|URJcg|' + 'onMKID2d;D': 'L1&JVH|{R,' + 'o~}KVA:VbI': qqJpIWfqFB + 'p#R$-mG/L': 'C+sW!v-+p]' + 'p.yv>BQ470': 'P*4KZ6]?rf' + 'p:SPZ:e/*[': 'dryK>>pO;}' + 'pDDrFhd%u': 'Q]0R7JuJ6g' + 'pYRw[=6;JI': 'bo<0OKHNdG' + 'p]t8:Tka7q': OawZ7gbEjw + pkj3RPBUrF: 'Cw^_3<[mGM' + 'pwwZu{7f~(': 'q>RaVbPB3|' + 'pz(G*g': 'b~m$wS)FZZ' + 'q2#Yv[O0:|': 'Kx5=:O[u*^' + 'q7do-wLx%E': 'c$J,,={OZd' + 'q8][)Ri}=q': 'hqnhi=ysa:' + 'q8fF$4,c6_': 'b#r[:tRj+!' + 'q:XyGf#>Wg': 'do3`,9Ol!H' + 'q=w[fB$d+$': 'z|A6R3H)3B' + 'r(h9&#wc~B': nnM01DlaVX + 'r/B|ra%L,.': 'FGrbm!9:Cd' + 'r5C646H*+*': 'QKm&4[Vr$t' + 'r6BV%|..*5': J4zOBxF6Lw + 'raI-$$|wT': 'po@@>#g44W' + 'r>g<|,FWi%': 'O86#Kfnj]4' + 'r?E$4f-d0/': 'c}m%T+N!Gz' + 'rPekx@n9:L': 'ueNqs:h=(z' + 'rSneU!3}bn': 's>VRzs_ra_' + 'r]*Ioa%$bK': 't@-V_iMKbK' + 's#:aIK$9^x': 'Gs*[8}bP33' + 's4E?ZF[E<>': 'b|Gr(/*-kx' + 's9-GL*@AXD': 'o+)poCG*[u' + 's<_WLSB3I/': 'oLdeL6a~[X' + 's>f40rn,O]': 'HtFq_[S@sg' + 'sB7rZQsRN~' + sM0GPt1HS1: 'wt&@Z9Ke*y' + 'sRw-ik:I$v': 'NH:[}beT-z' + 'sSW/?WMy17': 'B5/<2!_D3h' + 'sT5=H|@KU|': 'pnd}~gw/:?' + sXzvT.g31p: 'b_#ie@t!%Q' + 'sY>@9w~i^p': 'ILj+.~~7_X' + 'skOM(?n$GG': 'ELkg0G|C?W' + spKoFRM4if: QyNb5MKVnQ + 'ssbyA9}]QP': 'e&L!j2[3Fp' + 'stRa/S(0$F': 'fFI@z-#q(E' + 'sxOMs+bE8@': 'cep8BP*Eja' + 'sx|l+io`B@': 'L&YSIviD+L' + 's|K&2S,UP|': 'f)MSa#ZHdL' + 't$y1C2R-Av': 'r%;LR/o1J0' + 't+v9b--,qs': 's^sd#Qsi,{' + 't-0>F]bu>P': x1I5QDSZdm + 't2&ri|[Un:': 'Hg&j8uJWA$' + t3aZXtpNyi: 'C00t,nt-~8' + 't50y/[AEhF': 'la.c2n+A7e' + 'tMMtBUpb$&': 'ox5y>?yriv' + 'tTnCoO/*tk': 'd.lZ`^MKjl' + 't_h*fbN#:(': 'y[:PO%mkg(' + 't`cagaWApo': 'w9bE`pR=z#' + 'tcneuK7v%`': gBJKTCx_/6 + tfCY4j6KEZ: 'k*{up;o{|2' + 'tj&bK}%P!F': 'Ql.8m|u}H!' + 'twrhzJ[k:Z': 'fy^;okm_1k' + 't|h3|;E4g{': 'NSVN;H$s)m' + 'u-`cspdp)D': Ce096-Qwie + 'u4^lH.xem5': 'yLv#s7(0/U' + 'u;|vR#mVMW': 's9/>8RT~Sr' + 'uH*Q(mcRt5': 'w0Je-<6ym_' + 'uRo^w#p5Be': 'j~OT/nTkAt' + 'uc61ToT%gt': z,yR__YX6x + 'ui3>b-?a@m': 'M7m8p!;nP0' + upiX0g1quR: 'G4K|pWoffU' + 'uv1fGXHN@)': 'l(DiRV,GPj' + 'u|I}oElaH)': 'i$+!}pvHZ(' + 'vE[T$`n9T)': 'PFcyiTRKq=' + 'vKx<0Qa!aP': 'E*M$FU#Mf5' + 'vNiD6A.v#s': 'p~,(pas|?P' + 'va/ky=f5pK': 'CTw3Yd}Sx4' + 'vf)G?[_?Pf': 'wOOaycR~z&' + 'vgdj#X?8dB': qD4Ir3_WvI + 'vpOTK6tMr@': 'y8o-zQb(8j' + 'vppEA|$@~g': 'k}cPQuF&TS' + 'vrn%j%6{nu': 't1~2K{sjcs' + 'vwc3}.=Z#&': 'qhlz{k!2cXc': 'kP@udSHU#/' + x2d4n4ADcP: 'B)~SL?|&BX' + 'xb&P*pH(Q': 'OB:w%H&-Al' + 'xqrJ^d[.(O': 'cE|I9C6tDG' + 'yX:tmrd5;]': 'x)A]S|GN_G' + 'z:*d{z(~V2': vJUDdBqkkp + 'zSc]-^U=0=': 'td+]W)8y3S' + crowdanki:uuid: + 43e2586a-9a65-11e8-a777-a0481cc15658: 8ccdc1f6-b042-4d55-8fcd-9e08b5b71dc7 diff --git a/fixtures/ultimate-geography/overlays/languages/nl.yaml b/fixtures/ultimate-geography/overlays/languages/nl.yaml new file mode 100644 index 0000000..ae69062 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/nl.yaml @@ -0,0 +1,721 @@ +id: overlay.translation.nl +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': Er bestaat een dispuut over de naam tussen de aangrenzende landen. In het Koreaans wordt de zee Oostzee genoemd. + Abkhazia: 'Abchazië' + Addis Ababa: Addis Abeba + Adriatic Sea: Adriatische Zee + Aegean Sea: 'Egeïsche Zee' + Africa: Afrika + Akrotiri and Dhekelia: Akrotiri en Dhekelia + Albania: 'Albanië' + Algeria: Algerije + Also known as Burma.: Ook bekend als Birma. + Also known as Cabo Verde.: '' + Also known as Czechia.: '' + Also known as East Timor.: Ook bekend als Timor-Leste. + Also known as Kiev.: '' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: '' + Also known as the Sea of Cortez.: 'Ook wel de Zee van Cortés genoemd.' + 'Also spelled as Sana''a.': '' + American Samoa: Amerikaans Samoa + 'Andorra (narrower, coat of arms with motto)': 'Andorra (smaller, wapen met het motto)' + Antigua and Barbuda: Antigua en Barbuda + Arabian Sea: Arabische Zee + Aral Sea: Aralmeer + Arctic Ocean: Arctische Oceaan + Argentina: 'Argentinië' + Armenia: 'Armenië' + Ashgabat: Asjchabad + Asia: 'Azië' + Athens: Athene + Atlantic Ocean: Atlantische Oceaan + Australia: 'Australië' + 'Australia (white stars, two more stars)': 'Australië (witte sterren, twee sterren meer)' + Austria: Oostenrijk + 'Austria (brighter red, wider white band)': 'Oostenrijk (helderder rood, bredere witte band)' + Autonomous community of Spain.: Autonome gemeenschap in Spanje. + Autonomous province of South Korea.: Autonome provincie van Zuid-Korea. + Autonomous region of Finland.: Autonome regio van Finland. + Autonomous region of Italy.: 'Autonome regio van Italië.' + Autonomous region of Papua New Guinea.: Autonome regio van Papoea-Nieuw-Guinea. + Autonomous region of Portugal.: Autonome regio van Portugal. + Azerbaijan: Azerbeidzjan + Azores: Azoren + Baghdad: Bagdad + Bahrain: Bahrein + 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrein (smaller, minder gekartelde randen, rood)' + Baku: Bakoe + Balkan Peninsula: Balkan + Baltic Sea: Oostzee + Banda Sea: Bandazee + Barents Sea: Barentszzee + Bay of Bengal: Golf van Bengalen + Bay of Biscay: Golf van Biskaje + Beijing: Peking + Beirut: Beiroet + Belarus: Wit-Rusland + Belgium: 'België' + Belgrade: Belgrado + Bering Strait: Beringstraat + Berlin: Berlijn + Bishkek: Bisjkek + Black Sea: Zwarte Zee + 'Bogotá': Bogota + 'Bolivia (coat of arms instead of star)': 'Bolivia (wapen in plaats van ster)' + Bosnia and Herzegovina: 'Bosnië en Herzegovina' + 'Brasília': Brasilia + Brazil: 'Brazilië' + British Virgin Islands: Britse Maagdeneilanden + Brussels: Brussel + Bucharest: Boekarest + Budapest: Boedapest + Bulgaria: Bulgarije + Cairo: 'Caïro' + Cambodia: Cambodja + Cameroon: Kameroen + 'Cameroon (green/red/yellow, yellow star)': 'Kameroen (groen/rood/geel, gele ster)' + Canary Islands: Canarische Eilanden + Cape Verde: 'Kaapverdië' + Caribbean Sea: Caribische Zee + Caspian Sea: Kaspische Zee + Cayman Islands: Kaaimaneilanden + Celebes Sea: Celebeszee + Celtic Sea: Keltische Zee + Central African Republic: Centraal-Afrikaanse Republiek + Cetinje is an honorary capital.: Voormalige hoofdstad is Cetinje. + Chad: Tsjaad + 'Chad (slightly darker blue)': 'Tsjaad (donkerder blauw)' + Chile: Chili + City of San Marino: San Marino + Claimed and controlled: Geclaimd en gecontroleerd + Claimed but not controlled: Geclaimd maar niet gecontroleerd + 'Colombia (no coat of arms)': 'Colombia (geen wapen)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Colombo wordt vaak de hoofdstad genoemd, maar Sri Jayawardenapura Kotte, een buitenwijk van Colombo, is de officiële, wetgevende hoofdstad.' + Comoros: Comoren + Constituent country in the Kingdom of Denmark.: Autonoom gebied binnen het Koninkrijk Denemarken. + Constituent country of the Kingdom of the Netherlands.: Autonoom gebied binnen het Koninkrijk der Nederlanden. + Constituent country of the United Kingdom.: Autonoom gebied binnen het Verenigd Koninkrijk. + Cook Islands: Cookeilanden + Copenhagen: Kopenhagen + Coral Sea: Koraalzee + Croatia: 'Kroatië' + Crown dependency of the United Kingdom.: Brits Kroonbezit. + 'Cuba (red triangle, blue stripes)': 'Cuba (rode driehoek, blauwe strepen)' + 'Curaçao (two stars in top-left corner)': 'Curaçao (twee sterren in de linkerbovenhoek)' + Czech Republic: 'Tsjechië' + Dead Sea: Dode Zee + Democratic Republic of the Congo: Democratische Republiek Congo + Denmark: Denemarken + Denmark Strait: Straat Denemarken + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': 'Betwiste; opgeëist door Israël; Ramallah is het administratieve centrum.' + 'Disputed; claimed by Palestine.': 'Betwiste; opgeëist door Palestina.' + Dominican Republic: Dominicaanse Republiek + Dushanbe: Doesjanbe + East China Sea: Oost-Chinese Zee + East Siberian Sea: Oost-Siberische Zee + 'Ecuador (with coat of arms)': 'Ecuador (met wapen)' + Egypt: Egypte + 'Egypt (emblem instead of text), Yemen (no text)': 'Egypte (embleem in plaats van tekst), Jemen (geen tekst)' + 'Egypt (with emblem), Iraq (with text)': 'Egypte (met embleem), Irak (met tekst)' + 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (ander wapen, donkerder blauw)' + England: Engeland + English Channel: Het Kanaal + Equatorial Guinea: Equatoriaal-Guinea + Estonia: Estland + Ethiopia: 'Ethiopië' + Europe: Europa + European Union: Europese Unie + Falkland Islands: Falklandeilanden + Faroe Islands: 'Faeröer' + Federated States of Micronesia: 'Federale Staten van Micronesië' + Formerly Zaire.: 'Voorheen Zaïre.' + Formerly known as Macedonia.: 'Voorheen bekend als Macedonië.' + France: Frankrijk + French Guiana: Frans-Guyana + French Polynesia: 'Frans-Polynesië' + Georgia: 'Georgië' + Germany: Duitsland + 'Ghana (star instead of coat of arms)': 'Ghana (ster in plaats van wapen)' + Greece: Griekenland + Greenland: Groenland + Guatemala City: Guatemala-Stad + Guinea: Guinee + 'Guinea (green and red flipped, slightly darker green)': 'Guinee (groen en rood omgekeerd, donkerder groen)' + Guinea-Bissau: Guinee-Bissau + Gulf of Alaska: Golf van Alaska + Gulf of California: 'Golf van Californië' + Gulf of Carpentaria: Golf van Carpentaria + Gulf of Guinea: Golf van Guinee + Gulf of Mexico: Golf van Mexico + Gulf of Thailand: Golf van Thailand + Haiti: 'Haïti' + Hawaii: 'Hawaï' + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: Historisch en cultureel gebied in Noord Europa, hetwelk de landen Denemarken, Noorwegen, Zweden en soms Finland en IJsland omvat. + Hong Kong: Hongkong + Hudson Bay: Hudsonbaai + Hungary: Hongarije + Iceland: IJsland + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'IJsland (blauwe achtergrond, rood met wit kruis), Noorwegen (rode achtergrond, blauw met wit kruis)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'IJsland (blauwe achtergrond, rood met wit kruis), Faeröer (witte achtergrond, rood/blauw kruis)' + Independent state claimed by Georgia.: 'Onafhankelijke staat opgeëist door Georgië.' + Independent state claimed by Moldova.: 'Onafhankelijke staat opgeëist door Moldavië.' + Independent state claimed by Somalia.: 'Onafhankelijke staat opgeëist door Somalië.' + Indian Ocean: Indische Oceaan + Indonesia: 'Indonesië' + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesië (breder, helderder rood), Monaco (wit en rood omgekeerd, smaller)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesië (breder, helderder rood), Polen (rood en wit omgekeerd, breder)' + Iraq: Irak + 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (tekst in plaats van embleem), Jemen (geen embleem)' + Ireland: Ierland + 'Ireland (orange and green flipped, wider)': 'Ierland (oranje en groen omgekeerd, breder)' + Island of Indonesia.: 'Eiland van Indonesië.' + Isle of Man: Man + Israel: 'Israël' + Italy: 'Italië' + Ivory Coast: Ivoorkust + 'Ivory Coast (green and orange flipped, narrower)': 'Ivoorkust (groen en oranje omgekeerd, smaller)' + Jerusalem: Jeruzalem + Jordan: 'Jordanië' + Juba: Djoeba + Kaliningrad Oblast: Oblast Kaliningrad + Kazakhstan: Kazachstan + Kenya: Kenia + Khartoum: Khartoem + Known as Nur-Sultan between 2019 and 2022: Gekend als Nur-Sultan tussen 2019 en 2022 + Known as Swaziland until 2018.: Bekend als Swaziland tot 2018. + Kuwait: Koeweit + Kuwait City: Koeweit + Kyiv: Kiev + Kyrgyzstan: 'Kirgizië' + Laayoune: al-Ajoen + Labrador Sea: Labradorzee + Latvia: Letland + 'Latvia (darker red, narrower white band)': 'Letland (donkerder rood, smallere witte band)' + Lebanon: Libanon + Libya: 'Libië' + Lisbon: Lissabon + Lithuania: Litouwen + London: Londen + Luxembourg: Luxemburg + 'Luxembourg (lighter blue)': 'Luxemburg (lichter blauw)' + Luxembourg City: Luxemburg + Madagascar: Madagaskar + Malaysia: 'Maleisië' + Maldives: Malediven + 'Mali (red and green flipped, slightly brighter green)': 'Mali (rood en groen omgekeerd, helderder groen)' + Manila: Manilla + Marshall Islands: Marshalleilanden + Mauritania: 'Mauritanië' + Mediterranean Sea: Middellandse Zee + Melanesia: 'Melanesië' + Mexico City: Mexico-Stad + Micronesia: 'Micronesië' + Moldova: 'Moldavië' + 'Moldova (wider, coat of arms with eagle)': 'Moldavië (breder, wapen met adelaar)' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (smaller, donkerder rood), Polen (rood en wit omgekeerd, donkerder rood)' + Mongolia: 'Mongolië' + Morocco: Marokko + Moscow: Moskou + Muscat: Masqat + 'N''Djamena': Ndjamena + Namibia: 'Namibië' + 'Nauru (single star below yellow band)': 'Nauru (enkele ster onder de gele band)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru heeft geen officiële hoofdstad; het Yaren-district is de de facto hoofdstad.' + Netherlands: Nederland + 'Netherlands (darker blue)': 'Nederland (donkerder blauw)' + New Caledonia: 'Nieuw-Caledonië' + New Zealand: Nieuw-Zeeland + 'New Zealand (red stars, two fewer stars)': 'Nieuw-Zeeland (rode sterren, twee sterren minder)' + 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (ander wapen, lichter blauw)' + North America: Noord-Amerika + North Korea: Noord-Korea + North Macedonia: 'Noord-Macedonië' + North Nicosia: Noord-Nicosia + North Sea: Noordzee + Northern Cyprus: Noord-Cyprus + Northern Ireland: Noord-Ierland + Northern Mariana Islands: Noordelijke Marianen + Norway: Noorwegen + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Noorwegen (rode achtergrond, blauw kruis), Faeröer (witte achtergrond, rood/blauw kruis)' + Norwegian Sea: Noorse Zee + Not a sovereign country: Geen soevereine staat + 'Oblast (administrative region) of the Russian Federation.': 'Oblast (administratieve regio) van de Russische Federatie.' + Oceania: 'Oceanië' + Official capital was moved from Bujumbura to Gitega in 2019.: Bujumbura was tot 2019 de hoofdstad. + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: Malabo was tot 2026 de hoofdstad. + 'Officially Côte d''Ivoire.': '' + Officially Luxembourg.: '' + Overseas department of France.: Overzees departement van Frankrijk. + Overseas territory of France.: Overzees land van Frankrijk. + Overseas territory of the United Kingdom.: Overzees land van het Verenigd Koninkrijk. + Pacific Ocean: Stille Oceaan + Palestine: Palestina + 'Palestine (black/white/green, red arrow)': 'Palestina (zwart/wit/groen, rode pijl)' + 'Palestine (no symbol)': 'Palestina (geen symbool)' + Panama City: Panama-stad + Papua New Guinea: Papoea-Nieuw-Guinea + Paris: Parijs + Partially recognised state claimed by China.: 'Deels erkende staat, opgeëist door China.' + Partially recognised state claimed by Morocco. Also known as Western Sahara.: 'Deels erkende staat, opgeëist door Marokko.' + Partially recognised state claimed by Serbia.: 'Deels erkende staat, opgeëist door Servië.' + Persian Gulf: Perzische Golf + Philippine Sea: Filipijnenzee + Philippines: Filipijnen + Poland: Polen + Polynesia: 'Polynesië' + Prague: Praag + Pretoria, Cape Town, Bloemfontein: Pretoria, Kaapstad, Bloemfontein + 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blauwe driehoek, rode strepen)' + 'Qatar (wider, more serrated edges, maroon)': 'Qatar (breder, meer gekartelde randen, kastanjebruin)' + Red Sea: Rode Zee + Region of France.: Franse regio. + Republic of the Congo: Congo + 'Reykjavík': Reykjavik + Riyadh: Riyad + Romania: 'Roemenië' + 'Romania (slightly lighter blue)': 'Roemenië (lichter blauw)' + Russia: Rusland + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rusland (geen wapen), Slovenië (breder, kleiner wapen)' + Sahrawi Arab Democratic Republic: Sahrawi Arabische Democratische Republiek + Saint Kitts and Nevis: Saint Kitts en Nevis + Saint Martin: 'Sint-Maarten (Franse Antillen)' + Saint Vincent and the Grenadines: Saint Vincent en de Grenadines + Sardinia: 'Sardinië' + Saudi Arabia: 'Saoedi-Arabië' + Scandinavia: Scandinavisch Schiereiland + Scotland: Schotland + Sea of Galilee: Meer van Tiberias + Sea of Japan: Japanse Zee + Sea of Okhotsk: Zee van Ochotsk + Semi-autonomous region of Tanzania.: Half-autonome regio van Tanzania. + 'Senegal (green/yellow/red, green star)': 'Senegal (groen/geel/rood, groene ster)' + Seoul: Seoel + Serbia: 'Servië' + Seychelles: Seychellen + Sicily: 'Sicilië' + Sint Maarten: 'Sint Maarten (Koninkrijk der Nederlanden)' + Slovakia: Slowakije + 'Slovakia (narrower, bigger coat of arms)': 'Slowakije (smaller, groter wapen)' + 'Slovakia (with coat of arms)': 'Slowakije (met wapen)' + Slovenia: 'Slovenië' + Solomon Islands: Salomonseilanden + Somalia: 'Somalië' + South Africa: Zuid-Afrika + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'Zuid-Afrika is een federale republiek met drie hoofdsteden: Pretoria (bestuurlijke macht), Kaapstad (wetgevende macht) en Bloemfontein (rechterlijke macht).' + South America: Zuid-Amerika + South China Sea: Zuid-Chinese Zee + South Korea: Zuid-Korea + South Ossetia: 'Zuid-Ossetië' + South Sudan: Zuid-Soedan + South Tarawa: Zuid-Tarawa + Southern Ocean: Zuidelijke Oceaan + Sovereign country: Soevereine staat + Spain: Spanje + Special Administrative Region of China.: Speciale bestuurlijke regio van de volksrepubliek China. + Sri Jayawardenepura Kotte: Sri Jayewardenapura Kotte + 'St. George''s': 'Saint George''s' + 'St. John''s': 'Saint John''s' + State of the United States.: Staat van de Verenigde Staten. + State recognised only by Turkey and claimed by Cyprus.: 'Staat enkel erkend door Turkije en opgeëist door Cyprus.' + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Subregio van Oceanië met duizenden kleine eilanden in de centrale en zuidelijke Stille Oceaan.' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Subregio van Oceanië met duizenden kleine eilanden in de westerse Stille Oceaan.' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Subregio van Oceanië, waaronder Vanuatu, de Salomonseilanden, Fiji en Papoea-Nieuw-Guinea.' + Sudan: Soedan + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Soedan (rood/wit/zwart, groene pijl), Sahrawi Arabische Democratische Republiek (met wassende maan en ster)' + Sukhumi: Soechoemi + Svalbard: Spitsbergen + Sweden: Zweden + Switzerland: Zwitserland + 'Switzerland has no official capital; Bern is the de facto capital.': 'Zwitserland heeft geen officiële hoofdstad maar Bern vervult deze rol in de praktijk en wordt aangeduid als bondsstad.' + Syria: 'Syrië' + 'São Tomé': 'Sao Tomé' + 'São Tomé and Príncipe': 'Sao Tomé en Principe' + Tajikistan: Tadzjikistan + Tashkent: Tasjkent + Tasman Sea: Tasmanzee + Tehran: Teheran + The Bahamas: 'Bahama''s' + The Gambia: Gambia + Timor Sea: Timorzee + Timor-Leste: Oost-Timor + Tokyo: Tokio + Transnistria: 'Transnistrië' + Trinidad and Tobago: Trinidad en Tobago + Tskhinvali: Tschinvali + Tunisia: 'Tunesië' + Turkey: Turkije + Turks and Caicos Islands: Turks- en Caicoseilanden + Uganda: Oeganda + Ukraine: 'Oekraïne' + Unincorporated internal area of Norway.: Intern gebied zonder rechtspersoonlijkheid van Noorwegen. + Unincorporated territory of the United States.: Gebied zonder rechtspersoonlijkheid van de Verenigde Staten. + United Arab Emirates: Verenigde Arabische Emiraten + United Kingdom: Verenigd Koninkrijk + United States Virgin Islands: Amerikaanse Maagdeneilanden + United States of America: Verenigde Staten + Uzbekistan: Oezbekistan + Vatican City: Vaticaanstad + Vienna: Wenen + Wallis and Futuna: Wallis en Futuna + Warsaw: Warschau + Washington, D.C.: Washington D.C. + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: 'Amsterdam is de officiële hoofdstad, hoewel het parlement en de regering in Den Haag zetelen.' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: 'Ondanks Dodoma de officiële hoofdstad is, is Dar es Salaam de facto de zetel van de regering.' + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': Ondanks Laayoune, ook wel bekend als al-Ajoen, de verklaarde hoofdstad is, is Tifariti de feitelijke zetel van de regering. + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: Mbabane is de bestuurlijke hoofdstad en Lobamba de traditionele koninklijke hoofdstad en wettelijke zetel. + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: 'Porto-Novo is de officiële hoofdstad, maar de economische hoofdstad en grootste stad van Benin is Cotonou.' + While Sucre is the constitutional capital, La Paz is the seat of government.: Ondanks Sucre de constitutionele hoofdstad is, is La Paz de zetel van de regering. + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: Abidjan was tot 1983 de hoofdstad, de meeste overheidsgebouwen en ambassades bevinden zich er nog steeds. + White Sea: Witte Zee + World region covering the Australian continent and most of the islands in the Pacific Ocean.: Wereldregio die het Australische continent en de meeste eilanden in de Stille Oceaan beslaat. + Yellow Sea: Gele Zee + Yemen: Jemen + Yerevan: Jerevan + 'Åland Islands': 'Åland' + additions: + notes.note.baltic-sea.fields.field.country-info: Ook wel Baltische Zee. + notes.note.belarus.fields.field.country-info: Ook bekend als Belarus. + notes.note.china.fields.field.capital-info: Ook als Beijing bekend. + notes.note.federated-states-of-micronesia.fields.field.country-info: 'Ook wel Micronesië of Micronesia genoemd.' + notes.note.maldives.fields.field.country-info: Ook wel Maldiven genoemd. + notes.note.pacific-ocean.fields.field.country-info: Ook wel Grote Oceaan of Pacifische Oceaan genoemd. + notes.note.sea-of-galilee.fields.field.country-info: Ook wel Meer van Kinneret, Meer van Galilea of Meer van Genezareth genoemd. + variables: + label.capital: + Capital: Hoofdstad + label.capital-hint.answer: + 'Hint: {{Capital hint}}': 'Aanwijzing: {{Capital hint}}' + label.flag: + Flag: Vlag + label.location: + Location: Ligging + note-type.name: + Ultimate Geography: 'Ultimate Geography [NL]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': 'Vlag vergelijkbaar met {{Flag similarity}}.' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': tBCiVBcuG_ + '5JJ2]i)p4': yXSMWI1KUx + '5S(f7%O-$': 's!x^ui`nhG' + '?n!)c/h=Q': 'IuT|OM*Nw~' + 'AES],s:8=': 'gGm>+9`k&' + 'B:xP}@&|' + 'bW)ch:vs1P': 'ioJ7(frmya' + 'b[(2`4t&?J': 'fN1A,quIbA3.' + 'd}R:qHj1,7': 'q3=L7rObtd' + 'e!KSebP}pt': 'fE^)_>&{*d' + 'e+/O]%*qfk': 'K`AixN?4fO' + 'e.t$Vi5,>f': '.9`Zb?|4]' + 'e42BpV,P}C': 'MXc[U*Dc|y' + 'e5P*D#I{m{': 'j!9GChs@cQ' + 'eHH?U': 'p}2F6mZ.QA' + 'euW0jm=wfR': 'g:S=I8r:O>' + 'e{e8vi@^PY': 'q;6Wv3u)tx' + 'f#4-L[mp5*': 'i$q|D&dUr3' + 'f*2@qiHh_1': 'o2AxTxOe+;' + 'f*LO~>!0PN': 'K7t:ah)Nlf' + 'f21=-A~W;~': 'P+Jx`aqYih' + 'f>c7x2E]Q6': 'QYr^22xCo3' + 'fRSM{K+;[w': 'bq]dHo&Tn~' + 'fT!w8R#>dg': 'fezBXMIU#+' + 'fV`&p81%d(': 'b&EqNhY[/p' + fkq7lhLkdH: 'w7op[fQgTo' + 'fxAN),FAc$': 'Ef]sAhH-1=' + 'g-]bb&[.E!': 'fQMbKoTu%<' + 'g9v~y/;=z(': 'p?N:RJ`9Qb' + 'g<[f89~U)A': 'E-hC9K}m$~' + 'gBCR?!O*;E': 'JM7]2EJ[qX' + 'gB|K:+r?1V': 'w_*e?Ms1]_' + 'gDsLPj(9S#': 'A1z!_C=d(c' + 'gFOojQO(MH': 'zumu&z#g.>' + 'gG6SWfM>H0': 'ygzZMO52<5' + 'gRVZ]qJ#>O': 'I+N4^}Lq%;' + 'gYIoR`|AxW': 'q>9E7+;4K.' + 'gePC:Xt_nW': 'sp;kVFWNG~' + 'gp[h@]y13l': 'uc9G:E._YR' + 'gsS{jR,]|Q': 'G;pW`r5kYU' + 'gx:;za!?C9': '/zj4yGq-~' + 'gz9${p}er*': 'Hp1dL9r:*P' + 'g~%pF`(x{u': 'm@%Rs{o2LO' + 'h&`>n98~V1': 'Ob.MR2u/-&' + 'h9Jr(H[=1~': 'O0h/x6zav@' + 'h?a*UKfT_M': rIn3klxCO3 + 'hE5j*e,Q?[': 'O|]ivh~%v{' + 'hNE9}>;Q&-': 'plhRl:Tz/O' + 'hXNTVF<:SH': 'QL%7i$x/c,' + 'hZ/V/D&rO`': 'z@fLLI>[@B' + 'h_9=mkZXAH': 'C_6GTKD=fH' + hoHandbYAy: 'Ca[)+1M+q?' + 'hrI_uiW>E]': 'Q+z>Go{V83' + 'h}lnSN_f$_': 'CpG.*qzHU5' + 'h~5xz+=ke~': 'yM2(NER.TK' + 'i)]+09JUR&': 'rBXL[4*?g5' + 'i2]v|D3@:j': byp9L/7/,y + 'iB-8Br~onq': '*/b5m[q8j' + 'iIS#Nw]tr]': 'epu:F7KRn!' + 'iR;Um(L/~c=': 'l57t^$S&*u' + 'i^J8&$(1]|': 'Neqxx;S4OH' + 'ia8as9sCY|': 'K[L!R^!9Z,' + 'igiK,ff[eO': 'uK!1aq#,Ii' + 'ijP>aJBT!9': vNlrE-EWLf + 'ilGtw#=asM': 'e(k0BGyL4h' + 'j%*8gN)q>d': 'sM=)Xv_Xa-' + 'j)i)pB*HJ,': 'n8ieJA{+lP' + 'j2IQ=f=w3@': 'e8|;;S>xQ.' + j7eEZkzsCZ: 'trUg0y&dqG' + 'j9:K,~DQ2v': 'NIKs(pApAY' + 'jg|}': 'jxR[;SimkU' + 'jCd]`-=k,:': 'iOD+[;vOc1' + 'jDA-V?/hVj': 'Bo5dwc%z#M' + jI9P-f6r3M: 'c(O@!xlX|c' + 'jKIDq|}J&c': 'CH0T!),h9{' + 'jKO,9y0M;#': 'tUW+`.SQo9' + 'jN^Vc%9OQ5': 'f($keyMpus' + 'jN|NAUP*h}': 'F5Vc$,%[aL/r)' + 'kZ52zV#[Iv': 'e=OVM]`Q{U' + 'k[)#L4.[v3': 'dG[-P1]Ly-' + 'khB~(m^@Z5': 'K1#2~5/6~Z' + 'kkbYQXw5b;': 'e90l[|lH!I' + 'koI(}OyJ:@': 'lR`ZpmCTOP' + 'kopwzzB``A': 'v#>8]5Eg[x' + 'k}4/oj#$~s': 'K~OD+`QJa(' + 'k}A9]O:0xw': 'rd_(OgnX-P' + 'l$2O|9w`F~': 'c7Zh2xn$t_' + 'l$zM~ihPFE': 'Cx2EH*D>}Z' + 'lI)Ah/E': 'mxqXkoW;I]' + 'l*j*EE:!Y?': 's$RMwIQGZb' + 'l+>Ki!j#v+': 'dm/G5**HY~' + 'l3Ly8PBxt(': 'rqH=>+|p}D' + 'lAxH.CfJB@': 'cl`b5TBXX(' + 'leh^fmrt*#': 'B2:MgtMw1@' + 'lhWI;$JC9(': 'kQ8x(01<}' + libliptkB4: 'rs3Uv}D,)h' + 'lmRujD!(A7': 'CTD4|lJ}]5' + 'ln^3p)j3b.': 'jmR])+zhJo' + 'lp@e##m.!+': 'LoxeR.`jw@' + 'lpbD0St&OU': 'L&Wh0Oh!9*' + 'lq`T^ZBF2Y': 'K0`3{Z#>H&' + 'l}[BG9.nQS': 'm$4os@Y#(Y' + 'm)WUtJS`H&': 'lQZ,yi}j,|' + 'm*#%sE.`.;': 'z&9OMJ`KZclA6' + 'mq)8`hi{hb': 'p{:,t++CFI' + 'mtd;>7T|g>': 'l::->dQxMA' + 'mz[>>AxIr4': 'cU)|/vPi6j' + 'm{noqE29%I': 'mB&*r_Fpq%' + 'm{oM,jr>C#': 'Ne`2FO/S': 'sUW(K@)rpu' + 'n+t>2`:;P/': O8/WkWCPZP + 'n:7&>YT5A=': 'q-&Hq~16&{' + 'nIfRn(r' + 'nsS59c`VL,': 'qL:(ZnK[{b' + 'nuU8A_$i2M': e1k,rPrJXi + 'n{{o2K1:dw': 'jov_[O@hK6' + 'o#&RMA_REo': 'OwHb{Mz|$h' + 'o3VS!KWb/@': xLWoq0vCna + 'o9{x/yz': 'AfMgn:9E6@' + 'oa87&PB&Bm': bPqp-AGH3Y + 'onMKID2d;D': 'sGn#POf<9-' + 'o~}KVA:VbI': 'J)9J,MA_jm' + 'p#R$-mG/L': 'sgfCxE,%}I' + 'p.yv>BQ470': 'eCQL}h5w8Y' + 'p:SPZ:e/*[': 'B{FDr5`ABh' + 'pDDrFhd%u': 'GmH*JA[[k`' + 'pYRw[=6;JI': 'zSI<3YA/@<' + 'p]t8:Tka7q': 'oI5~{W$B0+' + pkj3RPBUrF: 'Oy&*?g': 'mUci[@*LWg': 'oA1,v5>^){' + 'q=w[]sc0f?(|' + 'q[S4`,F0D=': MjYBzKMsYj + 'q[}7hk(+=e': n1HR1,WDIm + 'qiv/4tOlf=': 'M?CI|U:7E' + qvITjNxkLN: 'A16VT}*wpu' + 'qx%>fB$d+$': 'n58Xs' + 'r/B|ra%L,.': 'q,5sUDZJ@|' + 'r5C646H*+*': 'HL//^#,,5X' + 'r6BV%|..*5': 'hs$=I;b%H{' + 'raI-$$|wT': 'p((H!2~3O7' + 'r>g<|,FWi%': 'A7d`$VleNv' + 'r?E$4f-d0/': 'Abp+=0%|oz' + 'rPekx@n9:L': 'xm[hZ@]0D2' + 'rSneU!3}bn': 'u@^{[]68&M' + 'r]*Ioa%$bK': 'zlk#[1bx]e' + 's#:aIK$9^x': 'A$m)oTJ1BW' + 's4E?ZF[E<>': 'hf40rn,O]': 'i1Ug;$!s.p' + 'sB7rZQsR@9w~i^p': 'E/MT}kv2tI' + 'skOM(?n$GG': 'AXB' + 'sxOMs+bE8@': 'qj/S4:J~Wh' + 'sx|l+io`B@': 'd8zQ]l>xmo' + 's|K&2S,UP|': 'B8UUt(OH@c' + 't$y1C2R-Av': 'pediB(:-]T' + 't+v9b--,qs': 'cXB4#4>X;-' + 't-0>F]bu>P': 'F&@Rc;dl]z' + 't2&ri|[Un:': 'Q+I:vHQr7%' + t3aZXtpNyi: 'c+d=6X;DR:' + 't50y/[AEhF': 'oO/>jPN^a5' + 'tMMtBUpb$&': 'Qm?>e]Q]ag' + 'tTnCoO/*tk': jpDvwSb-OL + 't_h*fbN#:(': 'sHKG^]G1Hi' + 't`cagaWApo': 'EjFv^:qmnZ' + 'tcneuK7v%`': 'D@,Q^H)}Ln' + tfCY4j6KEZ: 'sze_Yzh;5$' + 'tj&bK}%P!F': 'bit-NvK^B3' + 'twrhzJ[k:Z': 'pV`:gi7=r2' + 't|h3|;E4g{': 's5?gh*QP;:' + 'u-`cspdp)D': 'Iva{=NlZof' + 'u4^lH.xem5': 'M5*2c!ENJ`' + 'u;|vR#mVMW': 'F[Xrq2xy>E' + 'uH*Q(mcRt5': 'ebcMj4pw`P' + 'uRo^w#p5Be': 'p#WecI-Q`c' + 'uc61ToT%gt': 'J[n$C~*-<~' + 'ui3>b-?a@m': 'OQK^$9xbg,' + upiX0g1quR: 'Mmj=@>?Sx{' + 'uv1fGXHN@)': 'vu]t,+fglW' + 'u|I}oElaH)': 'N-%-M=n!dh' + 'vE[T$`n9T)': 'r=ab^[Q' + 'vNiD6A.v#s': 'Bapr]RwJ#?' + 'va/ky=f5pK': 'nm*l%S8r+{' + 'vf)G?[_?Pf': 'i`?s5yONk<' + 'vgdj#X?8dB': 'o0AQQ^2)G6' + 'vpOTK6tMr@': 'gw%[JT~eOZ' + 'vppEA|$@~g': 'u2G!Pc,B?g' + 'vrn%j%6{nu': 'l$|!y^BHT^' + 'vwc3}.=Z#&': 'M=3y(}U;7v' + 'wa>{k!2cXc': 'o3&;VW(Hf,' + x2d4n4ADcP: 'cH2i]a9RGb' + 'xb&P*pH(Q': 'HedPVg8N$)H' + 'b`5Q_Z=HQT': 'eYTxN=I7~*' + 'bd~Ght5tLR': 'oG4Xq?fD~!' + 'bk2ByZIeU+': 'APd:*Sw~Jo' + 'bnEK#)[;qT': 'DP;c*Gso.!' + 'c(,2{yXI#E': 'm9:dQqYhJ-' + 'c,5(mUGzYT': 'rr).TrA1*?' + 'cAlyc~G.y%': cgFhukF/h- + 'cEO^y)Md[G': 'j?gTI%edF-' + 'cL?8!xP#Wj': 'i^0~4vv^)z' + 'cPO|Cru8n<': 'G9`LF2O_m`' + 'cV4idO^r8W': 'g7Q$z&jd|J' + 'cYKvF#ptF7': 'K04)[9u{;U' + 'c^,~EC6Pb2': 'MXy1;P2k/U' + 'ck!6;xK$/^': 'wij7pNM2{4' + 'cv+Nq9:aSA5<' + 'd-q4J~CNW_': 'En5vg+xV2M' + 'd3c`&/1Y^D': 'GMVFQ3%=_`' + 'd5m8%D6,;u': 'tb.s`6ajY!' + 'dC:e5G4J/p': 'B;L,H^Gdc?' + 'dUZ^=SE5BG': 'v40(;6p*}' + 'dWA49p}F{k': Jcw/VM6mWA + 'dY96`C-f(_': 'oC1a!U&Wzg' + 'd``B*6:eCx': 'B$dB1.]P{>' + 'dbP/Cj}sB+': 'd.uD&(B8mn' + 'de=9c:,@j9': 'Kc/ra`3(37' + 'dlU7VG|3Gd': 'ga{WsQ_.xK' + 'd}R:qHj1,7': 'm1fd8FZkD)' + 'e!KSebP}pt': 'sqTj^jwG6J' + 'e+/O]%*qfk': Eiu-RV/Iic + 'e.t$Vi5,>f': 'Is7;|To~I0' + 'e42BpV,P}C': 'vVp3u$}6,}' + 'e5P*D#I{m{': '44Z:NG|/2' + 'e' + 'eALZe^HCHq': 'uf:+,ayBHx' + 'eHJ?)TAdTo': 'rB7G?Z1O`u' + 'eN#^kPGv*L': 'n=-`bGotOp' + 'eTHH?U': 's)j|{A1@v,' + 'euW0jm=wfR': 'noP8iDid~*' + 'e{e8vi@^PY': 'R>nCNt0yu' + 'f#4-L[mp5*': 'vO82@=AYb4' + 'f*2@qiHh_1': 'EV^6i(gTp`' + 'f*LO~>!0PN': 'to@xOWmFJ.' + 'f21=-A~W;~': 'oXmy@&NBhz' + 'f>c7x2E]Q6': 'cBteEeBrv@' + 'fRSM{K+;[w': 'rp9WMFvoC`' + 'fT!w8R#>dg': 'cn;X?=;$x$' + 'fV`&p81%d(': 'Ipwb%/>_NH' + fkq7lhLkdH: 'wENu7P2Tf>' + 'fxAN),FAc$': 'k<2o{CZc@I' + 'g-]bb&[.E!': '}r6Y-pS^`' + 'g9v~y/;=z(': 'gv%|cnW_iW' + 'g<[f89~U)A': 'Gjm,Z+1Xq<' + 'gBCR?!O*;E': kerXOzu2o7 + 'gB|K:+r?1V': 'IhM_;-(g;*' + 'gDsLPj(9S#': 'OqXRZlbw}(' + 'gFOojQO(MH': 'cq(y`}6&AT' + 'gG6SWfM>H0': i0rhYlrB0u + 'gRVZ]qJ#>O': 'KBG*PF:x]!' + 'gYIoR`|AxW': 'Dx=^!Ay8w)' + 'gePC:Xt_nW': 'qN4y}IRxo9' + 'gp[h@]y13l': vaqxA_9kBR + 'gsS{jR,]|Q': 'erWr`a8I.' + 'gx:;za!?C9': 'yTe`BA-t|0' + 'gz9${p}er*': 'e=09`Q:lUY' + 'g~%pF`(x{u': 'OIc;jg+Y}!' + 'h&`>n98~V1': 'Eo@49=+F}8' + 'h9Jr(H[=1~': 'c~d(lr9Fvb' + 'h?a*UKfT_M': 'v4}AH&TlZ^' + 'hE5j*e,Q?[': 'Me4;.M^B3E' + 'hNE9}>;Q&-': KB32Lzzff7 + 'hXNTVF<:SH': 'DwiS>P`d`K' + 'hZ/V/D&rO`': 'e~0n<#f5bd' + 'h_9=mkZXAH': 'oK+nX%5Azo' + hoHandbYAy: 'gFErY.d_};' + 'hrI_uiW>E]': MafF-rLgDx + 'h}lnSN_f$_': 'PK^Qz|@`4!' + 'h~5xz+=ke~': 'jr&*:[}a%D' + 'i)]+09JUR&': 'D4kW+fbRGj' + 'i2]v|D3@:j': 'j{8I6i:uI^' + 'iB-8Br~onq': 'Qofk00d#]|' + 'iIS#Nw]tr]': P3zYwkzeeI + 'iR;Um(L/~c=': 'CK)-k5{^o/' + 'i^J8&$(1]|': 'GpKt5mjHz`' + 'ia8as9sCY|': 'rz:n2?FQ,S' + 'igiK,ff[eO': 'o93{:oXnx&' + 'ijP>aJBT!9': 'nsttHvgDm>' + 'ilGtw#=asM': 'u/n1Ph{G86' + 'j%*8gN)q>d': HNwlMP3HsK + 'j)i)pB*HJ,': 'P0$X6wa6aL' + 'j2IQ=f=w3@': 'hlD&(T)S+:' + j7eEZkzsCZ: 'jQ(g9H1Yt{' + 'j9:K,~DQ2v': 'I1nWmc<' + 'j=L,YBvsBk': PX.1yc,,.R + 'jBd@A?>g|}': 'e_c+~H}Uz@' + 'jCd]`-=k,:': 'nsSu>A!qF^' + 'jDA-V?/hVj': 'C,T`nz1)sq' + jI9P-f6r3M: 'Ho0:llLz?,' + 'jKIDq|}J&c': 'd|JbnAP.Wy' + 'jKO,9y0M;#': 'QseIpMO;9u' + 'jN^Vc%9OQ5': 'r2|aFq?Aaf' + 'jN|NAUP*h}': 'N:NFPUaE)d' + 'jTNoKo}Bu+': 'bO(L55c^DT' + 'jX=#G8wu#(': 'D6j;d-P4IS' + 'jYz[ibrr9m': 'qUay%|m3A)' + 'jcu!gLw/&r': 'EsgIVed0P)' + 'jrM28*HbyG': 'tln?%x~7F~' + 'j|z@PMgdx,': 'K)!dRmp|}@' + 'k$1_yaF?9#': 'J.W)SYM8sK' + 'k/u1:B%DJH': 'Kk%+6g2I$N' + 'k0{O[6l]bH': 'u(p9/S}e{:' + 'k1]FH+s8j@': 'nD4?vsWvX8' + 'k9E.p^>:KH9MRE' + 'kE!iKHLN_g': 'r,rl(W~R!#' + 'kQ:NIhJy~E': 'uQQ~q`h$RB' + 'kS)hDm%w}R': 'Lg7/ATNE^0' + 'kX]hN=_.c-': 'J]tb-@Emki' + 'kZ52zV#[Iv': 'bV~T=Qu3l{' + 'k[)#L4.[v3': 'o_Q;]Pr)2K' + 'khB~(m^@Z5': 'P?.F8Nyf*>' + 'kkbYQXw5b;': 'sZQ[ZR,vG^' + 'koI(}OyJ:@': 'jktcLTC(E;' + 'kopwzzB``A': 'HT6x8SDj(:' + 'k}4/oj#$~s': 'u2k7XVl,6]' + 'k}A9]O:0xw': 'grrlv;lc|U' + 'l$2O|9w`F~': 'P+B8Wp;7Ki!j#v+': 'mmXJoK#R`6' + 'l3Ly8PBxt(': fc5RwyMAlq + 'lAxH.CfJB@': '}p:<>`uNI' + 'lH^2D]w;[H': 'yAA!g~.{$T' + 'lU@C3`Uk5#}c' + 'mi+kf0LTW.': 'oq+z3Invfu' + 'mk1VltJl<]': 'x,_A^{yR`-' + 'mk20`,N=`=': '2]!$k739K' + 'mkgcU7[bA%': 'k`S4q(;Y;@' + 'mq)8`hi{hb': 'bfp}dMQSWW' + 'mtd;>7T|g>': 'A$@0/RKWZt' + 'mz[>>AxIr4': 'h8#>aN=*pf' + 'm{noqE29%I': 'EmbtmCi$[&' + 'm{oM,jr>C#': 'psgj>Xdg^g' + 'n&>e`2FO/S': 'P!Q,2o]Xv`' + 'n+t>2`:;P/': 'n+bE8**,!P' + 'n:7&>YT5A=': 'n-)8Zg=(tt' + 'nIfRn/yz': 'v>bBTG<5Gh' + 'oa87&PB&Bm': 'NsA5?JrP~1' + 'onMKID2d;D': 'o=u5,~YY$n' + 'o~}KVA:VbI': 'ojB*fV3Uwu' + 'p#R$-mG/L': 'Q.;i59|#bh' + 'p.yv>BQ470': 'Ff`Q)s:oK:' + 'p:SPZ:e/*[': 'sif]k_+X`R' + 'pDDrFhd%u': 'FoGORw~;@y' + 'pYRw[=6;JI': 'MGk<]E]6Ax' + 'p]t8:Tka7q': 'll0rItP2i#' + pkj3RPBUrF: 'hZJjw>^KS1' + 'pwwZu{7f~(': 'GkUycywYs:' + 'pz(G*9eHNM' + 'p}Qa|!7PpR': 'lXJyrwE+g': 'PLCJ5=X}y%' + 'q2#Yv[O0:|': 'PV>94O`ec&' + 'q7do-wLx%E': 'LrH4?MS:.Y' + 'q8][)Ri}=q': 'lASVnaclh*' + 'q8fF$4,c6_': 'e54Fy{u]u+' + 'q:XyGf#>Wg': yhZg46q,-C + 'q=w[j~SL1' + 'q[S4`,F0D=': z9oZHcCKE6 + 'q[}7hk(+=e': 'K{nI7fB$d+$': 'HK:>.+2ex6' + 'r(h9&#wc~B': 'ejDP,^qvKr' + 'r/B|ra%L,.': 'LH}a8g>8j0' + 'r5C646H*+*': 'ujW*5ockuC' + 'r6BV%|..*5': 'k,(otxIVcI' + 'raI-$$|wT': 'LL=PU%A:N#' + 'r>g<|,FWi%': 'N2MVXSvID:' + 'r?E$4f-d0/': 'bIs.bxZR0(' + 'rPekx@n9:L': 'GgGoW+/m9v' + 'rSneU!3}bn': 'OAj=#W15kV' + 'r]*Ioa%$bK': 'M#.,MF3zu#' + 's#:aIK$9^x': 'vnS!8Q]FlM' + 's4E?ZF[E<>': 'G-yD_gaG`b' + 's9-GL*@AXD': 'tjvz_4.G7@' + 's<_WLSB3I/': 'tSLAu,1V>Q' + 's>f40rn,O]': 'Lva!X_tl`2' + 'sB7rZQsRRJP,/*W' + sM0GPt1HS1: iQm-U0VW2S + 'sRw-ik:I$v': 'd`_!}E{gW[' + 'sSW/?WMy17': 'hh/_8cG~di' + 'sT5=H|@KU|': 'N(rD|+f@[k' + sXzvT.g31p: 'tMIwged`Yl' + 'sY>@9w~i^p': 'r}O8m^>5CD' + 'skOM(?n$GG': 'Mu]P2-BY~V' + spKoFRM4if: 's+!2n<F]bu>P': KuoySbahQ- + 't2&ri|[Un:': Byx01EmNqQ + t3aZXtpNyi: 'l2*Cj7{@n2' + 't50y/[AEhF': 'o?c_P]7cRa' + 'tMMtBUpb$&': 'guaI|||b-?a@m': 'HF&vW)dD9Y' + upiX0g1quR: 'e@[V/X}*-w' + 'uv1fGXHN@)': 'QwC`JOy)f)' + 'u|I}oElaH)': 'uQ-90}yhTf' + 'vE[T$`n9T)': 'o(ubw#KC7V' + 'vKx<0Qa!aP': 'n|X/>eJYEp' + 'vNiD6A.v#s': 'd:Z2Wp!K#J' + 'va/ky=f5pK': 'i3Cz?V6y+J' + 'vf)G?[_?Pf': 'Nd05D{&,6r' + 'vgdj#X?8dB': 'yW)Se2ZZ0+' + 'vpOTK6tMr@': 'vc/xDNSP!Q' + 'vppEA|$@~g': 'H2VIy>Ka56' + 'vrn%j%6{nu': 'NY@cC.#2,D' + 'vwc3}.=Z#&': 'u79LUet]wo' + 'wa>{k!2cXc': 'JE12$)9ER@' + x2d4n4ADcP: 'F&7yB5tW/w' + 'xb&P*pH(Q': 'Q*1]Cvu?#a' + 'xqrJ^d[.(O': 'EezTS:.:Ou' + 'yX:tmrd5;]': 'L4WiVp>Un|' + 'z:*d{z(~V2': 'kBb;v^=11(' + 'zSc]-^U=0=': 'O?:STIyrxP' + crowdanki:uuid: + 43e2586a-9a65-11e8-a777-a0481cc15658: 84786477-e28a-490e-958d-7b35946d7b11 diff --git a/fixtures/ultimate-geography/overlays/languages/pt.yaml b/fixtures/ultimate-geography/overlays/languages/pt.yaml new file mode 100644 index 0000000..9663605 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/pt.yaml @@ -0,0 +1,872 @@ +id: overlay.translation.pt +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': 'Existe uma disputa de nomenclatura entre os países limítrofes do mar, com a Coreia do Sul apoiando notavelmente o nome Mar do Leste.' + Abkhazia: 'Abecásia' + Abu Dhabi: Abu Dabi + Accra: Acra + Addis Ababa: Adis Abeba + Adriatic Sea: 'Mar Adriático' + Aegean Sea: Mar Egeu + Afghanistan: 'Afeganistão' + Africa: 'África' + Akrotiri and Dhekelia: 'Acrotíri e Deceleia' + Alaska: Alasca + Albania: 'Albânia' + Algeria: 'Argélia' + Algiers: Argel + Also known as Burma.: '' + Also known as Cabo Verde.: '' + Also known as Czechia.: '' + Also known as East Timor.: '' + Also known as Kiev.: '' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: '' + Also known as the Sea of Cortez.: 'Também conhecido como Mar de Cortez.' + 'Also spelled as Sana''a.': '' + American Samoa: Samoa Americana + Amman: 'Amã' + Amsterdam: 'Amsterdã (BR), Amsterdão (PT)' + 'Andorra (narrower, coat of arms with motto)': 'Andorra (mais estreita, brasão com lema)' + Andorra la Vella: Andorra-a-Velha + Anguilla: Anguila + Ankara: Ancara + Antarctica: 'Antártica' + Antigua and Barbuda: 'Antígua e Barbuda' + Arabian Sea: 'Mar Arábico' + Aral Sea: Mar de Aral + Arctic Ocean: 'Oceano Ártico' + Armenia: 'Armênia (BR), Arménia (PT)' + Ashgabat: Asgabate + Asia: 'Ásia' + 'Asunción': 'Assunção' + Athens: Atenas + Atlantic Ocean: 'Oceano Atlântico' + Australia: 'Austrália' + 'Australia (white stars, two more stars)': 'Austrália (estrelas brancas, duas estrelas a mais)' + Austria: 'Áustria' + 'Austria (brighter red, wider white band)': 'Áustria (vermelho mais brilhante, faixa branca mais larga)' + Autonomous community of Spain.: 'Comunidade autônoma da Espanha.' + Autonomous province of South Korea.: 'Província autônoma da Coreia do Sul.' + Autonomous region of Finland.: 'Região autônoma da Finlândia.' + Autonomous region of Italy.: 'Região autônoma da Itália.' + Autonomous region of Papua New Guinea.: 'Região autônoma de Papua-Nova Guiné.' + Autonomous region of Portugal.: + notes.note.azores.fields.field.country-info: 'Região autônoma de Portugal.' + notes.note.madeira.fields.field.country-info: 'Região Autônoma de Portugal.' + Azerbaijan: 'Azerbaijão' + Azores: 'Açores' + Baghdad: 'Bagdá (BR), Bagdade (PT)' + Bahrain: 'Barém' + 'Bahrain (narrower, fewer serrated edges, red)': 'Barém (mais estreita, menos bordas serrilhadas, vermelha)' + Baku: Bacu + Balkan Peninsula: 'Bálcãs' + Baltic Sea: 'Mar Báltico' + Bamako: Bamaco + Banda Sea: Mar de Banda + Bandar Seri Begawan: 'Bandar Seri Begauã' + Bangkok: 'Bancoque (BR), Banguecoque (PT)' + Bangladesh: Bangladexe + Barents Sea: Mar de Barents + Bay of Bengal: 'Baía de Bengala' + Bay of Biscay: Golfo da Biscaia + Beijing: Pequim + Beirut: Beirute + Belarus: 'Bielorrúsia' + Belfast: Belfaste + Belgium: 'Bélgica' + Belgrade: Belgrado + Belmopan: 'Belmopã' + Benin: Benim + Bering Strait: Estreito de Bering + Berlin: Berlim + Bermuda: Bermudas + Bern: Berna + Bhutan: 'Butão' + Bishkek: Bisqueque + Black Sea: Mar Negro + Bolivia: 'Bolívia' + 'Bolivia (coat of arms instead of star)': ' Bolívia (brasão de armas em vez de estrela)' + Bosnia and Herzegovina: 'Bósnia e Herzegovina' + Botswana: Botsuana + Brazil: Brasil + Brazzaville: Brazavile + British Virgin Islands: 'Ilhas Virgens Britânicas' + Brussels: Bruxelas + Bucharest: Bucareste + Budapest: Budapeste + Bulgaria: 'Bulgária' + Burkina Faso: Burquina Fasso + Cambodia: Camboja + Cameroon: 'Camarões' + 'Cameroon (green/red/yellow, yellow star)': 'Camarões (verde/vermelho/amarelo, estrela amarela)' + Canada: 'Canadá' + Canary Islands: 'Ilhas Canárias' + Canberra: Camberra + Cape Verde: Cabo Verde + Cardiff: Cardife + Caribbean Sea: 'Mar das Caraíbas' + Caspian Sea: 'Mar Cáspio' + Cayman Islands: 'Ilhas Caimã' + Celebes Sea: Mar de Celebes + Celtic Sea: Mar Celta + Central African Republic: 'República Centro-Africana' + Cetinje is an honorary capital.: 'Cetinje é uma capital honorária.' + Chad: Chade + 'Chad (slightly darker blue)': 'Chade (azul ligeiramente mais escuro)' + Charlotte Amalie: 'Carlota Amália' + 'Chișinău': Quixinau + City of San Marino: 'São Marinho' + Ciudad de la Paz: Cidade da Paz + Claimed and controlled: Reivindicado e controlado + Claimed but not controlled: 'Reivindicada, mas não controlada' + Colombia: 'Colômbia' + 'Colombia (no coat of arms)': 'Colômbia (sem brasão)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Colombo é muitas vezes referido como a capital, mas Seri Jaiavardenapura-Cota, um subúrbio de Colombo, é a capital legislativa oficial.' + Comoros: Comores + Conakry: Conacri + Constituent country in the Kingdom of Denmark.: 'País constituinte do Reino da Dinamarca.' + Constituent country of the Kingdom of the Netherlands.: 'País constituinte do Reino dos Países Baixos.' + Constituent country of the United Kingdom.: 'País constituinte do Reino Unido.' + Cook Islands: Ilhas Cook + Copenhagen: 'Copenhague (BR), Copenhaga (PT)' + Coral Sea: Mar de Coral + Corsica: 'Córsega' + Croatia: 'Croácia' + Crown dependency of the United Kingdom.: + notes.note.guernsey.fields.field.country-info: 'Dependência da coroa do Reino Unido.' + notes.note.isle-of-man.fields.field.country-info: Ilha de Man + notes.note.jersey.fields.field.country-info: 'Dependência da coroa do Reino Unido.' + 'Cuba (red triangle, blue stripes)': 'Cuba (triângulo vermelho, listras azuis)' + 'Curaçao (two stars in top-left corner)': 'Curaçao (duas estrelas no canto superior esquerdo)' + Cyprus: Chipre + Czech Republic: 'Tchéquia (BR), Chéquia (PT)' + Dakar: Dacar + Damascus: Damasco + Dead Sea: Mar Morto + Democratic Republic of the Congo: 'República Democrática do Congo' + Denmark: Dinamarca + Denmark Strait: Estreito da Dinamarca + Dhaka: Daca + Dili: 'Díli' + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': 'Disputada; reivindicada por Israel; Ramala é o centro administrativo.' + 'Disputed; claimed by Palestine.': 'Disputado; reivindicado pela Palestina.' + Djibouti: 'Djibuti (BR), Jibuti (PT)' + Doha: Doa + Dominican Republic: 'República Dominicana' + Dublin: Dublim + Dushanbe: 'Duxambé' + East China Sea: Mar da China Oriental + East Siberian Sea: Mar Siberiano Oriental + Ecuador: Equador + 'Ecuador (with coat of arms)': 'Equador (com brasão)' + Edinburgh: Edimburgo + Egypt: 'Egito (BR), Egipto (PT)' + 'Egypt (emblem instead of text), Yemen (no text)': 'Egito (emblema em vez de texto) e Iêmen (sem texto)' + 'Egypt (with emblem), Iraq (with text)': 'Egito (com emblema), e Iraque (com texto)' + 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (brasão diferente, azul ligeiramente mais escuro)' + England: Inglaterra + English Channel: Canal da Mancha + Equatorial Guinea: 'Guiné Equatorial' + Eritrea: Eritreia + Estonia: 'Estônia (BR), Estónia (PT)' + Eswatini: 'Essuatíni' + Ethiopia: 'Etiópia' + Europe: Europa + European Union: 'União Europeia' + Falkland Islands: Ilhas Malvinas + Faroe Islands: 'Ilhas Faroé' + Federated States of Micronesia: 'Estados Federados da Micronésia' + Finland: 'Finlândia' + Formerly Zaire.: '' + Formerly known as Macedonia.: 'Anteriormente conhecida como Macedônia.' + France: 'França' + French Guiana: Guiana Francesa + French Polynesia: 'Polinésia Francesa' + Gabon: 'Gabão' + Georgia: 'Geórgia' + Germany: Alemanha + Ghana: Gana + 'Ghana (star instead of coat of arms)': 'Gana (estrela em vez de brasão)' + Gitega: Guitega + Greece: 'Grécia' + Greenland: 'Groenlândia' + Grenada: Granada + Guadeloupe: Guadalupe + Guatemala City: Cidade da Guatemala + Guernsey: Guernesei + Guinea: 'Guiné' + 'Guinea (green and red flipped, slightly darker green)': 'Guiné (verde e vermelho invertidos, verde ligeiramente mais escuro)' + Guinea-Bissau: 'Guiné-Bissau' + Gulf of Alaska: Golfo do Alasca + Gulf of California: 'Golfo da Califórnia' + Gulf of Carpentaria: 'Golfo de Carpentária' + Gulf of Guinea: 'Golfo da Guiné' + Gulf of Mexico: 'Golfo do México' + Gulf of Thailand: 'Golfo da Tailândia' + Guyana: Guiana + 'Hagåtña': Aganha + Hanoi: 'Hanói' + Hawaii: 'Havaí' + Helsinki: 'Helsinque (BR), Helsínquia (PT)' + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: 'Região histórica e cultural do norte da Europa, que inclui os países da Dinamarca, Noruega e Suécia, e às vezes Finlândia e Islândia.' + Hudson Bay: 'Baía de Hudson' + Hungary: Hungria + Iceland: 'Islândia' + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Islândia (fundo azul, cruz vermelha e branca) e Noruega (fundo vermelho, cruz azul e branca)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Islândia (fundo azul, cruz vermelha) e Ilhas Faroé (fundo branco, cruz vermelha e azul)' + Independent state claimed by Georgia.: 'Estado independente reivindicado pela Geórgia.' + Independent state claimed by Moldova.: 'Estado independente reivindicado pela Moldávia.' + Independent state claimed by Somalia.: 'Estado independente reivindicado pela Somália.' + India: 'Índia' + Indian Ocean: 'Oceano Índico' + Indonesia: 'Indonésia' + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonésia (branco e vermelho invertidos, vermelho mais brilhante) e Mônaco (branco e vermelho invertidos, mais estreita)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonésia (mais larga, vermelho mais brilhante) e Polônia (mais larga, vermelho e branco invertidos)' + Iran: 'Irã (BR), Irão (PT)' + Iraq: Iraque + 'Iraq (text instead of emblem), Yemen (no emblem)': 'Iraque (texto em vez de emblema) e Iêmen (sem emblema)' + Ireland: Irlanda + 'Ireland (orange and green flipped, wider)': 'Irlanda (laranja e verde invertidos, mais larga)' + Islamabad: Islamabade + Island of Indonesia.: 'Ilha da Indonésia.' + Isle of Man: Ilha de Man + Italy: 'Itália' + Ivory Coast: Costa do Marfim + 'Ivory Coast (green and orange flipped, narrower)': 'Costa do Marfim (verde e laranja invertidos, mais estreita)' + Jakarta: Jacarta + Japan: 'Japão' + Jersey: 'Jérsia' + Jerusalem: 'Jerusalém' + Jordan: 'Jordânia' + Kabul: Cabul + Kaliningrad Oblast: Caliningrado + Kampala: Campala + Kathmandu: Catmandu + Kazakhstan: 'Cazaquistão' + Kenya: 'Quênia (BR), Quénia (PT)' + Khartoum: Cartum + Kigali: Quigali + Kinshasa: Quinxassa + Kiribati: Quiribati + Known as Nur-Sultan between 2019 and 2022: 'Conhecida como Nur-Sultã entre 2019 e 2022' + Known as Swaziland until 2018.: 'Conhecido como Suazilândia até 2018.' + Kuala Lumpur: Quala Lumpur + Kuwait City: Kuwait + Kyiv: Quieve + Kyrgyzstan: 'Quirguistão' + Laayoune: Laiune + Labrador Sea: Mar de Labrador + Latvia: 'Letônia (BR), Letónia (PT)' + 'Latvia (darker red, narrower white band)': 'Letônia (vermelho mais escuro, faixa branca mais estreita)' + Lebanon: 'Líbano' + Lesotho: Lesoto + Liberia: 'Libéria' + Libreville: Librevile + Libya: 'Líbia' + Lilongwe: 'Lilongué' + Lisbon: Lisboa + Lithuania: 'Lituânia' + Ljubljana: Liubliana + London: Londres + Lusaka: Lusaca + Luxembourg: Luxemburgo + 'Luxembourg (lighter blue)': 'do Luxemburgo (azul mais claro)' + Luxembourg City: Luxemburgo + Madrid: 'Madri (BR), Madrid (PT)' + Malawi: 'Maláui' + Malaysia: 'Malásia' + Maldives: Maldivas + 'Mali (red and green flipped, slightly brighter green)': 'Mali (vermelho e verde invertidos, verde ligeiramente mais brilhante)' + Managua: 'Manágua' + Mariehamn: Marianhamina + Marshall Islands: Ilhas Marshall + Martinique: Martinica + Mauritania: 'Mauritânia' + Mauritius: 'Maurício (BR), Maurícia (PT)' + Mayotte: Maiote + Mbabane: Mebabane + Mediterranean Sea: 'Mar Mediterrâneo' + Melanesia: 'Melanésia' + Mexico: 'México' + Mexico City: 'Cidade do México' + Micronesia: 'Micronésia' + Minsk: Minsque + Mogadishu: 'Mogadíscio' + Moldova: 'Moldávia' + 'Moldova (wider, coat of arms with eagle)': 'Moldávia (mais larga, brasão com águia)' + Monaco: 'Mônaco (BR), Mónaco (PT)' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Mônaco (mais estreita, vermelho mais escuro) e Polônia (vermelho e branco invertidos, vermelho mais escuro)' + Mongolia: 'Mongólia' + Monrovia: 'Monróvia' + Montevideo: 'Montevidéu (BR), Montevideu (PT)' + Morocco: Marrocos + Moscow: 'Moscou (BR), Moscovo (PT)' + Mozambique: 'Moçambique' + Muscat: Mascate + Myanmar: Mianmar + 'N''Djamena': Jamena + Nairobi: 'Nairóbi' + Namibia: 'Namíbia' + 'Nauru (single star below yellow band)': 'Nauru (estrela única abaixo da faixa amarela)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru não tem capital oficial; o distrito de Iarém é apenas considerada a capital.' + Naypyidaw: 'Nepiedó' + Netherlands: 'Países Baixos' + 'Netherlands (darker blue)': 'Países Baixos (azul mais escuro)' + New Caledonia: 'Nova Caledônia' + New Delhi: Nova Deli + New Zealand: 'Nova Zelândia' + 'New Zealand (red stars, two fewer stars)': 'Nova Zelândia (estrelas vermelhas, duas estrelas a menos)' + Niamey: Niamei + Nicaragua: 'Nicarágua' + 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicarágua (brasão diferente, azul ligeiramente mais claro)' + Nicosia: 'Nicósia' + Niger: 'Níger' + Nigeria: 'Nigéria' + Niue: 'Niuê' + North America: 'América do Norte' + North Korea: Coreia do Norte + North Macedonia: 'Macedônia do Norte (BR), Macedónia do Norte (PT)' + North Nicosia: 'Nicósia do Norte' + North Sea: Mar do Norte + Northern Cyprus: Chipre do Norte + Northern Ireland: Irlanda do Norte + Northern Mariana Islands: Ilhas Marianas do Norte + Norway: Noruega + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Noruega (fundo vermelho, cruz azul) e Ilhas Faroé (fundo branco, cruz vermelha e azul)' + Norwegian Sea: Mar da Noruega + Not a sovereign country: 'Não é um Estado soberano' + Nouakchott: Nuaquexote + 'Nouméa': 'Numeá' + 'Nukuʻalofa': Nucualofa + Nuuk: Nuque + 'Oblast (administrative region) of the Russian Federation.': 'Oblast (região administrativa) da Federação Russa.' + Official capital was moved from Bujumbura to Gitega in 2019.: A capital oficial foi transferida de Bujumbura para Guitega em 2019. + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: A capital oficial foi transferida de Malabo para Cidade da Paz em 2026. + 'Officially Côte d''Ivoire.': 'Oficialmente Côte d''Ivoire.' + Officially Luxembourg.: '' + Oman: 'Omã (BR), Omão (PT)' + Oranjestad: Oranjestade + Ottawa: Otava + Ouagadougou: Uagadugu + Overseas department of France.: 'Departamento ultramarino da França.' + Overseas territory of France.: + notes.note.french-polynesia.fields.field.country-info: 'Território ultramarino da França.' + notes.note.new-caledonia.fields.field.country-info: 'Território ultramarino da França.' + notes.note.saint-martin.fields.field.country-info: 'País constituinte do Reino dos Países Baixos.' + notes.note.wallis-and-futuna.fields.field.country-info: 'Território ultramarino da França.' + Overseas territory of the United Kingdom.: 'Território ultramarino do Reino Unido.' + Pacific Ocean: 'Oceano Pacífico' + Pakistan: 'Paquistão' + Palestine: Palestina + 'Palestine (black/white/green, red arrow)': 'Palestina (preto/branco/verde, seta vermelha)' + 'Palestine (no symbol)': 'Palestina (sem símbolo)' + Palikir: Paliquir + Panama: 'Panamá' + Panama City: 'Cidade do Panamá' + Papeete: Papete + Papua New Guinea: 'Papua-Nova Guiné' + Paraguay: Paraguai + Partially recognised state claimed by China.: Estado parcialmente reconhecido reivindicado pela China. + Partially recognised state claimed by Morocco. Also known as Western Sahara.: Estado parcialmente reconhecido reivindicado por Marrocos. + Partially recognised state claimed by Serbia.: 'Estado parcialmente reconhecido reivindicado pela Sérvia.' + Persian Gulf: 'Golfo Pérsico' + Philippine Sea: Mar das Filipinas + Philippines: Filipinas + Phnom Penh: Penome Pene + Poland: 'Polônia (BR), Polónia (PT)' + Polynesia: 'Polinésia' + Port Louis: 'Porto Luís' + Port Moresby: Porto Moresby + Port Vila: Porto Vila + Port of Spain: Porto da Espanha + Port-au-Prince: 'Porto Príncipe' + Porto-Novo: Porto Novo + Prague: Praga + Pretoria, Cape Town, Bloemfontein: 'Pretória, Cidade do Cabo, Blumefontaina' + Puerto Rico: Porto Rico + 'Puerto Rico (blue triangle, red stripes)': 'Porto Rico (triângulo azul, listras vermelhas)' + Pyongyang: Pionguiangue + Qatar: Catar + 'Qatar (wider, more serrated edges, maroon)': 'Catar (mais larga, bordas serrilhadas, marrom)' + Rabat: Rabate + Red Sea: Mar Vermelho + Region of France.: 'Região da França.' + Republic of the Congo: Congo + 'Reykjavík': Reiquiavique + Riyadh: Riade + Romania: 'Romênia (BR), Roménia (PT)' + 'Romania (slightly lighter blue)': 'Romênia (azul ligeiramente mais claro)' + Rome: Roma + Russia: 'Rússia' + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rússia (sem brasão) e Eslovênia (brasão mais largo e menor)' + Rwanda: Ruanda + 'Réunion': 'Reunião' + Sahrawi Arab Democratic Republic: 'República Árabe Saaraui Democrática (RASD)' + Saint Kitts and Nevis: 'São Cristóvão e Névis' + Saint Lucia: 'Santa Lúcia' + Saint Martin: 'São Martinho (França)' + Saint Vincent and the Grenadines: 'São Vicente e Granadinas' + 'San José': 'São José' + San Juan: 'São João' + San Marino: 'São Marinho' + San Salvador: 'São Salvador' + Sanaa: 'Saná' + Santo Domingo: 'São Domingos' + Sardinia: Sardenha + Saudi Arabia: 'Arábia Saudita' + Scandinavia: 'Escandinávia' + Scotland: 'Escócia' + Sea of Galilee: Mar da Galileia + Sea of Japan: 'Mar do Japão' + Sea of Okhotsk: Mar de Okhotsk + Semi-autonomous region of Tanzania.: 'Região semi-autônoma da Tanzânia.' + 'Senegal (green/yellow/red, green star)': 'Senegal (verde/amarelo/vermelho, estrela verde)' + Seoul: Seul + Serbia: 'Sérvia' + Seychelles: Seicheles + Sicily: 'Sicília' + Sierra Leone: Serra Leoa + Singapore: Singapura + Sint Maarten: 'São Martinho (Países Baixos)' + Skopje: 'Escópia' + Slovakia: 'Eslováquia' + 'Slovakia (narrower, bigger coat of arms)': 'Eslováquia (mais estreita, brasão maior)' + 'Slovakia (with coat of arms)': 'Eslováquia (com brasão)' + Slovenia: 'Eslovênia (BR), Eslovénia (PT)' + Sofia: 'Sófia' + Solomon Islands: 'Ilhas Salomão' + Somalia: 'Somália' + Somaliland: 'Somalilândia' + South Africa: 'África do Sul' + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'A África do Sul não tem capital legalmente definida: os ramos do governo estão divididos em três cidades: Pretória (executivo), Cidade do Cabo (legislativo) e Blumefontaina (judicial).' + South America: 'América do Sul' + South China Sea: Mar da China Meridional + South Korea: Coreia do Sul + South Ossetia: 'Ossétia do Sul' + South Sudan: 'Sudão do Sul' + South Tarawa: Taraua do Sul + Southern Ocean: 'Oceano Antártico' + Sovereign country: Estado soberano + Spain: Espanha + Special Administrative Region of China.: 'Região Administrativa Especial da China.' + Sri Jayawardenepura Kotte: Seri Jaiavardenapura-Cota + 'St. George''s': 'São Jorge' + 'St. John''s': 'São João' + State of the United States.: Estado dos Estados Unidos. + State recognised only by Turkey and claimed by Cyprus.: Estado reconhecido apenas pela Turquia e reivindicado por Chipre. + Stockholm: Estocolmo + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Sub-região da Oceania que compreende milhares de pequenas ilhas no Oceano Pacífico central e meridional.' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Sub-região da Oceania que compreende milhares de pequenas ilhas no oeste do Oceano Pacífico.' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Sub-região da Oceania que inclui Vanuatu, Ilhas Salomão, Fiji e Papua-Nova Guiné.' + Sudan: 'Sudão' + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudão (vermelho/branco/preto, seta verde) e República Árabe Saaraui Democrática (com estrela e crescente)' + Sukhumi: Sucumi + Svalbard: Esvalbarda + Sweden: 'Suécia' + Switzerland: 'Suíca' + 'Switzerland has no official capital; Bern is the de facto capital.': 'A Suíça não tem capital oficial; Berna é apenas considerada a capital.' + Syria: 'Síria' + 'São Tomé and Príncipe': 'São Tomé e Príncipe' + Taipei: 'Taipé' + Tajikistan: 'Tajiquistão' + Tallinn: Talim + Tanzania: 'Tanzânia' + Tashkent: Tasquente + Tasman Sea: Mar de Tasman + Tbilisi: Tiblissi + Tehran: 'Teerão' + Thailand: 'Tailândia' + The Bahamas: Bahamas + The Gambia: 'Gâmbia' + Thimphu: Timbu + Timor Sea: Mar de Timor + Tiraspol: 'Tiráspol' + Tokyo: 'Tóquio' + Transnistria: 'Transnístria' + Trinidad and Tobago: Trindade e Tobago + Tripoli: 'Trípoli' + Tskhinvali: 'Tsequinváli' + Tunis: Tunes + Tunisia: 'Tunísia' + Turkey: Turquia + Turkmenistan: 'Turcomenistão' + Turks and Caicos Islands: Turcas e Caicos + Ukraine: 'Ucrânia' + Ulaanbaatar: 'Ulã Bator' + Unincorporated internal area of Norway.: 'Área interna não incorporada da Noruega.' + Unincorporated territory of the United States.: 'Território não incorporado dos Estados Unidos.' + United Arab Emirates: 'Emirados Árabes Unidos' + United Kingdom: Reino Unido + United States Virgin Islands: Ilhas Virgens Americanas + United States of America: Estados Unidos + Uruguay: Uruguai + Uzbekistan: 'Uzbequistão' + Valletta: Valeta + Vatican City: Vaticano + Victoria: 'Vitória' + Vienna: Viena + Vientiane: Vienciana + Vietnam: 'Vietnã (BR), Vietname (PT)' + Wales: 'País de Gales' + Wallis and Futuna: Wallis e Futuna + Warsaw: 'Varsóvia' + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: 'Embora Amsterdã seja a capital oficial, Haia é a sede dos ramos executivo e legislativo.' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: 'Embora Dodoma seja a capital oficial, Dar es Salã é a sede do governo.' + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': 'Embora Laiune, também conhecida como El Aiune, seja a capital declarada, Tifariti é a sede do governo.' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: 'Embora Mebabane seja a capital oficial e executiva, Lobamba é a capital tradicional, espiritual e legislativa.' + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: 'Embora Porto Novo seja capital oficial, Cotonu é a sede do governo.' + While Sucre is the constitutional capital, La Paz is the seat of government.: 'Embora Sucre seja a capital constitucional, La Paz é a sede do governo.' + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: 'Embora Iamussucro seja a capital oficial, Abidjã é a sede do governo.' + White Sea: Mar Branco + Windhoek: Vinduque + World region covering the Australian continent and most of the islands in the Pacific Ocean.: '' + Yamoussoukro: Iamussucro + 'Yaoundé': 'Iaundé' + Yaren: 'Iarém' + Yellow Sea: Mar Amarelo + Yemen: 'Iêmen (BR), Iémen (PT)' + Yerevan: 'Erevã' + Zagreb: Zagrebe + Zambia: 'Zâmbia' + Zimbabwe: 'Zimbábue' + 'Åland Islands': Alanda + additions: + notes.note.antigua-and-barbuda.fields.field.capital-hint: Estado soberano + notes.note.netherlands.fields.field.country-info: Conhecidos informalmente como Holanda. + notes.note.puerto-rico.fields.field.capital-hint: 'Não é um Estado soberano' + variables: + label.capital-hint.answer: + 'Hint: {{Capital hint}}': 'Dica: {{Capital hint}}' + label.flag: + Flag: Bandeira + label.location: + Location: 'Localização' + note-type.name: + Ultimate Geography: 'Ultimate Geography [PT]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': 'Bandeira parecida com: {{Flag similarity}}.' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': 'tEG*ja>]Nv' + '5JJ2]i)p4': 'C]:=;Tc=R' + 'G/^_##fcPN': 'z!(JMxmH=r' + 'Gn;q2Y)Xe4': 'v]Gu}_Sgmm' + 'H*c[ML+`s=': 'H|DRE(4*?I' + 'He!e=h?_2V': 'L~fP+M7' + LPF4AfaHKR: 'b#XvD_w?e<' + 'Lol+XIvY21': 'L0+1:;EPg/' + 'M$bc0[jYf0': 'k[}Db7_UdK' + 'O~pXkAbd3': 'w@=dG}+guF' + 'QSq%~rFaP-': 'FNcuka+yo(' + 'Qlib4)q2Hk': 'Q0g%)%UY`W' + 'SlOEXfq#|': 'B|Vp[->]3b' + 'W5?6yQZD2': bm_rpQxEpV + '_]DH+g9!$': 'OJ4}BH2QfK' + 'b@M[XTc?}5': 'ef`]u!|V{`' + 'bD?R}i8_[c': 'zCAd}@3:9U' + 'bW)ch:vs1P': 'O0hlf9Z-!z' + 'b[(2`4t&?J': 'leBrcK+h^' + 'b^Try&me1A': M4ikwvZDb7 + 'b`5Q_Z=HQT': 'odvY;N=cs5' + 'bd~Ght5tLR': 'fV^xZWq#U8' + 'bk2ByZIeU+': 'v%N%Db' + 'c^,~EC6Pb2': 'Gjci[`.B}=' + 'ck!6;xK$/^': 'p4FOjAZZk@' + 'cv+Nq9:aXV1}xL2' + 'dUZ^=SE5BG': pZ,nCA,.0R + 'dWA49p}F{k': 'c+r?+))vw9' + 'dY96`C-f(_': 'wDA~K0^p(*' + 'd``B*6:eCx': 'i@oGe!DhQ3' + 'dbP/Cj}sB+': 'JZR=,>T#/~' + 'de=9c:,@j9': 'e_W9-v{whs' + 'dlU7VG|3Gd': 'Oz!#K-Ae}V' + 'd}R:qHj1,7': 'iS2|sUiSh`' + 'e!KSebP}pt': 'C@O711uUdL' + 'e+/O]%*qfk': 'O>f7b9D1hF' + 'e.t$Vi5,>f': 'c#3w!!.a^C' + 'e42BpV,P}C': 'jJMevp,*!q' + 'e5P*D#I{m{': 'fXz}(XSfFj' + 'eHH?U': 'E_&3V&K[)_' + 'euW0jm=wfR': 'h+>APLLb!{' + 'e{e8vi@^PY': 'I1w1@S?{u2' + 'f#4-L[mp5*': 'sktKlO<#lL' + 'f*2@qiHh_1': 'Bmj!9P&|{=' + 'f*LO~>!0PN': 'kG0ycx1-&@' + 'f21=-A~W;~': 'F9/|i`ALwZ' + 'f>c7x2E]Q6': 'w:J$-ahRdF' + 'fRSM{K+;[w': 'c_|S!EHTyr' + 'fT!w8R#>dg': 'M.$!dle5NI' + 'fV`&p81%d(': 'i,9aCetA#N' + fkq7lhLkdH: 'lwH=^9v6=A' + 'fxAN),FAc$': 'hD@WEVZn_<' + 'g-]bb&[.E!': 'sf|,28pc/9' + 'g9v~y/;=z(': 'o{L!(ulPuC' + 'g<[f89~U)A': 'Db5g|/x_nA' + 'gBCR?!O*;E': 'bS{]Ow*V`E' + 'gB|K:+r?1V': EsE.-qmRj + 'gDsLPj(9S#': 'I7oz98q8(-' + 'gFOojQO(MH': 'w=(azb)1FN' + 'gG6SWfM>H0': 'rJI8->gW%l' + 'gRVZ]qJ#>O': 'N*r4!:+/}l' + 'gYIoR`|AxW': 'ctod6{!$KH' + 'gePC:Xt_nW': 'GUDe$&2Ul)' + 'gp[h@]y13l': 'GmXe5(x+Sd' + 'gsS{jR,]|Q': 'hK{#z+aR76' + 'gx:;za!?C9': 'KOyffK#a5H' + 'gz9${p}er*': 'h{SZE<_@pF' + 'g~%pF`(x{u': 'hl9LK`>En1' + 'h&`>n98~V1': 'lj(hdT=ie~' + 'h9Jr(H[=1~': 'msxAK,LCh{' + 'h?a*UKfT_M': 'j?GNhOWK$8' + 'hE5j*e,Q?[': 'Hdh?z^QMqR' + 'hNE9}>;Q&-': 'KgOKBn.!r{' + 'hXNTVF<:SH': 'Dc^-[NKeM9' + 'hZ/V/D&rO`': 'wXo*CTq02' + 'hrI_uiW>E]': 'e;|GubFA|k' + 'h}lnSN_f$_': 'NGbgN#o/Ie' + 'h~5xz+=ke~': 'x1O4C)?pj1' + 'i)]+09JUR&': 'm~e0bQg80^' + 'i2]v|D3@:j': 'w@c5memCCS' + 'iB-8Br~onq': 'HJN6(i^z~8' + 'iIS#Nw]tr]': 'i]HndhS#jS' + 'iR;Um(L/~c=': 'Qm#z-X2)Eh' + 'i^J8&$(1]|': 'phTnzN7*fo' + 'ia8as9sCY|': 'ryXF~Goixn' + 'igiK,ff[eO': 'AySdJa*$Gr' + 'ijP>aJBT!9': 'tf~~,M|yc&' + 'ilGtw#=asM': 'MGy&<;.7Fv' + 'j%*8gN)q>d': 'Q-U(os@S&S' + 'j)i)pB*HJ,': 'lBV|L_Y=Rb' + 'j2IQ=f=w3@': 'H^&r5vlQP5' + j7eEZkzsCZ: 'Pvf0f_)tm@' + 'j9:K,~DQ2v': 'DuTF$#n0`&' + 'jg|}': 'rt/cNV?%Os' + 'jCd]`-=k,:': 'qb(2zU!2v_' + 'jDA-V?/hVj': 'sSCPjs@7rI' + jI9P-f6r3M: 'vP%fqrhm.3' + 'jKIDq|}J&c': 'D9X*P<0(5R' + 'jKO,9y0M;#': 'EIju~|9Wsy' + 'jN^Vc%9OQ5': 'ld@6qzuh4:' + 'jN|NAUP*h}': 'Bs.>u/)4+/' + 'jTNoKo}Bu+': 'iM3lUK!|>*' + 'jX=#G8wu#(': 'u3Kaf5B|v`' + 'jYz[ibrr9m': JQ.625BEIH + 'jcu!gLw/&r': 'wk!g#_2-WE' + 'jrM28*HbyG': 't%glMxI7,6' + 'j|z@PMgdx,': 'Ooz-xs3u9<' + 'k$1_yaF?9#': 'hdIq-F%tt3' + 'k/u1:B%DJH': 'db,aa$e4w1' + 'k0{O[6l]bH': 'KsSd*uaf=t' + 'k1]FH+s8j@': 'tI[-w:S[nz' + 'k9E.p^iv~[H' + 'k[)#L4.[v3': 'gaM:=v}hwy' + 'khB~(m^@Z5': 'AofLiY]`Ti' + 'kkbYQXw5b;': 'DK#M)PH#C[' + 'koI(}OyJ:@': 'L`GYI/ekhp' + 'kopwzzB``A': 'oxXKJj(4f:' + 'k}4/oj#$~s': 'hq<./^6hM?' + 'k}A9]O:0xw': 'f*6$32AnHC' + 'l$2O|9w`F~': 'B|UQP+^>3F' + 'l$zM~ihPFE': 'LtQnEnv]mQ' + 'lI)Ah/E': 'w?EtZ3tDY9' + 'l*j*EE:!Y?': 'ndPX6tuc3[' + 'l+>Ki!j#v+': 'Q9>YoEa|XH' + 'l3Ly8PBxt(': 'd2v?T?w[3T' + 'lAxH.CfJB@': 'yOTi(~YAk6' + 'lH^2D]w;[H': p1SyIlllJ + 'lU@C3z' + 'm?}%V6s8]*': 'zclXW*ihxo' + mJH4kwPF.-: 'IJ3K*pLd9J' + 'm[96fa+HDS': 'ly9aPK_@5B' + mbGXc0y3Pe: 'ly*3s`V(Xf' + 'mbz=7n[0v9': 'O:=Be7T|g>': 'k$9ns[=Fo{' + 'mz[>>AxIr4': 'cIm?2955ae' + 'm{noqE29%I': 'su=H/7(%]k' + 'm{oM,jr>C#': 'Kry$&wZmW`' + 'n&>e`2FO/S': 'B[J}Z4r84@' + 'n+t>2`:;P/': 'BhD{XddD~W' + 'n:7&>YT5A=': 'i6aNJwa%b_' + 'nIfRn/yz': 'dH*XdVw~QD' + 'oa87&PB&Bm': 'e(pOqB.N-d' + 'onMKID2d;D': 'w/S^-U}^gl' + 'o~}KVA:VbI': p.bqdPDaHE + 'p#R$-mG/L': 'HhAtBQ470': 'Gj;ANGAUma' + 'p:SPZ:e/*[': 'b,`C.1o0K]' + 'pDDrFhd%u': 'Q{)8B-]wUq' + 'pYRw[=6;JI': 'v)_Oh`bF,*' + 'p]t8:Tka7q': ',yh{iX8m{' + pkj3RPBUrF: 's@13Ith.ZH' + 'pwwZu{7f~(': 'd}3y)SgyoJ' + 'pz(G*g': 'C]:V&kby#o' + 'q2#Yv[O0:|': 'JIBCWg': 'Lrgy`zP-}Q' + 'q=w[fB$d+$': 'N]c2]O;WnL' + 'r(h9&#wc~B': 'A}C>b%U#S}' + 'r/B|ra%L,.': 'ODBApmZ]n:' + 'r5C646H*+*': 'nzv6%{z8e8' + 'r6BV%|..*5': 'Q%iy;<@JH&' + 'raI-$$|wT': 'I|&C8!2g<|,FWi%': 'BvP+F:QCTH' + 'r?E$4f-d0/': 'Lkpnrm+}N*' + 'rPekx@n9:L': 't]9y*|+tZ' + 'rSneU!3}bn': 'eprA@[$2!T' + 'r]*Ioa%$bK': 'CMz9QB%2X^' + 's#:aIK$9^x': 'L+`m,K,TjN' + 's4E?ZF[E<>': 'M@Uja@7DwC' + 's9-GL*@AXD': 'oV{vjm9,oV' + 's<_WLSB3I/': 'b#,3j{C;Ii' + 's>f40rn,O]': 't1Nxa2/6]g' + 'sB7rZQsR@9w~i^p': 'f*5blT|2d0' + 'skOM(?n$GG': 'zph}.HR/d/' + spKoFRM4if: 'B`AJ:-Smw.' + 'ssbyA9}]QP': 'B|dJ6T]U$e' + 'stRa/S(0$F': 'gm+.@W/3UE' + 'sxOMs+bE8@': 'pF]bu>P': 'w|**t4}*5:' + 't2&ri|[Un:': 't|n7?F8IO|' + t3aZXtpNyi: 'voNLUF#rIP' + 't50y/[AEhF': 'H>U*FzuU!q' + 'tMMtBUpb$&': 'G%yn8F/|RL' + 'tTnCoO/*tk': 'KIg(;uu).*' + 't_h*fbN#:(': 'I[EaKvyq9C' + 't`cagaWApo': 'y#6U?b$7aD' + 'tcneuK7v%`': 'hsYz#zh=Oy' + tfCY4j6KEZ: 'FZZeug$TYn' + 'tj&bK}%P!F': 'IGD[r5m}DP' + 'twrhzJ[k:Z': 'pa6]%Am.&V' + 't|h3|;E4g{': 's1i:%+hNpC' + 'u-`cspdp)D': 'c!=R$K79Ci' + 'u4^lH.xem5': 'cSW!~j9W8E' + 'u;|vR#mVMW': 'qw`r9pt!l-' + 'uH*Q(mcRt5': 'DEMGqHWrE%' + 'uRo^w#p5Be': 'Nj!o5F[Y={' + 'uc61ToT%gt': 'L>wqh,KqZ.' + 'ui3>b-?a@m': 'yC[`HZT>8~' + upiX0g1quR: PiHl6BLN4X + 'uv1fGXHN@)': 'tU-h*1K,YQ3`' + 'va/ky=f5pK': 'yS!)#wTR3,' + 'vf)G?[_?Pf': 'nhJb958%1' + 'vgdj#X?8dB': 'uDFLbP[Qng' + 'vpOTK6tMr@': r79P4ThUyr + 'vppEA|$@~g': 'M2F[Y-o77g' + 'vrn%j%6{nu': 'K]w/Sxim{^' + 'vwc3}.=Z#&': 'fLt!@-+AbO' + 'wa>{k!2cXc': 'PdPaEws@.2' + x2d4n4ADcP: 'm4fmq/A-=@' + 'xb&P*pH(Q': 'fAFgA,TS$w' + 'xqrJ^d[.(O': 'O]6^$Y;9N' + 'yX:tmrd5;]': 'k.5}wu' + 'EWXNcl' + 'SlOEXfq#|': 'Aa(5k`u_H.' + 'W5?6yQZD2': 'N}PXRbob?8' + '_]DH+g9!$': 'JS=q2$2L3]' + 'b@M[XTc?}5': 'L?:>A,WJLB' + 'bD?R}i8_[c': 'k.2O4>+A!V' + 'bW)ch:vs1P': 'vAR>ZSEGi/' + 'b[(2`4t&?J': 'w^9U1RGI}b' + 'b^Try&me1A': 'zR_=AM#e1.' + 'b`5Q_Z=HQT': N.on85Va4D + 'bd~Ght5tLR': 'hb~_{qK>D=' + 'bk2ByZIeU+': 'DNb~sC&J5%' + 'bnEK#)[;qT': 'A)h(R4;N+m' + 'c(,2{yXI#E': 'ERCyEa-V]o' + 'c,5(mUGzYT': 'N&H{fk[c3L' + 'cAlyc~G.y%': 'dQpb|V+vI#' + 'cEO^y)Md[G': 'ym)YXCoojW' + 'cL?8!xP#Wj': 'K{N|AkKxc*' + 'cPO|Cru8n<': 'b9(smVRG!j' + 'cV4idO^r8W': 'emf@Kw7U_W' + 'cYKvF#ptF7': 'A,M2k`~Wd[' + 'c^,~EC6Pb2': 'd`zDA9y~gZ' + 'ck!6;xK$/^': 'i2(em&ob8T' + 'cv+Nq9:a63YEjQ' + 'e!KSebP}pt': 'k}F}r?|uyT' + 'e+/O]%*qfk': 'Jm&ky}FZz?' + 'e.t$Vi5,>f': 'pcq,]2w8O[' + 'e42BpV,P}C': 'w_RRz=p' + 'e5P*D#I{m{': pMLZoOPY5w + 'eHH?U': 'Btv`iTH0LW' + 'euW0jm=wfR': 'CeFx+%T#{9' + 'e{e8vi@^PY': 'il|4oTIh7t' + 'f#4-L[mp5*': 'F9=}W4o(m(' + 'f*2@qiHh_1': 'O6aT:6ldHZ' + 'f*LO~>!0PN': 't2BlE6w@U0' + 'f21=-A~W;~': 'Q-1OO^)n>C' + 'f>c7x2E]Q6': 'wz:QAN8%&+' + 'fRSM{K+;[w': de.tDgRhFi + 'fT!w8R#>dg': 'EMPCtOx;Rj' + 'fV`&p81%d(': 'JqECz@.%4i' + fkq7lhLkdH: 'y!Xu5N$EBT' + 'fxAN),FAc$': Et,iKxkLUP + 'g-]bb&[.E!': pQlr/JYlFs + 'g9v~y/;=z(': 'p*/Yx`XElV' + 'g<[f89~U)A': 'H4;V}Gjc$p' + 'gBCR?!O*;E': 'H+Rb-3u-sn' + 'gB|K:+r?1V': 'gVIrkH[g_z' + 'gDsLPj(9S#': 'u~.H0': 'yXRb0bi*D.' + 'gRVZ]qJ#>O': 'f-#kZAo/ON' + 'gYIoR`|AxW': 'xD;}}nO&|5' + 'gePC:Xt_nW': 'jOH{z(39>-i<' + 'gx:;za!?C9': 'j[3-~lc=uu' + 'gz9${p}er*': 'PiubQg^Ss5' + 'g~%pF`(x{u': 'yJuwsoHp0*' + 'h&`>n98~V1': 'xddNP~dyT_' + 'h9Jr(H[=1~': 'Q]#{ocTF.T' + 'h?a*UKfT_M': 'in7S|U&pW]' + 'hE5j*e,Q?[': 'r[B4}qKEKl' + 'hNE9}>;Q&-': 'ry#$]bo`k}' + 'hXNTVF<:SH': pR,WF6cJZz + 'hZ/V/D&rO`': 'ewx9/IV5o%' + 'h_9=mkZXAH': 'w,s~/*1#Rg' + hoHandbYAy: 'cD]Q9Pd0ZO' + 'hrI_uiW>E]': 'A#PXVu3{OG' + 'h}lnSN_f$_': 'J4U9*&@eJO' + 'h~5xz+=ke~': bms0vPoZ2i + 'i)]+09JUR&': 'g?oII<6[ZM' + 'i2]v|D3@:j': 'GvL1Tm(L/~c=': 'uB1!(|)h6G' + 'i^J8&$(1]|': 'JumHq-w(@9' + 'ia8as9sCY|': 'pwG/f8TA%+' + 'igiK,ff[eO': 'G>bi?;%r/h' + 'ijP>aJBT!9': 'cdC>T>VY4t' + 'ilGtw#=asM': 'jI?2qf!yDb' + 'j%*8gN)q>d': 'LN}*YP~{-L' + 'j)i)pB*HJ,': 'M_^g|}': 'I`2u4=)$rI' + 'jCd]`-=k,:': 'EEpm1lUrG]' + 'jDA-V?/hVj': 'fn`TSL4H+K' + jI9P-f6r3M: 'F,ZHHr/u@K' + 'jKIDq|}J&c': dzu,L2RTO6 + 'jKO,9y0M;#': 'G:w,Y[(aW1' + 'jN^Vc%9OQ5': 'CP<0cpy;,I' + 'jN|NAUP*h}': 'hE`:7lrR4P' + 'jTNoKo}Bu+': 'pJE)w+ZJK%' + 'jX=#G8wu#(': 'Dy8wkcY4!S' + 'jYz[ibrr9m': 'G:i:X1/~' + 'k}A9]O:0xw': 'f6}E@C#_3N' + 'l$2O|9w`F~': 'B}gk}F/NJD' + 'l$zM~ihPFE': 'kOtGr;P3}k' + 'lI)Ah/E': 'pHNtJdHo[e' + 'l*j*EE:!Y?': 'y-F:kD#HUc' + 'l+>Ki!j#v+': gWiiNVbwTm + 'l3Ly8PBxt(': 'FeM1Z:8]S-' + 'lAxH.CfJB@': 'xxk(Bq-Z`<' + 'lH^2D]w;[H': 'qjeAhS!PI:' + 'lU@C3DB_&l`' + 'mi+kf0LTW.': 'q?uiYsL{dZ' + 'mk1VltJl<]': 'E9ORP)qdh9' + 'mk20`,N=`=': 'Fvsev/q[3n' + 'mkgcU7[bA%': 'b5~CCSrPP.' + 'mq)8`hi{hb': 'o.R(%XRfF<' + 'mtd;>7T|g>': 'eTUA#N*+_%' + 'mz[>>AxIr4': '?R^lQY*D*' + 'm{noqE29%I': 'O>:Y)[/m@5' + 'm{oM,jr>C#': 'd;7/rK:;@r' + 'n&>e`2FO/S': 'E_Y_gka2`:;P/': 'yT?~lnZ:gX' + 'n:7&>YT5A=': 'I!PNDUSvR|' + 'nIfRn/yz': 'M~5K,Lq-LX' + 'oa87&PB&Bm': 'dl?k9`s(|K' + 'onMKID2d;D': 'gCZT`M|9xu' + 'o~}KVA:VbI': 'AFNn,$>?6v' + 'p#R$-mG/L': 'hME8pvJ-]J' + 'p.yv>BQ470': A4mx0mYQ.6 + 'p:SPZ:e/*[': 'K9nrzyH&DrFhd%u': 'NgILg': 'M_Aq-@$=Ed' + 'q2#Yv[O0:|': 'oQe#*&QQB' + 'q7do-wLx%E': 'iBkK=.H[[*' + 'q8][)Ri}=q': 'bD5Cj?{})5' + 'q8fF$4,c6_': 'ssdH.@,b*<' + 'q:XyGf#>Wg': 'J(!?!{vv{7' + 'q=w[' + 'q[}7hk(+=e': 'mmn-72S%.C' + 'qiv/4tOlf=': 'IW*fIQ@Vn!' + qvITjNxkLN: 'Nq3Z?3&|oE' + 'qx%>fB$d+$': 'm6|Qi!lWAQ' + 'r(h9&#wc~B': 'i-&Ri-vDye' + 'r/B|ra%L,.': 'i;wkVQEC8~' + 'r5C646H*+*': 'H[sth&^jGj' + 'r6BV%|..*5': 'dm&o}^E:Y%' + 'raI-$$|wT': 'v~cPQn}Wc%' + 'r>g<|,FWi%': 'iqg6(sCS87' + 'r?E$4f-d0/': 'u~Z*V|m1JC' + 'rPekx@n9:L': 'rz5xk#1': '!lbQb6Q:w' + 's9-GL*@AXD': 'OIwD?Vq[H}' + 's<_WLSB3I/': 'Gir-4TL#W$' + 's>f40rn,O]': 'HE)H&.0Xn6' + 'sB7rZQsRKc}4kE=' + sM0GPt1HS1: 'c3fq*,<:~U' + 'sRw-ik:I$v': 'PB_{Euk>>/' + 'sSW/?WMy17': 'J@?!E/?|:y' + 'sT5=H|@KU|': 'erbhQg!RK]' + sXzvT.g31p: 'q|wSo|9[WL' + 'sY>@9w~i^p': 'N>!/r-|iKh' + 'skOM(?n$GG': 'OB*1GftSKF' + spKoFRM4if: 'yR{s}{|vzP' + 'ssbyA9}]QP': 'h46=t>$v#7' + 'stRa/S(0$F': 'w<=bLWa9^e' + 'sxOMs+bE8@': 'nNI(>d`5|I' + 'sx|l+io`B@': 'i>!/t@^}^?' + 's|K&2S,UP|': 'MHvNbPV}Qa' + 't$y1C2R-Av': 'Hmbq~BF@]H' + 't+v9b--,qs': 'mBU4P!~(nj' + 't-0>F]bu>P': 'uoblR~tJv{' + 't2&ri|[Un:': 'cz=5m*@Fez' + t3aZXtpNyi: 'Pkxoj6=pZF' + 't50y/[AEhF': 'zRmOT{Xeqm' + 'tMMtBUpb$&': 'C&k%h=UUn#' + 'tTnCoO/*tk': 'LE9jC#sSOE' + 't_h*fbN#:(': 'QUEZaS<.i&' + 't`cagaWApo': taEpjjAUDi + 'tcneuK7v%`': 'J2r:;Tb}bM' + tfCY4j6KEZ: mdpMDyaJo8 + 'tj&bK}%P!F': 'vqizD*x9.v' + 'twrhzJ[k:Z': 'if&Ipt4{R!' + 't|h3|;E4g{': 'g_banf#h(Q' + 'u-`cspdp)D': 'QF&bT0`se)' + 'u4^lH.xem5': 'n8DMh4+UMz' + 'u;|vR#mVMW': 'xBC@m/Re-7' + 'uH*Q(mcRt5': 'iT]9Jy+|:B' + 'uRo^w#p5Be': 'Gn220)gYMN' + 'uc61ToT%gt': 'J/Z,LIAPP>' + 'ui3>b-?a@m': 'vC~!WO}1Yx' + upiX0g1quR: 'H16eX1]VWw' + 'uv1fGXHN@)': 't9Ryk}`qj2' + 'u|I}oElaH)': 'L_#MCSsliG' + 'vE[T$`n9T)': 'O,{dn7CExu' + 'vKx<0Qa!aP': 'D&9$.-JH&Q' + 'vNiD6A.v#s': 'hqBB({k!2cXc': 'cPg&WpVKO~' + x2d4n4ADcP: 'J3~et+v|e@' + 'xb&P*pH(Q': 'B[,~m31[Og' + 'xqrJ^d[.(O': 'b/*Gfh]U>A' + 'yX:tmrd5;]': 'M0{EnlSZ[&' + 'z:*d{z(~V2': 'fs]5;0c;}B' + 'zSc]-^U=0=': 'hpvj)-!USf' + crowdanki:uuid: + 43e2586a-9a65-11e8-a777-a0481cc15658: 2aa62e36-601e-4e4c-a124-5a79b14f8697 diff --git a/fixtures/ultimate-geography/overlays/languages/sv.yaml b/fixtures/ultimate-geography/overlays/languages/sv.yaml new file mode 100644 index 0000000..b81a300 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/sv.yaml @@ -0,0 +1,772 @@ +id: overlay.translation.sv +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': 'Sydkorea förespråkar namnet Östhavet, Nordkorea Ostkoreanska havet.' + Abkhazia: Abchazien + Addis Ababa: Addis Abeba + Adriatic Sea: Adriatiska havet + Aegean Sea: Egeiska havet + Africa: Afrika + Akrotiri and Dhekelia: Akrotiri och Dhekelia + Albania: Albanien + Algeria: Algeriet + Algiers: Alger + Also known as Burma.: 'Även känt som Burma.' + Also known as Cabo Verde.: '' + Also known as Czechia.: '' + Also known as East Timor.: 'Även känt som Timor-Leste.' + Also known as Kiev.: '' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: 'Även känd som Siambukten.' + Also known as the Sea of Cortez.: 'Även känt som Cortez hav.' + 'Also spelled as Sana''a.': '' + American Samoa: Amerikanska Samoa + 'Andorra (narrower, coat of arms with motto)': 'Andorra (smalare, vapensköld med valspråk)' + Antarctica: Antarktis + Antigua and Barbuda: Antigua och Barbuda + Arabian Sea: Arabiska havet + Aral Sea: 'Aralsjön' + Arctic Ocean: Norra ishavet + Armenia: Armenien + Ashgabat: Asjchabad + Asia: Asien + Athens: Aten + Atlantic Ocean: Atlanten + Australia: Australien + 'Australia (white stars, two more stars)': 'Australien (vita stjärnor, två fler stjärnor)' + Austria: 'Österrike' + 'Austria (brighter red, wider white band)': 'Österrike (ljusare röd, bredare vitt band)' + Autonomous community of Spain.: Spansk autonom region. + Autonomous province of South Korea.: 'Autonom region i Sydkorea; tidigare känd som Quelpart.' + Autonomous region of Finland.: 'Självstyrande del av Finland.' + Autonomous region of Italy.: Italiensk autonom region. + Autonomous region of Papua New Guinea.: Autonom region i Papua Nya Guinea. + Autonomous region of Portugal.: + notes.note.azores.fields.field.country-info: 'Portugisisk autonom ögrupp.' + notes.note.madeira.fields.field.country-info: Portugisisk autonom region. + Azerbaijan: Azerbajdzjan + Azores: Azorerna + Baghdad: Bagdad + 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrain (smalare, färre tänder, röd)' + Balkan Peninsula: 'Balkanhalvön' + Baltic Sea: 'Östersjön' + Banda Sea: 'Bandasjön' + Barents Sea: Barents hav + Bay of Bengal: Bengaliska viken + Bay of Biscay: Biscayabukten + Beijing: Peking + Belgium: Belgien + Belgrade: Belgrad + Bering Strait: Berings sund + Bishkek: Bisjkek + Black Sea: Svarta havet + 'Bolivia (coat of arms instead of star)': 'Bolivia (vapensköld istället för stjärna)' + Bosnia and Herzegovina: Bosnien och Hercegovina + Brazil: Brasilien + British Virgin Islands: 'Brittiska Jungfruöarna' + Brussels: Bryssel + Bucharest: Bukarest + Bulgaria: Bulgarien + Cairo: Kairo + Cambodia: Kambodja + Cameroon: Kamerun + 'Cameroon (green/red/yellow, yellow star)': 'Kamerun (grön/röd/gul, gul stjärna)' + Canada: Kanada + Canary Islands: 'Kanarieöarna' + Cape Verde: Kap Verde + Caribbean Sea: Karibiska havet + Caspian Sea: Kaspiska havet + Cayman Islands: 'Caymanöarna' + Celebes Sea: 'Sulawesisjön' + Celtic Sea: Keltiska havet + Central African Republic: Centralafrikanska republiken + Cetinje is an honorary capital.: 'Cetinje är hedershuvudstad.' + Chad: Tchad + 'Chad (slightly darker blue)': 'Tchad (aningen mörkare blå)' + China: Kina + City of San Marino: San Marino + Claimed and controlled: 'Hävdas och kontrolleras' + Claimed but not controlled: 'Påstått men inte kontrollerat' + 'Colombia (no coat of arms)': 'Colombia (ingen vapensköld)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Kallas även Kotte eller Sri Jayawardenapura Kotte; Colombo refereras ofta till som huvudstad, men förorten Sri Jayawardenapura är officiell huvudstad.' + Comoros: Komorerna + Constituent country in the Kingdom of Denmark.: Riksdel av Danmark. + Constituent country of the Kingdom of the Netherlands.: 'Ett av fyra länder som utgör staten Nederländerna.' + Constituent country of the United Kingdom.: Riksdel av Storbritannien. + Cook Islands: 'Cooköarna' + Copenhagen: 'Köpenhamn' + Coral Sea: Korallhavet + Corsica: Korsika + Croatia: Kroatien + Crown dependency of the United Kingdom.: Brittisk kronbesittning. + Cuba: Kuba + 'Cuba (red triangle, blue stripes)': 'Kuba (röd triangel, blå ränder)' + 'Curaçao (two stars in top-left corner)': 'Curaçao (två stjärnor i övre vänstra hörnet)' + Cyprus: Cypern + Czech Republic: Tjeckien + Damascus: Damaskus + Dead Sea: 'Döda havet' + Democratic Republic of the Congo: Kongo-Kinshasa + Denmark: Danmark + Denmark Strait: Danmarksundet + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': 'Omstridd; hävdas av Israel; Ramallah är det administrativa centrumet.' + 'Disputed; claimed by Palestine.': 'Omstridd; hävdas av Palestina.' + Dominican Republic: Dominikanska republiken + Dushanbe: Dusjanbe + East China Sea: 'Östkinesiska havet' + East Siberian Sea: 'Östsibiriska havet' + 'Ecuador (with coat of arms)': 'Ecuador (med vapensköld)' + Egypt: Egypten + 'Egypt (emblem instead of text), Yemen (no text)': 'Egypten (statsvapen istället för text), Jemen (ingen text)' + 'Egypt (with emblem), Iraq (with text)': 'Egypten (med emblem), Irak (med text)' + 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (annan vapensköld, aningen mörkare blå)' + English Channel: Engelska kanalen + Equatorial Guinea: Ekvatorialguinea + Estonia: Estland + Eswatini: Swaziland + Ethiopia: Etiopien + Europe: Europa + European Union: Europeiska unionen + Falkland Islands: 'Falklandsöarna' + Faroe Islands: 'Färöarna' + Federated States of Micronesia: Mikronesiens federerade stater + Formerly Zaire.: Formellt Demokratiska republiken Kongo. + Formerly known as Macedonia.: 'Tidigare känt som Makedonien.' + France: Frankrike + French Guiana: Franska Guyana + French Polynesia: Franska Polynesien + Georgia: Georgien + Germany: Tyskland + 'Ghana (star instead of coat of arms)': 'Ghana (stjärnor istället för vapensköld)' + Greece: Grekland + Greenland: 'Grönland' + 'Guinea (green and red flipped, slightly darker green)': 'Guinea (grön och röd växlade, aningen mörkare grön)' + Gulf of Alaska: Alaskagolfen + Gulf of California: Californiaviken + Gulf of Carpentaria: Carpentariaviken + Gulf of Guinea: Guineabukten + Gulf of Mexico: Mexikanska golfen + Gulf of Thailand: Thailandviken + Havana: Havanna + Helsinki: Helsingfors + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: 'Skandinavien består av Sverige, Norge och Danmark. Norden inkluderar även Finland och Island, och utomskandinaviska områden som Svalbard och Grönland. ' + Hong Kong: Hongkong + Hungary: Ungern + Iceland: Island + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Island (blå bakgrund, rött och vitt kors), Norge (röd bakgrund, blått och vitt kors)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Island (blå bakgrund, rött kors), Färöarna (vit bakgrund, rött och blått kors)' + Independent state claimed by Georgia.: 'Delvis erkänd stat, görs anspråk på av Georgien.' + Independent state claimed by Moldova.: 'Självutropad stat, görs anspråk på av Moldavien.' + Independent state claimed by Somalia.: 'Självutropad stat, görs anspråk på av Somalien.' + India: Indien + Indian Ocean: Indiska oceanen + Indonesia: Indonesien + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesien (vit och röd växlade, ljusare röd), Monaco (vit och röd växlade, smalare)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesien (bredare, ljusare röd), Polen (röd och vit växlade, bredare)' + Iraq: Irak + 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (text istället för statsvapen), Jemen (inget statsvapen)' + Ireland: Irland + 'Ireland (orange and green flipped, wider)': 'Irland (orange och grön växlade, bredare)' + Island of Indonesia.: 'Ö i Indonesien.' + Italy: Italien + Ivory Coast: Elfenbenskusten + 'Ivory Coast (green and orange flipped, narrower)': 'Elfenbenskusten (grön och orange växlade, smalare)' + Jordan: Jordanien + Kaliningrad Oblast: Kaliningrad oblast + Kathmandu: Katmandu + Kazakhstan: Kazakstan + Known as Nur-Sultan between 2019 and 2022: Hette Nur-Sultan mellan 2019 och 2022 + Known as Swaziland until 2018.: 'Även känt som Eswatini.' + Kuwait City: Kuwait + Kyiv: Kiev + Kyrgyzstan: Kirgizistan + Laayoune: al-Ayun + Labrador Sea: Labradorhavet + Latvia: Lettland + 'Latvia (darker red, narrower white band)': 'Lettland (mörkare röd, smalare vitt band)' + Lebanon: Libanon + Libya: Libyen + Lisbon: Lissabon + Lithuania: Litauen + Luxembourg: Luxemburg + 'Luxembourg (lighter blue)': 'Luxemburg (ljusare blå)' + Luxembourg City: Luxemburg + Macau: Macao + Madagascar: Madagaskar + Maldives: Maldiverna + 'Mali (red and green flipped, slightly brighter green)': 'Mali (röd och grön växlade, aningen ljusare grön)' + Marshall Islands: 'Marshallöarna' + Mauritania: Mauretanien + Mediterranean Sea: Medelhavet + Melanesia: Melanesien + Mexico: Mexiko + Micronesia: Mikronesien + Moldova: Moldavien + 'Moldova (wider, coat of arms with eagle)': 'Moldavien (bredare, vapensköld med örn)' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (smalare, mörkare röd), Polen (röd och vit växlade, mörkare röd)' + Mongolia: Mongoliet + Morocco: Marocko + Moscow: Moskva + Mozambique: 'Moçambique' + Muscat: Muskat + 'Nauru (single star below yellow band)': 'Nauru (ensam stjärna under den gula randen)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru saknar städer, Yaren är de facto huvudort.' + Netherlands: 'Nederländerna' + 'Netherlands (darker blue)': 'Nederländerna (mörkare blå)' + New Caledonia: Nya Kaledonien + New Zealand: Nya Zeeland + 'New Zealand (red stars, two fewer stars)': 'Nya Zeeland (röda stjärnor, två färre stjärnor)' + 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (annan vapensköld, aningen ljusare blå)' + North America: Nordamerika + North Korea: Nordkorea + North Macedonia: Nordmakedonien + North Nicosia: Norra Nicosia + North Sea: 'Nordsjön' + Northern Cyprus: Nordcypern + Northern Ireland: Nordirland + Northern Mariana Islands: Nordmarianerna + Norway: Norge + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norge (röd bakgrund, blått kors), Färöarna (vit bakgrund, rött och blått kors)' + Norwegian Sea: Norska havet + Not a sovereign country: 'Ej suverän stat' + 'Nukuʻalofa': 'Nuku''alofa' + 'Oblast (administrative region) of the Russian Federation.': 'Även känt som Ryska Östpreussen.' + Oceania: Oceanien + Official capital was moved from Bujumbura to Gitega in 2019.: 'Tidigare känt som Kitega; flyttat från Bujumbura i slutet av 2018.' + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: 'Flyttat från Malabo i 2026.' + 'Officially Côte d''Ivoire.': '' + Officially Luxembourg.: '' + Overseas department of France.: + notes.note.french-guiana.fields.field.country-info: Franskt utomeuropeiskt departement. + notes.note.guadeloupe.fields.field.country-info: Utomeuropeiskt franskt departement. + notes.note.martinique.fields.field.country-info: Franskt utomeuropeiskt departement. + notes.note.mayotte.fields.field.country-info: Franskt utomeuropeiskt departement. + notes.note.r-union.fields.field.country-info: Franskt utomeuropeiskt departement. + Overseas territory of France.: + notes.note.french-polynesia.fields.field.country-info: 'Franskt utomeuropeiskt förvaltningsområde.' + notes.note.new-caledonia.fields.field.country-info: Franskt utomeuropeiskt territorium. + notes.note.saint-martin.fields.field.country-info: 'Franskt utomeuropeiskt område.' + notes.note.wallis-and-futuna.fields.field.country-info: 'Franskt utomeuropeiskt område.' + Overseas territory of the United Kingdom.: + notes.note.akrotiri-and-dhekelia.fields.field.country-info: 'Brittiska “suveräna basområden”.' + notes.note.anguilla.fields.field.country-info: Brittiskt utomeuropeiskt territorium. + notes.note.bermuda.fields.field.country-info: Brittiskt utomeuropeiskt territorium. + notes.note.british-virgin-islands.fields.field.country-info: Brittiskt utomeuropeiskt territorium. + notes.note.cayman-islands.fields.field.country-info: Brittiskt utomeuropeiskt territorium. + notes.note.falkland-islands.fields.field.country-info: Brittiskt utomeuropeiskt territorium. + notes.note.gibraltar.fields.field.country-info: Brittiskt territorium. + notes.note.turks-and-caicos-islands.fields.field.country-info: Brittiskt utomeuropeiskt territorium. + Pacific Ocean: Stilla havet + Palestine: Palestina + 'Palestine (black/white/green, red arrow)': 'Palestina (svart/vit/grön, röd triangel)' + 'Palestine (no symbol)': 'Palestina (ingen symbol)' + Papua New Guinea: Papua Nya Guinea + Partially recognised state claimed by China.: 'Delvis erkänd stat, görs anspråk på av Kina.' + Partially recognised state claimed by Morocco. Also known as Western Sahara.: 'Även känt som Västsahariska republiken.' + Partially recognised state claimed by Serbia.: 'Delvis erkänd stat, görs anspråk på av Serbien.' + Persian Gulf: Persiska viken + Philippine Sea: 'Filippinska sjön' + Philippines: Filippinerna + Poland: Polen + Polynesia: Polynesien + Prague: Prag + Pretoria, Cape Town, Bloemfontein: Pretoria, Kapstaden, Bloemfontein + 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blå triangel, röda ränder)' + 'Qatar (wider, more serrated edges, maroon)': 'Qatar (bredare, fler tänder, rödbrun)' + Red Sea: 'Röda havet' + Region of France.: Fransk region. + Republic of the Congo: Kongo-Brazzaville + 'Reykjavík': Reykjavik + Romania: 'Rumänien' + 'Romania (slightly lighter blue)': 'Rumänien (aningen ljusare blå)' + Rome: Rom + Russia: Ryssland + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Ryssland (ingen vapensköld), Slovenien (bredare, mindre vapensköld)' + Sahrawi Arab Democratic Republic: Sahariska arabiska demokratiska republiken + Saint Kitts and Nevis: Saint Kitts och Nevis + Saint Vincent and the Grenadines: Saint Vincent och Grenadinerna + Santiago: Santiago de Chile + Sardinia: Sardinien + Saudi Arabia: Saudiarabien + Scandinavia: Skandinavien + Scotland: Skottland + Sea of Galilee: 'Gennesaretsjön' + Sea of Japan: Japanska havet + Sea of Okhotsk: Ochotska havet + Semi-autonomous region of Tanzania.: 'Delvis självstyrande örike i union med Tanzania.' + 'Senegal (green/yellow/red, green star)': 'Senegal (grön/gul/röd, grön stjärna)' + Serbia: Serbien + Seychelles: Seychellerna + Sicily: Sicilien + Slovakia: Slovakien + 'Slovakia (narrower, bigger coat of arms)': 'Slovakien (smalare, större vapensköld)' + 'Slovakia (with coat of arms)': 'Slovakien (med vapensköld)' + Slovenia: Slovenien + Solomon Islands: 'Salomonöarna' + South Africa: Sydafrika + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'Sydafrika har ingen lagstadgad huvudstad, regeringen är delad över tre städer: Pretoria (administrativ), Kapstaden (legislativ), Bloemfontein (juridisk).' + South America: Sydamerika + South China Sea: Sydkinesiska havet + South Korea: Sydkorea + South Ossetia: Sydossetien + South Sudan: Sydsudan + Southern Ocean: Antarktiska oceanen + Sovereign country: 'Suverän stat' + Spain: Spanien + Special Administrative Region of China.: + notes.note.hong-kong.fields.field.country-info: 'Särskild administrativ region i Kina.' + notes.note.macau.fields.field.country-info: 'Särskild administrativ region i Kina, stavas alternativt Macau.' + Sri Jayawardenepura Kotte: Sri Jayawardenapura + 'St. George''s': 'Saint George''s' + 'St. John''s': 'Saint John''s' + State of the United States.: Amerikansk delstat. + State recognised only by Turkey and claimed by Cyprus.: 'Enbart erkänd av Turkiet, görs anspråk på av Cypern.' + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Delregion av Oceanien bestående av tusentals småöar i centrala och södra Stilla havet.' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Delregion av Oceanien bestående av tusentals småöar i västra Stilla havet.' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Delregion av Oceanien bestående av bland andra Vanuatu, Salomonöarna, Fiji och Papua Nya Guinea.' + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (röd/vit/svart, grön triangel), Sahariska arabiska demokratiska republiken (med stjärna och halvmåne)' + Sukhumi: Suchumi + Suriname: Surinam + Sweden: Sverige + Switzerland: Schweiz + 'Switzerland has no official capital; Bern is the de facto capital.': 'Schweiz har ingen officiell huvudstad; Bern är de facto huvudstad.' + Syria: Syrien + 'São Tomé and Príncipe': 'São Tomé och Príncipe' + Tajikistan: Tadzjikistan + Tashkent: Tasjkent + Tasman Sea: Tasmanhavet + Tehran: Teheran + The Bahamas: Bahamas + The Gambia: Gambia + Timor Sea: 'Timorsjön' + Timor-Leste: 'Östtimor' + Transnistria: Transnistrien + Trinidad and Tobago: Trinidad och Tobago + Tskhinvali: Tschinvali + Tunisia: Tunisien + Turkey: Turkiet + Turks and Caicos Islands: 'Turks- och Caicosöarna' + Ukraine: Ukraina + Ulaanbaatar: Ulan Bator + Unincorporated internal area of Norway.: 'Norsk ögrupp.' + Unincorporated territory of the United States.: 'Amerikanskt icke-inkorporerat område.' + United Arab Emirates: 'Förenade Arabemiraten' + United Kingdom: Storbritannien + United States Virgin Islands: 'Amerikanska Jungfruöarna' + United States of America: USA + Vatican City: + notes.note.vatican-city.fields.field.capital: Vatikanstaden + notes.note.vatican-city.fields.field.country: Vatikanstaten + Vienna: Wien + Wallis and Futuna: 'Wallis- och Futunaöarna' + Warsaw: Warszawa + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: 'Haag är administrativ huvudstad.' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: 'Dar es-Salaam är de facto administrativ huvudstad.' + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': 'Stavas även al-''Ayun, El Aaiún; Tifariti är de facto säte för parlamentet.' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: 'Lobamba är traditionell och lagstiftande huvudstad.' + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: 'Cotonou är administrativ huvudstad.' + While Sucre is the constitutional capital, La Paz is the seat of government.: 'La Paz är säte för regeringen.' + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: 'Stavas även Yamassoukro; Abidjan är de facto administrativ huvudstad.' + White Sea: Vita havet + World region covering the Australian continent and most of the islands in the Pacific Ocean.: 'Världsdel bestående av Australien och majoriteten av öarna och ögrupperna i Stilla havet.' + Yellow Sea: Gula havet + Yemen: Jemen + Yerevan: Jerevan + 'Åland Islands': 'Åland' + additions: + notes.note.arabian-sea.fields.field.country-info: 'Även känt som Arabiska sjön.' + notes.note.bahrain.fields.field.country-info: Stavas alternativt Bahrein. + notes.note.balkan-peninsula.fields.field.country-info: 'Även känt som Balkan.' + notes.note.belarus.fields.field.country-info: 'Även känt som Vitryssland.' + notes.note.federated-states-of-micronesia.fields.field.country-info: 'Även känt som Mikronesiska federationen.' + notes.note.mongolia.fields.field.capital-info: 'Stavas även Ulaanbaatar.' + notes.note.myanmar.fields.field.capital-info: 'Stavas även Nay Pyi Taw.' + notes.note.persian-gulf.fields.field.country-info: 'Även känd som Iranska viken.' + notes.note.qatar.fields.field.country-info: Stavas alternativt Katar. + notes.note.republic-of-the-congo.fields.field.country-info: Formellt Republiken Kongo. + notes.note.sea-of-galilee.fields.field.country-info: 'Även känd som Galileiska sjön och Tiberiassjön.' + notes.note.united-kingdom.fields.field.country-info: 'Formellt Förenade kungariket.' + notes.note.united-states-of-america.fields.field.country-info: 'Formellt Amerikas förenta stater, även känt som Förenta staterna.' + notes.note.yellow-sea.fields.field.country-info: 'Kallas Västhavet i Nord- och Sydkorea.' + notes.note.yemen.fields.field.country-info: Stavas alternativt Yemen. + variables: + label.capital: + Capital: Huvudstad + label.capital-hint.answer: + 'Hint: {{Capital hint}}': 'Ledtråd: {{Capital hint}}' + label.flag: + Flag: Flagga + label.location: + Location: 'Läge' + note-type.name: + Ultimate Geography: 'Ultimate Geography [SV]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': 'Flaggan liknar {{Flag similarity}}.' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': 'HU{V)h>->1' + '5JJ2]i)p4': 'ch9]RU,k}a' + '5S(f7%O-$': 'u?NN' + 'EWXNc<6ju' + LPF4AfaHKR: 'DMw7[z1|/f' + 'Lol+XIvY21': 'AUQ5QqT7N~' + 'M$bc0[jYf0': 'B4o4Qgpxa=' + 'O~pXkAbd3': 'u._5@pRbGb' + 'QSq%~rFaP-': 'A`,,|zaW//' + 'Qlib4)q2Hk': 'Lg|45@I{1+' + 'SlOEXfq#|': 'u>bFUX4,^(' + 'W5?6yQZD2': 'Bp:8AxZIJ*' + '_]DH+g9!$': 'g*C+;[^vP9' + 'b@M[XTc?}5': 'OzZ|9/zKGy' + 'bD?R}i8_[c': 'B0j5`h}ycY' + 'bW)ch:vs1P': 'hWSXAL,1[1' + 'b[(2`4t&?J': 'P$;>yK&Bb#' + 'b^Try&me1A': 'N@2CXFim}.' + 'b`5Q_Z=HQT': 'MPv~EB{R-o' + 'bd~Ght5tLR': 'FBWKraq$]`' + 'bk2ByZIeU+': 'GO8K>ugF' + 'c(,2{yXI#E': '(;;{-bktU' + 'c,5(mUGzYT': 'b.K2|uT9#&' + 'cAlyc~G.y%': 'p){pPjR5:$' + 'cEO^y)Md[G': 'ite]=1-[IW' + 'cL?8!xP#Wj': 'ya#p*nPn}U' + 'cPO|Cru8n<': 'b4O.fdq=&&' + 'cV4idO^r8W': 'f&H7U0-[5}' + 'cYKvF#ptF7': 'P;wx%Rx}g%' + 'c^,~EC6Pb2': 'KG~t}M~,_o' + 'ck!6;xK$/^': 'vHOU`CM:P+' + 'cv+Nq9:a$PS' + 'd3c`&/1Y^D': 'y!.rR1N]Q%' + 'd5m8%D6,;u': 't!YaMvtxM)' + 'dC:e5G4J/p': 'iZ`BoOu7TH' + 'dUZ^=SE5BG': '^(x:orV#n' + 'dWA49p}F{k': 'O%#xYWz^#M' + 'dY96`C-f(_': KUSv4_ZftC + 'd``B*6:eCx': 'eItZ|YXm$3' + 'dbP/Cj}sB+': yychoEU/n9 + 'de=9c:,@j9': 'o6f7z~ZCr6' + 'dlU7VG|3Gd': 'f*A:Z4bkRD' + 'd}R:qHj1,7': 'pG~&s-.Pv=' + 'e!KSebP}pt': 'EnHK+Ktzh+' + 'e+/O]%*qfk': 'M@9EfpNi;z' + 'e.t$Vi5,>f': 'eqi30~lJCU' + 'e42BpV,P}C': 'o@l9/@&CEQ' + 'e5P*D#I{m{': 'p;W>c.=a5.' + 'eHH?U': 'iWh/XzA:Fx' + 'euW0jm=wfR': 'F5Fbm]10e3' + 'e{e8vi@^PY': 'O8La>q*+=D' + 'f#4-L[mp5*': 'nh!${YsPie' + 'f*2@qiHh_1': 'fL!0PN': 't1sE<.qtO/' + 'f21=-A~W;~': 'qv@;*gj=VH' + 'f>c7x2E]Q6': 'O|!r8pZc_@' + 'fRSM{K+;[w': 'x2Ho#9JF2&' + 'fT!w8R#>dg': 'taGg_[({a>' + 'fV`&p81%d(': 'N&=0FE?0K3' + fkq7lhLkdH: 'x}$b4LJvm3' + 'fxAN),FAc$': 'QyZ%*OdzFY' + 'g-]bb&[.E!': 'H/1j+}vOwc' + 'g9v~y/;=z(': caoMw/l6Qq + 'g<[f89~U)A': 'eRztGZ?5gt' + 'gBCR?!O*;E': 'wY{he1D;Tc' + 'gB|K:+r?1V': 'ncoVNK:5c/' + 'gDsLPj(9S#': 'EykZ%2$zVY' + 'gFOojQO(MH': 'n$t4JTV.L[' + 'gG6SWfM>H0': 'c3_S$ge,H%' + 'gRVZ]qJ#>O': 'ywSb;Oor?l' + 'gYIoR`|AxW': 'm9{}Vj*h)B' + 'gePC:Xt_nW': 'we!,qZc~VX' + 'gp[h@]y13l': 'o6Q=4QRc:P' + 'gsS{jR,]|Q': 'DnUNQy>V@c' + 'gx:;za!?C9': 'O6XK(yEdjN' + 'gz9${p}er*': 'O:){fAE+[h' + 'g~%pF`(x{u': 'qvU`=w(275' + 'h&`>n98~V1': i7sQICmsKP + 'h9Jr(H[=1~': 'lg>S)d~UyQ' + 'h?a*UKfT_M': 'uU![?ri)d_' + 'hE5j*e,Q?[': 'oVtF.y]H)M' + 'hNE9}>;Q&-': 'Ae@YR@cdSW' + 'hXNTVF<:SH': 'FO/D@z0E%#' + 'hZ/V/D&rO`': 'k^PtQpRdLQ' + 'h_9=mkZXAH': 'q*^h!N31k*' + hoHandbYAy: 'heo$u3%xYs' + 'hrI_uiW>E]': 'L3Z}n~/Szn' + 'h}lnSN_f$_': 'yp(op%kwx]' + 'h~5xz+=ke~': 'Gpeahm(L/~c=': 'c=F0q+d|m#' + 'i^J8&$(1]|': 'ugyWN%[pXR' + 'ia8as9sCY|': 'b2;iz|xg^@' + 'igiK,ff[eO': 'N`eaJBT!9': 'LH]h^.GA_i' + 'ilGtw#=asM': 'Qi.AoC*UvT' + 'j%*8gN)q>d': 'uVs:]g8l.&' + 'j)i)pB*HJ,': 'OlYF5,:|Q3' + 'j2IQ=f=w3@': 'B;S:g|}': 'rJG$qf|FO4' + 'jCd]`-=k,:': 'QO[lJt~Q]N' + 'jDA-V?/hVj': 'GilXrl.A_%' + jI9P-f6r3M: 'z}n.If4:?)' + 'jKIDq|}J&c': 'qW1*i(&R3$' + 'jKO,9y0M;#': 'p02Yuw`G)/' + 'jN^Vc%9OQ5': 'B`1@FnD9*.' + 'jN|NAUP*h}': 'h>OmJ~oZ~|' + 'jTNoKo}Bu+': 'rF%zt5M&?-' + 'jX=#G8wu#(': 'ki}T;HSiij' + 'jYz[ibrr9m': 'D.R!j]):&%' + 'jcu!gLw/&r': 'j(X|k-_Sv|' + 'jrM28*HbyG': OgRSGzP9iU + 'j|z@PMgdx,': 'gM+}.:OZH]' + 'k$1_yaF?9#': 'gM!|AcSE~W' + 'k/u1:B%DJH': 'yJ+?5Bb~=:' + 'k0{O[6l]bH': 'Pc}I71FH$K' + 'k1]FH+s8j@': 'A`?%3b$nUX' + 'k9E.p^jkXg' + 'k}A9]O:0xw': 'lsc1G{vU4.' + 'l$2O|9w`F~': 'x^M8w2)s#:' + 'l$zM~ihPFE': 'B,[GBme-:n' + 'lI)Ah/E': ':+A,]QJt-' + 'l*j*EE:!Y?': 'EKcj[W^+w]' + 'l+>Ki!j#v+': 'sj6y@z=dRP' + 'l3Ly8PBxt(': 'n$0d;K4M?l' + 'lAxH.CfJB@': 'r#k4%R`2$IfI' + 'm2F|cUkKhv': 'qvhov1fh!t' + 'm5j,b/y7T|g>': 'eDx9dZ&k(k' + 'mz[>>AxIr4': 'z9T_hmmT*f' + 'm{noqE29%I': 'w3V@j|Foq~' + 'm{oM,jr>C#': 'rVhH@X0hOr' + 'n&>e`2FO/S': 'l{J_M.Vp>X' + 'n+t>2`:;P/': 'wH[5PSfX;K' + 'n:7&>YT5A=': 'J.*@Nk4>0*' + 'nIfRner*)`9o' + 'o#&RMA_REo': 'w7J5,@!ZO`' + 'o3VS!KWb/@': 'Nky*kq6)GC' + 'o9{x/yz': 'jZpi[+b>`2' + 'oa87&PB&Bm': 'GH~LmtD8o#' + 'onMKID2d;D': 'F[Hq;*V/7K' + 'o~}KVA:VbI': 'njbrSU^agd' + 'p#R$-mG/L': 'xA+]rlW|<7' + 'p.yv>BQ470': 'FGu[yfPiJ7' + 'p:SPZ:e/*[': 'Op1]SnugtV' + 'pDDrFhd%u': 'w5=:(t5%R6' + 'pYRw[=6;JI': 'FoXsg8;#yi' + 'p]t8:Tka7q': 'f*vz&Ph[bR' + pkj3RPBUrF: 'o%$r]1QH{t' + 'pwwZu{7f~(': CpZYay1jbg + 'pz(G*g': 'J{(N4{^e:N' + 'q2#Yv[O0:|': 'tJ&;D}C]UV' + 'q7do-wLx%E': 'v?vAk$tZAa' + 'q8][)Ri}=q': 'N@bHVDe+Kb' + 'q8fF$4,c6_': '[&[K^KiK8' + 'q:XyGf#>Wg': 'O>(c0%czoO' + 'q=w[fB$d+$': 'Mj&h' + 'r6BV%|..*5': 'Qmb.+7huoY' + 'raI-$$|wT': 'f`?#zj#)DE' + 'r>g<|,FWi%': 'Ko1^yAR.yn' + 'r?E$4f-d0/': 'gux9;qM)nK' + 'rPekx@n9:L': 'kjo~PAZ.,y' + 'rSneU!3}bn': 'myDYy': 'Q@3rl@WPXz' + 's9-GL*@AXD': 'nN,;(#X}Wt' + 's<_WLSB3I/': 'DVc[0B1#@*' + 's>f40rn,O]': 'J]3b`qj!hC' + 'sB7rZQsR@9w~i^p': 'H_pUK6Xu]`' + 'skOM(?n$GG': 'hj]p(iMKl7' + spKoFRM4if: 'c@58oWbN3U' + 'ssbyA9}]QP': 'D((j`lUo!d' + 'stRa/S(0$F': 'KHEo&Oa7_F' + 'sxOMs+bE8@': 'J+O2qUIUz|' + 'sx|l+io`B@': 'p?*+RZF]bu>P': 'y=oLcr9$cQ' + 't2&ri|[Un:': 'c7[u_eSWiy' + t3aZXtpNyi: OX,mUDIStL + 't50y/[AEhF': 'I=mZ*3?!_l' + 'tMMtBUpb$&': '|*Z`50Bgs' + 'tTnCoO/*tk': 'Qg3C+rbaa*' + 't_h*fbN#:(': 'Leo~hDb-?a@m': 'A:WhEL~x!A' + upiX0g1quR: 'mSow!CwtLI' + 'uv1fGXHN@)': AUgq3IkilN + 'u|I}oElaH)': 'lNTL4T}K1)' + 'vE[T$`n9T)': 'nB97`B-u)' + 'vKx<0Qa!aP': 'fyeM9@jJW0' + 'vNiD6A.v#s': 'P.ls/JP5<}' + 'va/ky=f5pK': fGB0mK7OmU + 'vf)G?[_?Pf': 'u3u7Z|VQV`' + 'vgdj#X?8dB': 'b^b:}HG<(/' + 'vpOTK6tMr@': 'QIn7X6P~>i' + 'vppEA|$@~g': 'koAN*cMu(A' + 'vrn%j%6{nu': 'F%kRV:W:T$' + 'vwc3}.=Z#&': 'l,[F>UJ/PN' + 'wa>{k!2cXc': 'x=Gm50ksY@' + x2d4n4ADcP: 'p6lxT;d^Pq' + 'xb&P*pH(Q': 'P)2eJ7Sr]e' + 'xqrJ^d[.(O': 'g67ir]U+=9' + 'yX:tmrd5;]': 'GZ2yh|R:fR' + 'z:*d{z(~V2': 'x-s/YndH;0' + 'zSc]-^U=0=': lGG8WbUQzc + crowdanki:uuid: + 43c5ba66-9a65-11e8-90c9-a0481cc15658: 75bfcdb5-0ff3-4038-83cb-3e6ed974f439 + 43e2586a-9a65-11e8-a777-a0481cc15658: 3090ce89-5fd8-4107-86df-3e7a74ec288f +deck: + name: + intent: replace + value: 'Ultimate Geography [SV]' + expected_base: + value: Ultimate Geography + description: + intent: replace + value: | + FULLSTÄNDIG BESKRIVNING | UTGIVNINGSANMÄRKNINGAR | BIDRA + + Ultimate Geography v5.3 innehåller: + + - världens 205 självständiga stater (820 kort) + - 59 territorum, världsregioner och andra enheter (103 kort) + - 48 sjöar och hav (48 kort, enbart kartor) + - 7 kontinenter (7 kort, enbart kartor) + - sammanlagt 319 unika noter, 978 kort, 221 flaggor och 319 kartor. + + Kortleken finns tillgänglig på fjorton olika språk: engelska, tyska, spanska, franska, norska, tjeckiska, ryska, nederländska, svenska, portugisiska, kinesiska, polska, italienska och danska. För vardera språk finns dessutom en utökad version. + + För att underlätta inlärningen så har vissa kort en extra informationsrad, med exempelvis information om att flaggan på kortet liknar en annan flagga, politisk styrelse eller ett alternativt namn på ett land. + + Du kan skapa en filtrerad kortlek om du vill fokusera på specifika delar av kortleken. Till exempel kan du på så sätt välja att enbart studera suveräna stater, endast en specifik korttyp (såsom "karta → land") eller en specifik världsdel (till exempel Europa). + + Den här kortleken administreras på GitHub. Om du upptäcker ett fel, har förslag eller vill hjälpa till, tveka inte att öppna ett ärende. Vill du bli informerad om uppdateringar av kortleken? Bevaka källlkodskatalogen på GitHub eller prenumerera på feeden för utgåvor! + expected_base: + value: | + FULL DESCRIPTION | RELEASE NOTES | CONTRIBUTING + + Ultimate Geography v5.3 features: + + - the world's 205 sovereign states (820 cards) + - 59 territories, world regions, and other entities (103 cards) + - 48 oceans and seas (48 cards, maps only) + - 7 continents (7 cards, maps only) + - for a total of 319 unique notes, 978 cards, 221 flags and 319 maps. + + The deck is available in English, German, Spanish, French, Norwegian, Czech, Russian, Dutch, Swedish, Portuguese, Chinese (simplified and traditional), Polish, Italian and Danish. An extended version is also available in each language. To help with memorisation and provide context while learning, some notes include extra information such as similar flags, governance information, alternative country names, etc. + + You can use Anki's filtered deck feature to focus your study on a subset of the deck, such as sovereign states, a single note template (e.g. map to country), or a specific continent (e.g. Europe). + + This deck is maintained on GitHub. If you spot a mistake, have a suggestion or want to help, please don't hesitate to open an issue. Want to stay informed of new releases? Watch the GitHub repository or subscribe to the releases feed! diff --git a/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml b/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml new file mode 100644 index 0000000..a8682f7 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml @@ -0,0 +1,1032 @@ +id: overlay.translation.zh-tw +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': '' + Abkhazia: '阿布哈茲共和國 (Abkhazia)' + Abu Dhabi: '阿布達比 (Abu Dhabi)' + Abuja: '阿布加 (Abuja)' + Accra: '阿克拉 (Accra)' + Addis Ababa: '阿迪斯阿貝巴 (Addis Ababa)' + Adriatic Sea: '亞得里亞海 (Adriatic Sea)' + Aegean Sea: '愛琴海 (Aegean Sea)' + Afghanistan: '阿富汗 (Afghanistan)' + Africa: '非洲 (Africa)' + Akrotiri and Dhekelia: '亞克羅提利與德凱利亞 (Akrotiri and Dhekelia)' + Alaska: '阿拉斯加 (Alaska)' + Albania: '阿爾巴尼亞 (Albania)' + Algeria: '阿爾巴尼亞 (Algeria)' + Algiers: '阿爾及爾 (Algiers)' + Alofi: '阿洛非 (Alofi)' + Also known as Burma.: '' + Also known as Cabo Verde.: '' + Also known as Czechia.: '' + Also known as East Timor.: '' + Also known as Kiev.: '又稱為基輔。' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: '' + Also known as the Sea of Cortez.: '又稱科爾特斯海。' + 'Also spelled as Sana''a.': '' + American Samoa: '美屬薩摩亞 (American Samoa)' + Amman: '安曼 (Amman)' + Amsterdam: '阿姆斯特丹 (Amsterdam)' + Andorra: '安道爾 (Andorra)' + 'Andorra (narrower, coat of arms with motto)': '安道爾(更窄、徽章)' + Andorra la Vella: '安道爾城 (Andorra la Vella)' + Angola: '安哥拉 (Angola)' + Anguilla: '安奎拉 (Anguilla)' + Ankara: '安卡拉 (Ankara)' + Antananarivo: '安塔那那利弗 (Antananarivo)' + Antarctica: '南極洲 (Antarctica)' + Antigua and Barbuda: '安地卡及巴布達 (Antigua and Barbuda)' + Apia: '阿庇亞 (Apia)' + Arabian Sea: '阿拉伯海 (Arabian Sea)' + Aral Sea: '鹹海 (Aral Sea)' + Arctic Ocean: '北冰洋 (Arctic Ocean)' + Argentina: '阿根廷 (Argentina)' + Armenia: '亞美尼亞 (Armenia)' + Aruba: '荷屬阿魯巴 (Aruba)' + Ashgabat: '阿什哈巴特 (Ashgabat)' + Asia: '亞洲 (Asia)' + Asmara: '阿斯馬拉 (Asmara)' + Astana: '阿斯塔納 (Astana)' + 'Asunción': '亞松森 (Asunción)' + Athens: '雅典 (Athens)' + Atlantic Ocean: '大西洋 (Atlantic Ocean)' + Australia: '澳大利亞 (Australia)' + 'Australia (white stars, two more stars)': '澳洲(白色星星、多兩顆星星)' + Austria: '奧地利 (Austria)' + 'Austria (brighter red, wider white band)': '奧地利(更淺紅色、白線較寬)' + Autonomous community of Spain.: '西班牙的自治區。' + Autonomous province of South Korea.: '南韓的自治區。' + Autonomous region of Finland.: '芬蘭的自治區。' + Autonomous region of Italy.: '義大利的自治區。' + Autonomous region of Papua New Guinea.: '巴布亞紐幾內亞的自治區。' + Autonomous region of Portugal.: '葡萄牙的自治區。' + Avarua: '阿瓦魯阿 (Avarua)' + Azerbaijan: '亞塞拜然 (Azerbaijan)' + Azores: '亞速群島 (Azores)' + Baghdad: '巴格達 (Baghdad)' + Bahrain: '巴林 (Bahrain)' + 'Bahrain (narrower, fewer serrated edges, red)': '巴林(更窄、更少鋸齒狀邊緣、紅色)' + Baku: '巴庫 (Baku)' + Bali: '峇里島 (Bali)' + Balkan Peninsula: '巴爾幹半島 (Balkan Peninsula)' + Baltic Sea: '波羅的海 (Baltic Sea)' + Bamako: '巴馬科 (Bamako)' + Banda Sea: '班達海 (Banda Sea)' + Bandar Seri Begawan: '斯里巴加灣市 (Bandar Seri Begawan)' + Bangkok: '曼谷 (Bangkok)' + Bangladesh: '孟加拉 (Bangladesh)' + Bangui: '班基 (Bangui)' + Banjul: '班竹市 (Banjul)' + Barbados: '巴貝多 (Barbados)' + Barents Sea: '巴倫支海 (Barents Sea)' + Basseterre: '巴士底 (Basseterre)' + Bay of Bengal: '孟加拉灣 (Bay of Bengal)' + Bay of Biscay: '比斯開灣 (Bay of Biscay)' + Beijing: '北京 (Beijing)' + Beirut: '貝魯特 (Beirut)' + Belarus: '白俄羅斯 (Belarus)' + Belfast: '貝爾法斯特 (Belfast)' + Belgium: '比利時 (Belgium)' + Belgrade: '貝爾格勒 (Belgrade)' + Belize: '貝里斯 (Belize)' + Belmopan: '貝爾墨邦 (Belmopan)' + Benin: '貝南 (Benin)' + Bering Strait: '白令海峽 (Bering Strait)' + Berlin: '柏林 (Berlin)' + Bermuda: '百慕達 (Bermuda)' + Bern: '伯爾尼 (Bern)' + Bhutan: '不丹 (Bhutan)' + Bishkek: '比斯凱克 (Bishkek)' + Bissau: '比索 (Bissau)' + Black Sea: '黑海 (Black Sea)' + 'Bogotá': '波哥大 (Bogotá)' + Bolivia: '玻利維亞 (Bolivia)' + 'Bolivia (coat of arms instead of star)': '波利維亞(徽章、非星星)' + Bosnia and Herzegovina: '波士尼亞與赫塞哥維納 (Bosnia and Herzegovina)' + Botswana: '波札那 (Botswana)' + Bougainville: '布干維爾島 (Bougainville)' + 'Brasília': '巴西利亞 (Brasília)' + Bratislava: '布拉提斯拉瓦 (Bratislava)' + Brazil: '巴西 (Brazil)' + Brazzaville: '布拉薩 (Brazzaville)' + Bridgetown: '橋鎮 (Bridgetown)' + British Virgin Islands: '英屬維京群島 (British Virgin Islands)' + Brunei: '汶萊 (Brunei)' + Brussels: '布魯塞爾 (Brussels)' + Bucharest: '布加勒斯特 (Bucharest)' + Budapest: '布達佩斯 (Budapest)' + Buenos Aires: '布宜諾斯艾利斯 (Buenos Aires)' + Bulgaria: '保加利亞 (Bulgaria)' + Burkina Faso: '布吉納法索 (Burkina Faso)' + Burundi: '蒲隆地共和國 (Burundi)' + Cairo: '開羅 (Cairo)' + Cambodia: '柬埔寨 (Cambodia)' + Cameroon: '喀麥隆共和國 (Cameroon)' + 'Cameroon (green/red/yellow, yellow star)': '喀麥隆(綠紅黃、黃色星星)' + Canada: '加拿大 (Canada)' + Canary Islands: '加納利群島 (Canary Islands)' + Canberra: '坎培拉 (Canberra)' + Cape Verde: '維德角共和國 (Cape Verde)' + Caracas: '卡拉卡斯 (Caracas)' + Cardiff: '卡地夫 (Cardiff)' + Caribbean Sea: '加勒比海 (Caribbean Sea)' + Caspian Sea: '裏海 (Caspian Sea)' + Castries: '卡斯翠 (Castries)' + Cayman Islands: '開曼群島 (Cayman Islands)' + Celebes Sea: '西里伯斯海(蘇拉威西海) (Celebes Sea)' + Celtic Sea: '凱爾特海 (Celtic Sea)' + Central African Republic: '中非共和國 (Central African Republic)' + Cetinje is an honorary capital.: '策提涅是歷史首都。' + Chad: '查德 (Chad)' + 'Chad (slightly darker blue)': '查德(稍更深藍)' + Charlotte Amalie: '夏洛特阿馬利亞 (Charlotte Amalie)' + Chile: '智利 (Chile)' + China: '中國 (China)' + 'Chișinău': '基希訥烏 (Chișinău)' + City of San Marino: '聖馬利諾 (City of San Marino)' + Ciudad de la Paz: '拉巴斯城 (Ciudad de la Paz)' + Claimed and controlled: '宣稱擁有主權並有實際控制權' + Claimed but not controlled: '宣稱擁有主權但沒有實際控制權' + Colombia: '哥倫比亞 (Colombia)' + 'Colombia (no coat of arms)': '哥倫比亞(無徽章)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: '雖然可倫坡被稱為首都,但"斯里賈亞瓦德納普拉科特"是正式與立法的首都。' + Comoros: '葛摩聯盟 (Comoros)' + Conakry: '柯那克里 (Conakry)' + Constituent country in the Kingdom of Denmark.: '丹麥的構成國。' + Constituent country of the Kingdom of the Netherlands.: '荷蘭的構成國。' + Constituent country of the United Kingdom.: '英國的構成國。' + Cook Islands: '庫克群島 (Cook Islands)' + Copenhagen: '哥本哈根 (Copenhagen)' + Coral Sea: '珊瑚海 (Coral Sea)' + Corsica: '科西嘉 (Corsica)' + Costa Rica: '哥斯大黎加 (Costa Rica)' + Croatia: '克羅埃西亞 (Croatia)' + Crown dependency of the United Kingdom.: '英國皇家屬地。' + Cuba: '古巴 (Cuba)' + 'Cuba (red triangle, blue stripes)': '古巴(紅色三角、紅條紋)' + 'Curaçao': '庫拉索 (Curaçao)' + 'Curaçao (two stars in top-left corner)': '庫拉索(左上角有兩顆星星)' + Cyprus: '賽普勒斯 (Cyprus)' + Czech Republic: '捷克 (Czech Republic)' + Dakar: '達卡 (Dakar)' + Damascus: '大馬士革 (Damascus)' + Dead Sea: '死海 (Dead Sea)' + Democratic Republic of the Congo: '剛果民主共和國 (Democratic Republic of the Congo)' + Denmark: '丹麥 (Denmark)' + Denmark Strait: '丹麥海峽 (Denmark Strait)' + Dhaka: '達卡 (Dhaka)' + Dili: '帝利 (Dili)' + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': '有爭議,以色列宣稱擁有主權,拉馬拉是行政中心。' + 'Disputed; claimed by Palestine.': '有爭議,巴勒斯坦宣稱擁有主權。' + Djibouti: + notes.note.djibouti.fields.field.capital: '吉布地 (Djibouti)' + notes.note.djibouti.fields.field.country: '吉布地共和國 (Djibouti)' + Dodoma: '杜篤馬 (Dodoma)' + Doha: '杜哈 (Doha)' + Dominica: '多明尼加 (Dominica)' + Dominican Republic: '多明尼加共和國 (Dominican Republic)' + Dublin: '都柏林 (Dublin)' + Dushanbe: '杜尚貝 (Dushanbe)' + East China Sea: '東海 (East China Sea)' + East Siberian Sea: '東西伯利亞海 (East Siberian Sea)' + Ecuador: '厄瓜多 (Ecuador)' + 'Ecuador (with coat of arms)': '厄瓜多(有徽章)' + Edinburgh: '愛丁堡 (Edinburgh)' + Egypt: '埃及 (Egypt)' + 'Egypt (emblem instead of text), Yemen (no text)': '埃及(徽章、非文字)、葉門(無文字)' + 'Egypt (with emblem), Iraq (with text)': '埃及(徽章)、伊拉克(文字)' + El Salvador: '薩爾瓦多 (El Salvador)' + 'El Salvador (different coat of arms, slightly darker blue)': '薩爾瓦多(不同徽章、稍深藍)' + England: '英格蘭 (England)' + English Channel: '英吉利海峽 (English Channel)' + Equatorial Guinea: '赤道幾內亞 (Equatorial Guinea)' + Eritrea: '厄利垂亞 (Eritrea)' + Estonia: '愛沙尼亞 (Estonia)' + Eswatini: '史瓦帝尼 (Eswatini)' + Ethiopia: '衣索比亞 (Ethiopia)' + Europe: '歐洲 (Europe)' + European Union: '歐盟 (European Union)' + Falkland Islands: '福克蘭群島 (Falkland Islands)' + Faroe Islands: '法羅群島 (Faroe Islands)' + Federated States of Micronesia: '密克羅尼西亞 (Federated States of Micronesia)' + Fiji: '斐濟 (Fiji)' + Finland: '芬蘭 (Finland)' + Formerly Zaire.: '前身為薩伊。' + Formerly known as Macedonia.: '舊稱馬其頓。' + France: '法國 (France)' + Freetown: '自由城 (Freetown)' + French Guiana: '法屬圭亞那 (French Guiana)' + French Polynesia: '法屬玻里尼西亞 (French Polynesia)' + Funafuti: '富那富提 (Funafuti)' + Gabon: '加彭共和國 (Gabon)' + Gaborone: '嘉柏隆里 (Gaborone)' + Georgetown: '喬治城 (Georgetown)' + Georgia: '喬治亞 (Georgia)' + Germany: '格國 (Germany)' + Ghana: '迦納 (Ghana)' + 'Ghana (star instead of coat of arms)': '迦納(星星、非徽章)' + Gibraltar: '直布羅陀 (Gibraltar)' + Gitega: '吉特加 (Gitega)' + Greece: '希臘 (Greece)' + Greenland: '格陵蘭 (Greenland)' + Grenada: '格瑞那達 (Grenada)' + Guadeloupe: '瓜地洛普 (Guadeloupe)' + Guam: '關島 (Guam)' + Guatemala: '瓜地馬拉 (Guatemala)' + Guatemala City: '瓜地馬拉市 (Guatemala City)' + Guernsey: '根西 (Guernsey)' + Guinea: '幾內亞 (Guinea)' + 'Guinea (green and red flipped, slightly darker green)': '圭亞那(綠紅對調、稍微深綠)' + Guinea-Bissau: '幾內亞比索共和國 (Guinea-Bissau)' + Gulf of Alaska: '阿拉斯加灣 (Gulf of Alaska)' + Gulf of California: '加利福尼亞灣 (Gulf of California)' + Gulf of Carpentaria: '喀本塔利亞灣 (Gulf of Carpentaria)' + Gulf of Guinea: '幾內亞灣 (Gulf of Guinea)' + Gulf of Mexico: '墨西哥灣 (Gulf of Mexico)' + Gulf of Thailand: '泰國灣 (Gulf of Thailand)' + Guyana: '蓋亞那 (Guyana)' + 'Hagåtña': '亞加納 (Hagåtña)' + Haiti: '海地 (Haiti)' + Hanoi: '河內 (Hanoi)' + Harare: '哈拉雷 (Harare)' + Hargeisa: '哈爾格薩 (Hargeisa)' + Havana: '哈瓦那 (Havana)' + Hawaii: '夏威夷 (Hawaii)' + Helsinki: '赫爾辛基 (Helsinki)' + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: '北歐的歷史和文化地區,包括丹麥、挪威和瑞典國家,有時還包括芬蘭和冰島。' + Honduras: '宏都拉斯 (Honduras)' + Hong Kong: '香港 (Hong Kong)' + Honiara: '荷尼阿拉 (Honiara)' + Hudson Bay: '哈德遜灣 (Hudson Bay)' + Hungary: '匈牙利 (Hungary)' + Iceland: '冰島 (Iceland)' + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': '冰島(藍底、紅白十字)、挪威(紅底、藍白十字)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': '冰島(藍底、紅十字)、法羅群島(白底、紅藍十字)' + Independent state claimed by Georgia.: + notes.note.abkhazia.fields.field.country-info: '喬治亞宣稱擁有該地區主權,但目前是獨立狀態。' + notes.note.south-ossetia.fields.field.country-info: '從喬治亞獨立。' + Independent state claimed by Moldova.: '摩爾多瓦宣稱擁有該地區主權,但目前是獨立狀態。' + Independent state claimed by Somalia.: '索馬利亞宣稱擁有該地區主權,但目前是獨立狀態。' + India: '印度 (India)' + Indian Ocean: '印度洋 (Indian Ocean)' + Indonesia: '印度尼西亞(印尼) (Indonesia)' + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': '印尼(白紅對調、更多紅色)、摩納哥(白紅對調、更窄)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': '印尼(更寬、更淺紅)、波蘭(紅白對調、更寬)' + Iran: '伊朗 (Iran)' + Iraq: '伊拉克 (Iraq)' + 'Iraq (text instead of emblem), Yemen (no emblem)': '伊拉克(文字、無徽章)、葉門(無徽章)' + Ireland: '愛爾蘭 (Ireland)' + 'Ireland (orange and green flipped, wider)': '愛爾蘭(橘綠對調、更寬)' + Islamabad: '伊斯蘭馬巴德 (Islamabad)' + Island of Indonesia.: '印尼的島嶼。' + Isle of Man: '曼島 (Isle of Man)' + Israel: '以色列 (Israel)' + Italy: '義大利 (Italy)' + Ivory Coast: '象牙海岸 (Ivory Coast)' + 'Ivory Coast (green and orange flipped, narrower)': '象牙海岸(綠橘對調、更窄)' + Jakarta: '雅加達 (Jakarta)' + Jamaica: '牙買加 (Jamaica)' + Japan: '日本 (Japan)' + Java: '爪哇 (Java)' + Jeju: '濟州市 (Jeju)' + Jersey: '澤西島 (Jersey)' + Jerusalem: '耶路撒冷 (Jerusalem)' + Jordan: '約旦 (Jordan)' + Juba: '朱巴 (Juba)' + Kabul: '喀布爾 (Kabul)' + Kaliningrad Oblast: '加里寧格勒州 (Kaliningrad Oblast)' + Kampala: '坎帕拉 (Kampala)' + Kathmandu: '加德滿都 (Kathmandu)' + Kazakhstan: '哈薩克 (Kazakhstan)' + Kenya: '肯亞 (Kenya)' + Khartoum: '喀土穆 (Khartoum)' + Kigali: '吉佳利 (Kigali)' + Kingston: '京斯敦 (Kingston)' + Kingstown: '金石城 (Kingstown)' + Kinshasa: '金夏沙 (Kinshasa)' + Kiribati: '吉里巴斯 (Kiribati)' + Known as Nur-Sultan between 2019 and 2022: '2019年至2022年間稱為Nur-Sultan' + Known as Swaziland until 2018.: '' + Kosovo: '科索沃 (Kosovo)' + Kuala Lumpur: '吉隆坡 (Kuala Lumpur)' + Kuwait: '科威特 (Kuwait)' + Kuwait City: '科威特市 (Kuwait City)' + Kyiv: '基輔 (Kyiv)' + Kyrgyzstan: '吉爾吉斯 (Kyrgyzstan)' + Laayoune: '阿尤恩 (Laayoune)' + Labrador Sea: '拉布拉多海 (Labrador Sea)' + Laos: '寮國 (Laos)' + Latvia: '拉脫維亞 (Latvia)' + 'Latvia (darker red, narrower white band)': '拉脫維亞(更深紅、窄白線)' + Lebanon: '黎巴嫩 (Lebanon)' + Lesotho: '賴索托 (Lesotho)' + Liberia: '賴比瑞亞共和國 (Liberia)' + Libreville: '自由市 (Libreville)' + Libya: '利比亞 (Libya)' + Liechtenstein: '列支敦斯登 (Liechtenstein)' + Lilongwe: '里朗威 (Lilongwe)' + Lima: '利馬 (Lima)' + Lisbon: '里斯本 (Lisbon)' + Lithuania: '立陶宛 (Lithuania)' + Ljubljana: '盧布爾雅那 (Ljubljana)' + 'Lomé': '洛梅 (Lomé)' + London: '倫敦 (London)' + Luanda: '盧安達 (Luanda)' + Lusaka: '路沙卡 (Lusaka)' + Luxembourg: '盧森堡 (Luxembourg)' + 'Luxembourg (lighter blue)': '盧森堡(更淺藍)' + Luxembourg City: '盧森堡市 (Luxembourg City)' + Macau: '澳門 (Macau)' + Madagascar: '馬達加斯加 (Madagascar)' + Madeira: '馬德拉群島 (Madeira)' + Madrid: '馬德里 (Madrid)' + Majuro: '馬久羅 (Majuro)' + Malawi: '馬拉威共和國 (Malawi)' + Malaysia: '馬來西亞 (Malaysia)' + Maldives: '馬爾地夫 (Maldives)' + Mali: '馬利 (Mali)' + 'Mali (red and green flipped, slightly brighter green)': '馬利(紅綠對調、更淺綠)' + Malta: '馬爾他 (Malta)' + 'Malé': '馬列市 (Malé)' + Managua: '馬納瓜 (Managua)' + Manama: '麥納瑪 (Manama)' + Manila: '馬尼拉 (Manila)' + Maputo: '馬布多 (Maputo)' + Mariehamn: '瑪麗港 (Mariehamn)' + Marshall Islands: '馬爾紹群島 (Marshall Islands)' + Martinique: '馬丁尼克 (Martinique)' + Maseru: '馬賽魯 (Maseru)' + Mauritania: '茅利塔尼亞伊斯蘭共和國 (Mauritania)' + Mauritius: '模里西斯共和國 (Mauritius)' + Mayotte: '馬約特島 (Mayotte)' + Mbabane: '姆巴巴內市 (Mbabane)' + Mediterranean Sea: '地中海 (Mediterranean Sea)' + Melanesia: '美拉尼西亞 (Melanesia)' + Mexico: '墨西哥 (Mexico)' + Mexico City: '墨西哥城 (Mexico City)' + Micronesia: '密克羅尼西亞 (Micronesia)' + Minsk: '明斯克 (Minsk)' + Mogadishu: '摩加迪休 (Mogadishu)' + Moldova: '摩爾多瓦 (Moldova)' + 'Moldova (wider, coat of arms with eagle)': '摩爾多瓦(更寬、老鷹)' + Monaco: '摩納哥 (Monaco)' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': '摩納哥(更窄、更深紅)、波蘭(紅白對調、更深紅)' + Mongolia: '蒙古 (Mongolia)' + Monrovia: '蒙羅維亞 (Monrovia)' + Montenegro: '蒙特內哥羅 (Montenegro)' + Montevideo: '蒙特維多市 (Montevideo)' + Morocco: '摩洛哥 (Morocco)' + Moroni: '莫洛尼 (Moroni)' + Moscow: '莫斯科 (Moscow)' + Mozambique: '莫三比克共和國 (Mozambique)' + Muscat: '馬斯開特 (Muscat)' + Myanmar: '緬甸 (Myanmar)' + 'N''Djamena': '恩加美納 (N''Djamena)' + Nairobi: '奈洛比 (Nairobi)' + Namibia: '納米比亞共和國 (Namibia)' + Nassau: '拿騷 (Nassau)' + Nauru: '諾魯共和國 (Nauru)' + 'Nauru (single star below yellow band)': '諾魯(僅一顆星星於黃線下)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': '諾魯沒有法定首都,但亞倫是實際首都。' + Naypyidaw: '奈比都 (Naypyidaw)' + Nepal: '尼泊爾 (Nepal)' + Netherlands: '荷蘭 (Netherlands)' + 'Netherlands (darker blue)': '荷蘭(更深藍)' + New Caledonia: '新喀里多尼亞 (New Caledonia)' + New Delhi: '新德里 (New Delhi)' + New Zealand: '紐西蘭 (New Zealand)' + 'New Zealand (red stars, two fewer stars)': '紐西蘭(紅色星星、少兩顆星星)' + Ngerulmud: '恩吉魯穆德 (Ngerulmud)' + Niamey: '尼阿美 (Niamey)' + Nicaragua: '尼加拉瓜 (Nicaragua)' + 'Nicaragua (different coat of arms, slightly lighter blue)': '尼加拉瓜(不同徽章、稍淺藍)' + Nicosia: '尼克西亞 (Nicosia)' + Niger: '尼日 (Niger)' + Nigeria: '奈及利亞 (Nigeria)' + Niue: '紐埃島 (Niue)' + North America: '北美洲 (North America)' + North Korea: '北韓 (North Korea)' + North Macedonia: '北馬其頓 (North Macedonia)' + North Nicosia: '尼古西亞市 (North Nicosia)' + North Sea: '北海 (North Sea)' + Northern Cyprus: '北賽普勒斯 (Northern Cyprus)' + Northern Ireland: '北愛爾蘭 (Northern Ireland)' + Northern Mariana Islands: '北馬里亞納群島 (Northern Mariana Islands)' + Norway: '挪威 (Norway)' + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': '挪威(紅底、藍十字)' + Norwegian Sea: '挪威海 (Norwegian Sea)' + Not a sovereign country: '不是主權國家' + Nouakchott: '諾克少 (Nouakchott)' + 'Nouméa': '努美阿 (Nouméa)' + 'Nukuʻalofa': '努瓜婁發 (Nukuʻalofa)' + Nuuk: '努克 (Nuuk)' + 'Oblast (administrative region) of the Russian Federation.': '俄羅斯的州。' + Oceania: '大洋洲 (Oceania)' + Official capital was moved from Bujumbura to Gitega in 2019.: '2019年正式從布瓊布拉遷都到基特加。' + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: '2026年官方首都从马拉博迁至拉巴斯城。' + 'Officially Côte d''Ivoire.': '正式名: cote d''ivoire。' + Officially Luxembourg.: '' + Oman: '阿曼王國 (Oman)' + Oranjestad: '奧拉涅斯塔德 (Oranjestad)' + Oslo: '奧斯陸 (Oslo)' + Ottawa: '渥太華 (Ottawa)' + Ouagadougou: '瓦加杜古 (Ouagadougou)' + Overseas department of France.: '法國的海外省。' + Overseas territory of France.: '法國的海外領土。' + Overseas territory of the United Kingdom.: '英國的海外領土。' + Pacific Ocean: '太平洋 (Pacific Ocean)' + Pakistan: '巴基斯坦 (Pakistan)' + Palau: '帛琉 (Palau)' + Palestine: '巴勒斯坦 (Palestine)' + 'Palestine (black/white/green, red arrow)': '巴勒斯坦(黑白綠、紅色箭頭)' + 'Palestine (no symbol)': '巴勒斯坦(無標誌)' + Palikir: '帕里克爾 (Palikir)' + Panama: '巴拿馬 (Panama)' + Panama City: '巴拿馬城 (Panama City)' + Papeete: '帕皮提 (Papeete)' + Papua New Guinea: '巴布亞紐幾內亞 (Papua New Guinea)' + Paraguay: '巴拉圭 (Paraguay)' + Paramaribo: '巴拉馬利波 (Paramaribo)' + Paris: '巴黎 (Paris)' + Partially recognised state claimed by China.: '中國宣稱擁有該地區主權,但僅有部分國家承認。' + Partially recognised state claimed by Morocco. Also known as Western Sahara.: '摩洛哥宣稱擁有該地區主權,但僅有部分國家承認。又稱為西撒哈拉。' + Partially recognised state claimed by Serbia.: '塞爾維亞宣稱擁有該地區主權,但僅有部分國家承認。' + Persian Gulf: '波斯灣 (Persian Gulf)' + Peru: '祕魯 (Peru)' + Philippine Sea: '菲律賓海 (Philippine Sea)' + Philippines: '菲律賓 (Philippines)' + Phnom Penh: '金邊 (Phnom Penh)' + Podgorica: '波德戈里察 (Podgorica)' + Poland: '波蘭 (Poland)' + Polynesia: '玻里尼西亞 (Polynesia)' + Port Louis: '路易士港 (Port Louis)' + Port Moresby: '莫士比港 (Port Moresby)' + Port Vila: '維拉港 (Port Vila)' + Port of Spain: '西班牙港 (Port of Spain)' + Port-au-Prince: '太子港 (Port-au-Prince)' + Porto-Novo: '新港 (Porto-Novo)' + Portugal: '葡萄牙 (Portugal)' + Prague: '布拉格 (Prague)' + Praia: '培亞 (Praia)' + Pretoria, Cape Town, Bloemfontein: '普利托利亞、開普敦、布隆泉 (Pretoria, Cape Town, Bloemfontein)' + Pristina: '普里斯提納 (Pristina)' + Puerto Rico: '波多黎各 (Puerto Rico)' + 'Puerto Rico (blue triangle, red stripes)': '波多黎各(藍三角、紅條紋)' + Pyongyang: '平壤 (Pyongyang)' + Qatar: '卡達 (Qatar)' + 'Qatar (wider, more serrated edges, maroon)': '卡達(更寬、更多鋸齒狀邊緣、栗紅色)' + Quito: '基多市 (Quito)' + Rabat: '拉巴特 (Rabat)' + Red Sea: '紅海 (Red Sea)' + Region of France.: '法國的地區。' + Republic of the Congo: '剛果共和國 (Republic of the Congo)' + 'Reykjavík': '雷克雅維克 (Reykjavík)' + Riga: '里加 (Riga)' + Riyadh: '利雅德 (Riyadh)' + Romania: '羅馬尼亞 (Romania)' + 'Romania (slightly lighter blue)': '羅馬尼亞(稍微淺藍)' + Rome: '羅馬 (Rome)' + Roseau: '羅索 (Roseau)' + Russia: '俄羅斯 (Russia)' + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': '俄羅斯(沒徽章)、斯洛維尼亞(更寬、更小的徽章)' + Rwanda: '盧安達 (Rwanda)' + 'Réunion': '留尼旺島 (Réunion)' + Sahrawi Arab Democratic Republic: '撒拉威阿拉伯民主共和國(西薩哈拉) (Sahrawi Arab Democratic Republic)' + Saint Kitts and Nevis: '聖克里斯多福及尼維斯聯邦 (Saint Kitts and Nevis)' + Saint Lucia: '聖露西亞 (Saint Lucia)' + Saint Martin: '聖馬丁 (Saint Martin)' + Saint Vincent and the Grenadines: '聖文森及格瑞那丁 (Saint Vincent and the Grenadines)' + Samoa: '薩摩亞 (Samoa)' + 'San José': '聖荷西 (San José)' + San Juan: '聖胡安 (San Juan)' + San Marino: '聖馬利諾 (San Marino)' + San Salvador: '聖薩爾瓦多 (San Salvador)' + Sanaa: '沙那 (Sanaa)' + Santiago: '聖地牙哥 (Santiago)' + Santo Domingo: '聖多明哥 (Santo Domingo)' + Sarajevo: '塞拉耶佛 (Sarajevo)' + Sardinia: '薩丁尼亞 (Sardinia)' + Saudi Arabia: '沙烏地阿拉伯 (Saudi Arabia)' + Scandinavia: '斯堪地那維亞 (Scandinavia)' + Scotland: '蘇格蘭 (Scotland)' + Sea of Galilee: '加利利海 (Sea of Galilee)' + Sea of Japan: '日本海 (Sea of Japan)' + Sea of Okhotsk: '鄂霍次克海 (Sea of Okhotsk)' + Semi-autonomous region of Tanzania.: '坦尚尼亞的半自治區。' + Senegal: '塞內加爾 (Senegal)' + 'Senegal (green/yellow/red, green star)': '塞內加爾(綠黃紅、綠色星星)' + Seoul: '首爾 (Seoul)' + Serbia: '塞爾維亞 (Serbia)' + Seychelles: '塞席爾共和國 (Seychelles)' + Sicily: '西西里島 (Sicily)' + Sierra Leone: '獅子山 (Sierra Leone)' + Singapore: '新加坡 (Singapore)' + Sint Maarten: '荷屬聖馬丁 (Sint Maarten)' + Skopje: '斯科普里 (Skopje)' + Slovakia: '斯洛伐克 (Slovakia)' + 'Slovakia (narrower, bigger coat of arms)': '斯洛伐克(更窄、更大的徽章)' + 'Slovakia (with coat of arms)': '斯洛伐克(有徽章)' + Slovenia: '斯洛維尼亞 (Slovenia)' + Sofia: '索菲亞 (Sofia)' + Solomon Islands: '索羅門群島 (Solomon Islands)' + Somalia: '索馬利亞 (Somalia)' + Somaliland: '索馬利蘭共和國 (Somaliland)' + South Africa: '南非 (South Africa)' + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': '南非沒有法定首都,並有三大城市:普勒托利亞(行政)、開普敦(立法)、布隆方丹(司法)。' + South America: '南美洲 (South America)' + South China Sea: '南海 (South China Sea)' + South Korea: '韓國(南韓) (South Korea)' + South Ossetia: '南奧塞梯 (South Ossetia)' + South Sudan: '南蘇丹 (South Sudan)' + South Tarawa: '南塔拉瓦 (South Tarawa)' + Southern Ocean: '南冰洋 (Southern Ocean)' + Sovereign country: '主權國家' + Spain: '西班牙 (Spain)' + Special Administrative Region of China.: '中國的特別行政區。' + Sri Jayawardenepura Kotte: '可倫坡、 斯里賈亞瓦德納普拉科特 (Sri Jayawardenepura Kotte)' + Sri Lanka: '斯里蘭卡 (Sri Lanka)' + 'St. George''s': '聖喬治市 (St. George''s)' + 'St. John''s': '聖約翰 (St. John''s)' + State of the United States.: '美國的州。' + State recognised only by Turkey and claimed by Cyprus.: '只被土耳其承認,賽普勒斯宣稱擁有該地區主權。' + Stockholm: '斯德哥爾摩 (Stockholm)' + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: '大洋洲次區域,由太平洋中部和南部的數千個小島嶼組成。' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: '大洋洲次區域,由西太平洋數千個小島嶼組成。' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: '大洋洲次區域,包括萬那杜、所羅門群島、斐濟和巴布亞紐幾內亞。' + Sucre: '蘇克瑞 (Sucre)' + Sudan: '蘇丹 (Sudan)' + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': '蘇丹(紅白黑、綠色箭頭)、西撒哈拉(星星與新月)' + Sukhumi: '蘇呼米 (Sukhumi)' + Sumatra: '蘇門答臘 (Sumatra)' + Suriname: '蘇利南 (Suriname)' + Suva: '蘇瓦 (Suva)' + Svalbard: '斯瓦巴 (Svalbard)' + Sweden: '瑞典 (Sweden)' + Switzerland: '瑞士 (Switzerland)' + 'Switzerland has no official capital; Bern is the de facto capital.': '瑞士沒有法定首都,但伯爾尼是實際首都。' + Syria: '敘利亞 (Syria)' + 'São Tomé': '聖多美 (São Tomé)' + 'São Tomé and Príncipe': '聖多美普林西比民主共和國 (São Tomé and Príncipe)' + Taipei: '台北 (Taipei)' + Taiwan: '台灣(中華民國) (Taiwan)' + Tajikistan: '塔吉克共和國 (Tajikistan)' + Tallinn: '塔林 (Tallinn)' + Tanzania: '坦尚尼亞 (Tanzania)' + Tashkent: '塔什干 (Tashkent)' + Tasman Sea: '塔斯曼海 (Tasman Sea)' + Tbilisi: '第比利斯 (Tbilisi)' + Tegucigalpa: '德古西加巴 (Tegucigalpa)' + Tehran: '德黑蘭 (Tehran)' + Thailand: '泰國 (Thailand)' + The Bahamas: '巴哈馬 (The Bahamas)' + The Gambia: '甘比亞共和國 (The Gambia)' + Thimphu: '辛布市 (Thimphu)' + Timor Sea: '帝汶海 (Timor Sea)' + Timor-Leste: '東帝汶 (Timor-Leste)' + Tirana: '地拉那 (Tirana)' + Tiraspol: '基希涅夫 (Tiraspol)' + Togo: '多哥 (Togo)' + Tokyo: '東京 (Tokyo)' + Tonga: '東加 (Tonga)' + Transnistria: '德涅斯特河沿岸共和國 (Transnistria)' + Trinidad and Tobago: '千里達及托巴哥 (Trinidad and Tobago)' + Tripoli: '的黎波里 (Tripoli)' + Tskhinvali: '茲辛瓦利 (Tskhinvali)' + Tunis: '突尼斯 (Tunis)' + Tunisia: '突尼西亞 (Tunisia)' + Turkey: '土耳其 (Turkey)' + Turkmenistan: '土庫曼 (Turkmenistan)' + Turks and Caicos Islands: '土克凱可群島 (Turks and Caicos Islands)' + Tuvalu: '吐瓦魯 (Tuvalu)' + 'Tórshavn': '托爾斯港 (Tórshavn)' + Uganda: '烏干達 (Uganda)' + Ukraine: '烏克蘭 (Ukraine)' + Ulaanbaatar: '烏蘭巴托 (Ulaanbaatar)' + Unincorporated internal area of Norway.: '挪威非建制地區。' + Unincorporated territory of the United States.: '美國的屬地。' + United Arab Emirates: '阿拉伯聯合大公國 (United Arab Emirates)' + United Kingdom: '英國 (United Kingdom)' + United States Virgin Islands: '美屬維京群島 (United States Virgin Islands)' + United States of America: '美國(美利堅合眾國) (United States of America)' + Uruguay: '烏拉圭 (Uruguay)' + Uzbekistan: '烏茲別克 (Uzbekistan)' + Vaduz: '瓦杜茲 (Vaduz)' + Valletta: '瓦萊塔 (Valletta)' + Vanuatu: '萬那杜 (Vanuatu)' + Vatican City: '梵蒂岡 (Vatican City)' + Venezuela: '委內瑞拉 (Venezuela)' + Victoria: '維多利亞 (Victoria)' + Vienna: '維也納 (Vienna)' + Vientiane: '永珍 (Vientiane)' + Vietnam: '越南 (Vietnam)' + Vilnius: '維爾紐斯 (Vilnius)' + Wales: '威爾斯 (Wales)' + Wallis and Futuna: '瓦利斯群島和富圖那群島 (Wallis and Futuna)' + Warsaw: '華沙 (Warsaw)' + Washington, D.C.: '華盛頓DC (Washington, D.C.)' + Wellington: '威靈頓 (Wellington)' + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: '阿姆斯特丹是正式首都,行政立法首都為海牙。' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: '雖然多多馬是法定首都,但三蘭港是實際首都。' + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': '雖然阿尤恩是法定首都,但是提法里提實際首都。' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: '雖然姆巴巴納是法定、行政首都,洛班巴是傳統、精神、立法首都。' + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: '雖然波多諾伏是法定首都,但柯多努是實際首都。' + While Sucre is the constitutional capital, La Paz is the seat of government.: '雖然蘇克雷是法定首都,但是拉巴斯政府所在。' + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: '雖然亞穆蘇克羅是法定首都,但阿必尚是實際首都。' + White Sea: '白海 (White Sea)' + Willemstad: '威廉城 (Willemstad)' + Windhoek: '溫荷克 (Windhoek)' + World region covering the Australian continent and most of the islands in the Pacific Ocean.: '覆蓋澳洲大陸和太平洋大部分島嶼的區域。' + Yamoussoukro: '雅穆索戈 (Yamoussoukro)' + 'Yaoundé': '雅溫德 (Yaoundé)' + Yaren: '雅連 (Yaren)' + Yellow Sea: '黃海 (Yellow Sea)' + Yemen: '葉門 (Yemen)' + Yerevan: '耶烈萬 (Yerevan)' + Zagreb: '薩格勒布 (Zagreb)' + Zambia: '桑比亞 (Zambia)' + Zanzibar: '尚吉巴 (Zanzibar)' + Zimbabwe: '辛巴威 (Zimbabwe)' + 'Åland Islands': '奧蘭群島 (Åland Islands)' + additions: + notes.note.kiribati.fields.field.capital-info: '拉巴斯正在建造來取代馬拉波成為首都。' + variables: + label.capital: + Capital: '首都' + label.capital-hint.answer: + 'Hint: {{Capital hint}}': '提示:{{Capital hint}}' + label.flag: + Flag: '旗幟' + label.location: + Location: '位置' + note-type.name: + Ultimate Geography: 'Ultimate Geography [ZH-TW]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': '旗幟類似於:{{Flag similarity}}。' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': 'hP;i-k*Lq&' + '5JJ2]i)p4': 's%K%uD#*sw' + '5S(f7%O-$': 'we_2BuB.Z+' + '?n!)c/h=Q': 'wONP|LX.Q_' + 'AES],s:8=': 'rg~Oxq&v,q' + 'B:xq' + 'G/^_##fcPN': 'G9wDHnt]lR' + 'Gn;q2Y)Xe4': 'yb_9K`wphY' + 'H*c[ML+`s=': 'w;##5l={aM' + 'He!e=h?_2V': 'gkNaPEci^F' + 'J%,fuysl*&': 'MLn%?@{3-5' + 'JOT:U4ayh@': 'IxuFPY]!|V' + 'K*b}ip' + 'Lol+XIvY21': 'r=GA`|/%}m' + 'M$bc0[jYf0': 'nzVFM(2hen' + 'O~pXkAbd3': 'qG*a}3UG)u' + 'QSq%~rFaP-': 'sUt(spIN=3' + 'Qlib4)q2Hk': 'gRvIS4^Lge' + 'SlOEXfq#|': 'GfT}?H(Itk' + 'W5?6yQZD2': 'yLf3$zA,NG' + '_]DH+g9!$': 'A[&}6tcP(D' + 'b@M[XTc?}5': 'QA2Z`&%>+{' + 'bD?R}i8_[c': 'w~+;g=4]lx' + 'bW)ch:vs1P': 'Gp[PE61fhV' + 'b[(2`4t&?J': 'QZnt.4(%mo' + 'b^Try&me1A': 'ip)J{' + 'c(,2{yXI#E': 'KN=}b8)Yv%' + 'c,5(mUGzYT': 'C-=fx`$bT}' + 'cAlyc~G.y%': 'vm0Z5,/o[~' + 'cEO^y)Md[G': 'wd}GQaBf8^' + 'cL?8!xP#Wj': 'MPN<8x{bf#' + 'cPO|Cru8n<': 'd0,7(>HBr,' + 'cV4idO^r8W': 'C$4G)9=It:' + 'cYKvF#ptF7': 'OcX5@&/D_t' + 'c^,~EC6Pb2': 'O%hMRDX^>9' + 'ck!6;xK$/^': 'EkZVD=RUj#' + 'cv+Nq9:aZU!P_' + 'dbP/Cj}sB+': 'j,}M=b.Icy' + 'de=9c:,@j9': 'Q*%#@o})>]' + 'dlU7VG|3Gd': 'GAjT.#!J1@' + 'd}R:qHj1,7': 'Jr]Rx$vdq$' + 'e!KSebP}pt': '?:,j5Yjyl' + 'e+/O]%*qfk': 'CpdOVg*sVU' + 'e.t$Vi5,>f': 'wXGUH~)Ql(' + 'e42BpV,P}C': 'g;e5dQC_N=' + 'e5P*D#I{m{': 'lR/BZ##`|O' + 'eD' + 'e?h=Xhs+K&': 'AW_@tSbsx2' + 'eALZe^HCHq': 'vjf^W|(*8<' + 'eHJ?)TAdTo': 'PNd$F4p&3c' + 'eN#^kPGv*L': iH_T4wLlp, + 'eTHH?U': 'D}wop3Sg;c' + 'euW0jm=wfR': 'A-#|rzmH_H' + 'e{e8vi@^PY': 'O@9XZJX!0PN': c.ecIN49mi + 'f21=-A~W;~': 'FiNf!6B`-A' + 'f>c7x2E]Q6': 'm5V-SM=<@2' + 'fRSM{K+;[w': 'BFtu|[LXC[' + 'fT!w8R#>dg': 'MNe>6KJz>Q' + 'fV`&p81%d(': F9Gg3P7TV7 + fkq7lhLkdH: 'r`ewLw!Gt^' + 'fxAN),FAc$': 'Wu<%L~@!R' + 'g-]bb&[.E!': 'FkNL`}5EmT' + 'g9v~y/;=z(': 'z]rT_bEg+8' + 'g<[f89~U)A': 'l+c!U^Cm[3' + 'gBCR?!O*;E': 'F=!;a!Xf#Z' + 'gB|K:+r?1V': 'D88*#1:8B]' + 'gDsLPj(9S#': 'eQS2[,]W[c' + 'gFOojQO(MH': jxChKhrR_f + 'gG6SWfM>H0': 'NLqA4ocW^(' + 'gRVZ]qJ#>O': 'b=f8dwxf6,' + 'gYIoR`|AxW': 'F%ZvaD~rn$' + 'gePC:Xt_nW': 'dH5->JA4?,' + 'gp[h@]y13l': 'gj79~OV>hd' + 'gsS{jR,]|Q': 'EGG=`g;_2a' + 'gx:;za!?C9': 'xy{u)[Y5T.' + 'gz9${p}er*': 'i_WupG5`NH' + 'g~%pF`(x{u': 'x)P#4EEu03' + 'h&`>n98~V1': 'sUU#6=tmRv' + 'h9Jr(H[=1~': 'Q9jU$.Ei4q' + 'h?a*UKfT_M': 's#8JOS3OzS' + 'hE5j*e,Q?[': 'bUSqk+D|{;' + 'hNE9}>;Q&-': 'v0nE;h~kZ~' + 'hXNTVF<:SH': 'xpoQrEE]': 'iwf|+Y-DMZ' + 'h}lnSN_f$_': 'il_3&ea+_9' + 'h~5xz+=ke~': 'tC&_:#$T2i' + 'i)]+09JUR&': 't0PNjEDy~7' + 'i2]v|D3@:j': '^+oKH:A=t' + 'iB-8Br~onq': 'NPUF#o!QS-' + 'iIS#Nw]tr]': 'extWrMavp*' + 'iR;Um(L/~c=': 'xcDnaIk^fK' + 'i^J8&$(1]|': 'EF`@1,WVBK' + 'ia8as9sCY|': 'CQar?B#q*u' + 'igiK,ff[eO': '4iE[{aD4K' + 'ijP>aJBT!9': 'Po[p>b@7Jo' + 'ilGtw#=asM': 'l5`IXuhK?7' + 'j%*8gN)q>d': 'PjuCASX!1!' + 'j)i)pB*HJ,': ePKaTy,DGR + 'j2IQ=f=w3@': 'rv)qyS:)2O' + j7eEZkzsCZ: 'EH/@$WQER7' + 'j9:K,~DQ2v': 'n4>@0j_wL[' + 'jg|}': 'yHyzT~7ggt' + 'jCd]`-=k,:': 'o8gfBsL4(;' + 'jDA-V?/hVj': 'KRhqtz)r:' + jI9P-f6r3M: 'l>/)Hug!mW' + 'jKIDq|}J&c': 'p#pLS.CKZC' + 'jKO,9y0M;#': 'Bh#Ve`j#UW' + 'jN^Vc%9OQ5': 'q}(1`DT.am' + 'jN|NAUP*h}': 'uG2|*|Yh}_' + 'jTNoKo}Bu+': 'r>T|)f-nqo' + 'jX=#G8wu#(': 'fdc=:,[<%A' + 'jYz[ibrr9m': wTDfQYhjoa + 'jcu!gLw/&r': 'xIHB(EY-2e' + 'jrM28*HbyG': 's(o*X{,=M~' + 'j|z@PMgdx,': 'q@%w%D:POo' + 'k$1_yaF?9#': 'D9qv;Owm8Y' + 'k/u1:B%DJH': 'EvKZ+P|nmE' + 'k0{O[6l]bH': 'Mf#0M*Y}7m' + 'k1]FH+s8j@': zYu.6VjeUn + 'k9E.p^H3&w' + 'kE!iKHLN_g': 'E^+}$Fd}]X' + 'kQ:NIhJy~E': 'Gu^T-,V0xb' + 'kS)hDm%w}R': 'EazycZ_Z^_' + 'kX]hN=_.c-': oRMP/z9PW4 + 'kZ52zV#[Iv': 'fO-dah>=g>' + 'k[)#L4.[v3': 'Q1_h^!DKi!j#v+': 'sAcpxOzKv[' + 'l3Ly8PBxt(': 'f8O7+6f;RR' + 'lAxH.CfJB@': 'f>P{9v2]Bz' + 'lH^2D]w;[H': 'bqj>}eJA-L' + 'lU@C3IDn' + 'lhWI;$JC9(': 'I|[UZ!jURX' + libliptkB4: 'L^O|)G!]GZ' + 'lmRujD!(A7': 'iVnn1=xi{4' + 'ln^3p)j3b.': rNkWo7rM8m + 'lp@e##m.!+': 'b*bdAyf+0*' + 'lpbD0St&OU': 'e~PdHQDvCH' + 'lq`T^ZBF2Y': 'v[n(vSU^>b' + 'l}[BG9.nQS': 'O$abbk6n:u' + 'm)WUtJS`H&': 'Aj|9mQ]LS`' + 'm*#%sE.`.;': 't4{:V^Zw/O' + 'm*PYE=#wr^': 'qu:q$z3/}3' + 'm2F|cUkKhv': 'w-@AZOCawT' + 'm5j,b/y' + mJH4kwPF.-: 'x&b*o/}?f+' + 'm[96fa+HDS': 'sEq1@q]mMW' + mbGXc0y3Pe: 'Jl+~MwbBm-' + 'mbz=7n[0v9': 'c&t,@V?gkS' + 'mcc&0Kq^xE': ';jH7jP541' + 'mi+kf0LTW.': 'Q(VLcfD->n' + 'mk1VltJl<]': hoD/2dfvqx + 'mk20`,N=`=': 'uG9NEoHA_;' + 'mkgcU7[bA%': 'c^8$Lj5E?5' + 'mq)8`hi{hb': 'z:Rh_xOA1G' + 'mtd;>7T|g>': 'Q>AxIr4': 'gasQ]L`h5j' + 'm{noqE29%I': 'F.<0drjn&]' + 'm{oM,jr>C#': 'eA`%lT9XaC' + 'n&>e`2FO/S': 'cH>zn$fA;c' + 'n+t>2`:;P/': 'FM9N-?A5Ml' + 'n:7&>YT5A=': 'NSInc{q%IT' + 'nIfRn/yz': 'pr(?+A]W}t' + 'oa87&PB&Bm': 'Q:^I|cS~DH' + 'onMKID2d;D': 'If){M~;KV>' + 'o~}KVA:VbI': 'sA4?<]nph#' + 'p#R$-mG/L': 'xJp80dr|*d' + 'p.yv>BQ470': 'rs2^LqZyrx' + 'p:SPZ:e/*[': 'H]k$=x-e-{' + 'pDDrFhd%u': 't9TR?}R~%(' + 'pYRw[=6;JI': 'ENyS3po73(' + 'p]t8:Tka7q': 'Lt&mV$ktKh' + pkj3RPBUrF: 'gK5X3$8iIZ' + 'pwwZu{7f~(': 'N_~b;-nO>k' + 'pz(G*g': 'x%fcV]x!1_' + 'q2#Yv[O0:|': 's7Ivo{cHUY' + 'q7do-wLx%E': 'J#8B}2kf`U' + 'q8][)Ri}=q': 'Gk(tda>R/G' + 'q8fF$4,c6_': 'rNOpE,.$2?' + 'q:XyGf#>Wg': 'Jo|8kWbNKx' + 'q=w[' + 'qVi;t%/`F|': 'l7l~M4fz$?' + 'q[S4`,F0D=': 'm*g7V&~U8!' + 'q[}7hk(+=e': 'p@qz|I2nB5' + 'qiv/4tOlf=': 'Kg#ygW-[r' + qvITjNxkLN: 'oxS[,m0cv0' + 'qx%>fB$d+$': 'gBm$0u(##-' + 'r(h9&#wc~B': wacQ,48J1. + 'r/B|ra%L,.': '1;U0O.*[;' + 'r5C646H*+*': 'gMjU[c/jTb' + 'r6BV%|..*5': 'L+?uN^YOS5' + 'raI-$$|wT': 'kiaWqH-#7N' + 'r>g<|,FWi%': 'qK|#LatRJ9' + 'r?E$4f-d0/': P/Qv,VDEKQ + 'rPekx@n9:L': 'pRgD#Wsw2^' + 'rSneU!3}bn': 'o5,a23d~xt' + 'r]*Ioa%$bK': 'e]bdeJ!9Y9' + 's#:aIK$9^x': 'QsVUPdid;>' + 's4E?ZF[E<>': 'q^.I~1sjd/' + 's9-GL*@AXD': 's*-,K{=?iY' + 's<_WLSB3I/': 'QU($n4/X`2' + 's>f40rn,O]': 'f5K4q}cbI+' + 'sB7rZQsR@9w~i^p': 'O.Q96?A)d<' + 'skOM(?n$GG': 'Max>1>F1Glt/' + 'ssbyA9}]QP': 'A63kOvd::%' + 'stRa/S(0$F': LiEC8Ub3M8 + 'sxOMs+bE8@': 'I$=}<6%^YZ' + 'sx|l+io`B@': 'AZ}#k|F]bu>P': 'Enj/0}MeE0' + 't2&ri|[Un:': 'ufqRE1R}N|' + t3aZXtpNyi: 'v^*lp145,O82' + tfCY4j6KEZ: 'f7H42;?aOG' + 'tj&bK}%P!F': 'peu`}Xvu$<' + 'twrhzJ[k:Z': 'KJ7-X>}A,U' + 't|h3|;E4g{': 'sc15KG*9]<' + 'u-`cspdp)D': 'qiTMK38ei(' + 'u4^lH.xem5': 'FIs`1u}Odu' + 'u;|vR#mVMW': 'k/b,[~~NHp' + 'uH*Q(mcRt5': 'pT|UHy5*l&' + 'uRo^w#p5Be': 'CwUYOu3jq^' + 'uc61ToT%gt': 'x@%548M,.>' + 'ui3>b-?a@m': 'w;7hg#' + 'u|I}oElaH)': 'K^{=PMLj&`' + 'vE[T$`n9T)': 'h3jZfpL|`9' + 'vKx<0Qa!aP': 'BEF$8wbVr:' + 'vNiD6A.v#s': 'qOdNh:f:iF' + 'va/ky=f5pK': 'kS{CQ@,$y|' + 'vf)G?[_?Pf': 'DOhb!^w01(' + 'vgdj#X?8dB': 'c[b7Gzotv2' + 'vpOTK6tMr@': 'mg;tA3T#.4' + 'vppEA|$@~g': 'Ev5}H0X%#/' + 'vrn%j%6{nu': 'wkF*9:~90B' + 'vwc3}.=Z#&': 'v,6i%=6]ZK' + 'wa>{k!2cXc': 'k.>hAR#~`y' + x2d4n4ADcP: 'F&`S7L11,M' + 'xb&P*pH(Q': 'epq0k>2D`}' + 'xqrJ^d[.(O': 'k-Xc-G%>[C' + 'yX:tmrd5;]': 'q?nInVZ5|s' + 'z:*d{z(~V2': 'u;+Tt:Wc_*' + 'zSc]-^U=0=': qHwhDpenXZ + crowdanki:uuid: + 43c5ba66-9a65-11e8-90c9-a0481cc15658: 3d365148-83a7-4b53-8dbc-e3b183524025 + 43e2586a-9a65-11e8-a777-a0481cc15658: d75ad494-71f3-4909-beda-b80ec04c7a0d +deck: + name: + intent: replace + value: 'Ultimate Geography [ZH-TW]' + expected_base: + value: Ultimate Geography + description: + intent: replace + value: | + 完整說明 | 發布版本 | 貢獻指南 + + Ultimate Geography v5.3 包含: + + - 全世界 205 個主權國家 (820 張卡片) + - 59 個領土、世界區域和其他實體 (103 張卡片) + - 48 個海洋 (48 張卡片,僅有地圖) + - 7 個大陸 (7 張卡片,僅有地圖) + - 總共 319 個不同的筆記, 978 張卡片, 221 個國旗 以及 319 個地圖. + + 本牌組 包含 英文(English), 德文(German), 西班牙文(Spanish), 法文(French), 挪威語(Norwegian), 捷克語(Czech), 俄語(Russian), 荷蘭語(Dutch), 瑞典語(Swedish), 葡萄牙文(Portuguese), 簡體中文(Simplified Chinese/ZH), 繁體中文(Traditional Chinese/ZH-TW), 波蘭語(Polish), 意大利文(Italian)丹麥語(Danish)。 同時 每種語言皆有延伸版本。 為了幫助記憶和提供上下文,一些筆記帶有額外的資訊,例如類似的國旗、治理相關資訊、國家別名等。 + + 你還能使用Anki的過濾牌組功能 , 將您的學習重點放在某個種類的牌上, 例如主權國家州、一種筆記模板(例如地圖對國家)或特定大陸(例如歐洲)。 + + 這個牌組在 GitHub上維護。如果您發現錯誤、有建議或想要幫助我們,請不要猶豫打開一個問題(Issue)。 想要隨時了解新版本? 查看(Watch)我們的 GitHub Repository或者訂閱(Subscribe)發布通知(releases feed)! + expected_base: + value: | + FULL DESCRIPTION | RELEASE NOTES | CONTRIBUTING + + Ultimate Geography v5.3 features: + + - the world's 205 sovereign states (820 cards) + - 59 territories, world regions, and other entities (103 cards) + - 48 oceans and seas (48 cards, maps only) + - 7 continents (7 cards, maps only) + - for a total of 319 unique notes, 978 cards, 221 flags and 319 maps. + + The deck is available in English, German, Spanish, French, Norwegian, Czech, Russian, Dutch, Swedish, Portuguese, Chinese (simplified and traditional), Polish, Italian and Danish. An extended version is also available in each language. To help with memorisation and provide context while learning, some notes include extra information such as similar flags, governance information, alternative country names, etc. + + You can use Anki's filtered deck feature to focus your study on a subset of the deck, such as sovereign states, a single note template (e.g. map to country), or a specific continent (e.g. Europe). + + This deck is maintained on GitHub. If you spot a mistake, have a suggestion or want to help, please don't hesitate to open an issue. Want to stay informed of new releases? Watch the GitHub repository or subscribe to the releases feed! diff --git a/fixtures/ultimate-geography/overlays/languages/zh.yaml b/fixtures/ultimate-geography/overlays/languages/zh.yaml new file mode 100644 index 0000000..785bc48 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/languages/zh.yaml @@ -0,0 +1,1032 @@ +id: overlay.translation.zh +kind: translation +translations: + changes: + 'A naming dispute exists between the sea''s bordering countries, with South Korea notably supporting the name East Sea.': '日本海(Sea of Japan)的周边国家之间存在命名争议,韩国(South Korea)尤其支持东海(East Sea)的名称,中国在元朝及明朝初时期称“鲸海”。' + Abkhazia: '阿布哈兹(Abkhazia)' + Abu Dhabi: '阿布扎比(Abu Dhabi)' + Abuja: '阿布贾(Abuja)' + Accra: '阿克拉(Accra)' + Addis Ababa: '亚的斯亚贝巴(Addis Ababa)' + Adriatic Sea: '亚得里亚海(Adriatic Sea)' + Aegean Sea: '爱琴海(Aegean Sea)' + Afghanistan: '阿富汗(Afghanistan)' + Africa: '非洲(Africa)' + Akrotiri and Dhekelia: '阿克罗蒂里和德凯利亚(Akrotiri and Dhekelia)' + Alaska: '阿拉斯加(Alaska)' + Albania: '阿尔巴尼亚(Albania)' + Algeria: '阿尔及利亚(Algeria)' + Algiers: '阿尔及尔(Algiers)' + Alofi: '阿洛菲(Alofi)' + Also known as Burma.: '缅甸在国际上公认有2种名字(Myanmar, Burma)。' + Also known as Cabo Verde.: '也称为佛得角(Cabo Verde)。' + Also known as Czechia.: '也被称为捷克(Czechia)。' + Also known as East Timor.: '也称为东帝汶(Timor-Leste)。' + Also known as Kiev.: '基辅在国际上公认有2种名字(Kyiv, Kiev)。' + 'Also known as Türkiye': '' + Also known as the Gulf of Siam.: '也被称为暹罗湾(Gulf of Siam)。' + Also known as the Sea of Cortez.: '也被称为科尔特斯海(Sea of Cortez)。' + 'Also spelled as Sana''a.': '' + American Samoa: '美属萨摩亚(American Samoa)' + Amman: '安曼(Amman)' + Amsterdam: '阿姆斯特丹(Amsterdam)' + Andorra: '安道尔(Andorra)' + 'Andorra (narrower, coat of arms with motto)': '安道尔(更窄,带有座右铭的纹章)' + Andorra la Vella: '安道尔城(Andorra la Vella)' + Angola: '安哥拉(Angola)' + Anguilla: '安圭拉(Anguilla)' + Ankara: '安卡拉(Ankara)' + Antananarivo: '塔那那利佛(Antananarivo)' + Antarctica: '南极洲(Antarctica)' + Antigua and Barbuda: '安提瓜和巴布达(Antigua and Barbuda)' + Apia: '阿皮亚(Apia)' + Arabian Sea: '阿拉伯海(Arabian Sea)' + Aral Sea: '咸海(Aral Sea)' + Arctic Ocean: '北冰洋(Arctic Ocean)' + Argentina: '阿根廷(Argentina)' + Armenia: '亚美尼亚(Armenia)' + Aruba: '阿鲁巴(Aruba)' + Ashgabat: '阿什哈巴德(Ashgabat)' + Asia: '亚洲(Asia)' + Asmara: '阿斯马拉(Asmara)' + Astana: '阿斯塔纳(Astana)' + 'Asunción': '亚松森(Asunción)' + Athens: '雅典(Athens)' + Atlantic Ocean: '大西洋(Atlantic Ocean)' + Australia: '澳大利亚(Australia)' + 'Australia (white stars, two more stars)': '澳大利亚(白星,多两颗星)' + Austria: '奥地利(Austria)' + 'Austria (brighter red, wider white band)': '奥地利(更亮的红色,更宽的白色带)' + Autonomous community of Spain.: '西班牙(Spain)自治区。' + Autonomous province of South Korea.: '韩国(South Korea)自治省。' + Autonomous region of Finland.: '芬兰(Finland)自治区。' + Autonomous region of Italy.: '意大利(Italy)自治区。' + Autonomous region of Papua New Guinea.: '巴布亚新几内亚(Papua New Guinea)自治区。' + Autonomous region of Portugal.: '葡萄牙(Portugal)自治区。' + Avarua: '阿瓦鲁阿(Avarua)' + Azerbaijan: '阿塞拜疆(Azerbaijan)' + Azores: '亚速尔群岛(Azores)' + Baghdad: '巴格达(Baghdad)' + Bahrain: '巴林(Bahrain)' + 'Bahrain (narrower, fewer serrated edges, red)': '巴林(更窄,锯齿边缘更少,红色)' + Baku: '巴库(Baku)' + Bali: '巴厘岛(Bali)' + Balkan Peninsula: '巴尔干半岛(Balkan Peninsula)' + Baltic Sea: '波罗的海(Baltic Sea)' + Bamako: '巴马科(Bamako)' + Banda Sea: '班达海(Banda Sea)' + Bandar Seri Begawan: '斯里巴加湾市(Bandar Seri Begawan)' + Bangkok: '曼谷(Bangkok)' + Bangladesh: '孟加拉国(Bangladesh)' + Bangui: '班吉(Bangui)' + Banjul: '班珠尔(Banjul)' + Barbados: '巴巴多斯(Barbados)' + Barents Sea: '巴伦支海(Barents Sea)' + Basseterre: '巴斯特尔(Basseterre)' + Bay of Bengal: '孟加拉湾(Bay of Bengal)' + Bay of Biscay: '比斯开湾(Bay of Biscay)' + Beijing: '北京(Beijing)' + Beirut: '贝鲁特(Beirut)' + Belarus: '白俄罗斯(Belarus)' + Belfast: '贝尔法斯特(Belfast)' + Belgium: '比利时(Belgium)' + Belgrade: '贝尔格莱德(Belgrade)' + Belize: '伯利兹(Belize)' + Belmopan: '贝尔莫潘(Belmopan)' + Benin: '贝宁(Benin)' + Bering Strait: '白令海峡(Bering Strait)' + Berlin: '柏林(Berlin)' + Bermuda: '百慕大(Bermuda)' + Bern: '伯尔尼(Bern)' + Bhutan: '不丹(Bhutan)' + Bishkek: '比什凯克(Bishkek)' + Bissau: '比绍(Bissau)' + Black Sea: '黑海(Black Sea)' + 'Bogotá': '波哥大(Bogotá)' + Bolivia: '玻利维亚(Bolivia)' + 'Bolivia (coat of arms instead of star)': '玻利维亚(徽章而不是星形)' + Bosnia and Herzegovina: '波斯尼亚和黑塞哥维那(Bosnia and Herzegovina),简称“波黑”' + Botswana: '博茨瓦纳(Botswana)' + Bougainville: '布干维尔(Bougainville)' + 'Brasília': '巴西利亚(Brasília)' + Bratislava: '布拉迪斯拉发(Bratislava)' + Brazil: '巴西(Brazil)' + Brazzaville: '布拉柴维尔(Brazzaville)' + Bridgetown: '布里奇敦(Bridgetown)' + British Virgin Islands: '英属维尔京群岛(British Virgin Islands)' + Brunei: '文莱(Brunei)' + Brussels: '布鲁塞尔(Brussels)' + Bucharest: '布加勒斯特(Bucharest)' + Budapest: '布达佩斯(Budapest)' + Buenos Aires: '布宜诺斯艾利斯(Buenos Aires)' + Bulgaria: '保加利亚(Bulgaria)' + Burkina Faso: '布基纳法索(Burkina Faso)' + Burundi: '布隆迪(Burundi)' + Cairo: '开罗(Cairo)' + Cambodia: '柬埔寨(Cambodia)' + Cameroon: '喀麦隆(Cameroon)' + 'Cameroon (green/red/yellow, yellow star)': '喀麦隆(绿/红/黄,黄星)' + Canada: '加拿大(Canada)' + Canary Islands: '加那利群岛(Canary Islands)' + Canberra: '堪培拉(Canberra)' + Cape Verde: '佛得角(Cape Verde)' + Caracas: '加拉加斯(Caracas)' + Cardiff: '卡迪夫(Cardiff)' + Caribbean Sea: '加勒比海(Caribbean Sea)' + Caspian Sea: '里海(Caspian Sea)' + Castries: '卡斯特里(Castries)' + Cayman Islands: '开曼群岛(Cayman Islands)' + Celebes Sea: '苏拉威西海(Celebes Sea, 亦称西里伯斯海)' + Celtic Sea: '凯尔特海(Celtic Sea)' + Central African Republic: '中非共和国(Central African Republic)' + Cetinje is an honorary capital.: '采蒂涅(Cetinje),黑山古都和宗教、历史中心。' + Chad: '乍得(Chad)' + 'Chad (slightly darker blue)': '乍得(略深蓝色)' + Charlotte Amalie: '夏洛特阿马利亚(Charlotte Amalie)' + Chile: '智利(Chile)' + China: '中国(China)' + 'Chișinău': '基希讷乌(Chișinău)' + City of San Marino: '圣马力诺市(City of San Marino)' + Ciudad de la Paz: '拉巴斯城(Ciudad de la Paz)' + Claimed and controlled: '声称对其拥有主权并拥有实际控制权' + Claimed but not controlled: '声称对其拥有主权但没有实际控制权' + Colombia: '哥伦比亚(Colombia)' + 'Colombia (no coat of arms)': '哥伦比亚(无国徽)' + Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: '科伦坡(Colombo)通常被称为首都,但科伦坡郊区的斯里贾亚瓦德纳普拉科特(Sri Jayawardenepura Kotte)是官方的立法首都。' + Comoros: '科摩罗(Comoros)' + Conakry: '科纳克里(Conakry)' + Constituent country in the Kingdom of Denmark.: '丹麦属地之一。' + Constituent country of the Kingdom of the Netherlands.: '荷兰(Netherlands)的一部分。' + Constituent country of the United Kingdom.: '英国属地之一。' + Cook Islands: '库克群岛(Cook Islands)' + Copenhagen: '哥本哈根(Copenhagen)' + Coral Sea: '珊瑚海(Coral Sea)' + Corsica: '科西嘉岛(Corsica)' + Costa Rica: '哥斯达黎加(Costa Rica)' + Croatia: '克罗地亚(Croatia)' + Crown dependency of the United Kingdom.: '英国(U.K.)的皇家属地。' + Cuba: '古巴(Cuba)' + 'Cuba (red triangle, blue stripes)': '古巴(红三角,蓝条纹)' + 'Curaçao': '库拉索(Curaçao)' + 'Curaçao (two stars in top-left corner)': '库拉索岛(左上角的两颗星)' + Cyprus: '塞浦路斯(Cyprus)' + Czech Republic: '捷克共和国(Czech Republic)' + Dakar: '达喀尔(Dakar)' + Damascus: '大马士革(Damascus)' + Dead Sea: '死海(Dead Sea)' + Democratic Republic of the Congo: '刚果民主共和国(Democratic Republic of the Congo)' + Denmark: '丹麦(Denmark)' + Denmark Strait: '丹麦海峡(Denmark Strait)' + Dhaka: '达卡(Dhaka)' + Dili: '帝力(Dili)' + 'Disputed; claimed by Israel; Ramallah is the administrative centre.': '有争议的领土; 以色列(Israel)声称对其拥有主权; 拉马拉(Ramallah)是行政中心。' + 'Disputed; claimed by Palestine.': '有争议的领土; 巴勒斯坦(Palestine)声称对其拥有主权。' + Djibouti: '吉布提(Djibouti)' + Dodoma: '多多马(Dodoma)' + Doha: '多哈(Doha)' + Dominica: '多米尼加(Dominica)' + Dominican Republic: '多米尼加共和国(Dominican Republic)' + Dublin: '都柏林(Dublin)' + Dushanbe: '杜尚别(Dushanbe)' + East China Sea: '东海(East China Sea)' + East Siberian Sea: '东西伯利亚海(East Siberian Sea)' + Ecuador: '厄瓜多尔(Ecuador)' + 'Ecuador (with coat of arms)': '厄瓜多尔(有国徽)' + Edinburgh: '爱丁堡(Edinburgh)' + Egypt: '埃及(Egypt)' + 'Egypt (emblem instead of text), Yemen (no text)': '埃及(标志代替文字)、也门(无文字)' + 'Egypt (with emblem), Iraq (with text)': '埃及(带徽章)、伊拉克(带文字)' + El Salvador: '萨尔瓦多(El Salvador)' + 'El Salvador (different coat of arms, slightly darker blue)': '萨尔瓦多(不同的纹章,略深的蓝色)' + England: '英格兰(England)' + English Channel: '英吉利海峡(English Channel)' + Equatorial Guinea: '赤道几内亚(Equatorial Guinea)' + Eritrea: '厄立特里亚(Eritrea)' + Estonia: '爱沙尼亚(Estonia)' + Eswatini: '埃斯瓦蒂尼(Eswatini)' + Ethiopia: '埃塞俄比亚(Ethiopia)' + Europe: '欧洲(Europe)' + European Union: '欧盟(European Union)' + Falkland Islands: '福克兰群岛(Falkland Islands)' + Faroe Islands: '法罗群岛(Faroe Islands)' + Federated States of Micronesia: '密克罗尼西亚联邦(Federated States of Micronesia)' + Fiji: '斐济(Fiji)' + Finland: '芬兰(Finland)' + Formerly Zaire.: '原名扎伊尔(Zaire)。' + Formerly known as Macedonia.: '原名马其顿(Macedonia)。' + France: '法国(France)' + Freetown: '弗里敦(Freetown)' + French Guiana: '法属圭亚那(French Guiana)' + French Polynesia: '法属波利尼西亚(French Polynesia)' + Funafuti: '富纳富提(Funafuti)' + Gabon: '加蓬(Gabon)' + Gaborone: '哈博罗内(Gaborone)' + Georgetown: '乔治敦(Georgetown)' + Georgia: '格鲁吉亚(Georgia)' + Germany: '德国(Germany)' + Ghana: '加纳(Ghana)' + 'Ghana (star instead of coat of arms)': '加纳(星而不是国徽)' + Gibraltar: '直布罗陀(Gibraltar)' + Gitega: '基特加(Gitega)' + Greece: '希腊(Greece)' + Greenland: '格陵兰(Greenland)' + Grenada: '格林纳达(Grenada)' + Guadeloupe: '瓜德罗普岛(Guadeloupe)' + Guam: '关岛(Guam)' + Guatemala: '危地马拉(Guatemala)' + Guatemala City: '危地马拉市(Guatemala City)' + Guernsey: '根西岛(Guernsey)' + Guinea: '几内亚(Guinea)' + 'Guinea (green and red flipped, slightly darker green)': '几内亚(绿色和红色翻转,绿色略深)' + Guinea-Bissau: '几内亚比绍(Guinea-Bissau)' + Gulf of Alaska: '阿拉斯加湾(Gulf of Alaska)' + Gulf of California: '加利福尼亚湾(Gulf of California)' + Gulf of Carpentaria: '卡奔塔利亚湾(Gulf of Carpentaria)' + Gulf of Guinea: '几内亚湾(Gulf of Guinea)' + Gulf of Mexico: '墨西哥湾(Gulf of Mexico)' + Gulf of Thailand: '泰国湾(Gulf of Thailand)' + Guyana: '圭亚那(Guyana)' + 'Hagåtña': '哈加特纳(Hagåtña)' + Haiti: '海地(Haiti)' + Hanoi: '河内(Hanoi)' + Harare: '哈拉雷(Harare)' + Hargeisa: '哈尔格萨(Hargeisa)' + Havana: '哈瓦那(Havana)' + Hawaii: '夏威夷(Hawaii)' + Helsinki: '赫尔辛基(Helsinki)' + Historical and cultural region in Northern Europe, which includes the countries of Denmark, Norway and Sweden, and sometimes Finland and Iceland.: '北欧(Northern Europe)的历史和文化区域,包括丹麦(Denmark)、挪威(Norway)和瑞典(Sweden),有时也包括芬兰(Finland)和冰岛(Iceland)。' + Honduras: '洪都拉斯(Honduras)' + Hong Kong: '香港(Hong Kong)' + Honiara: '霍尼亚拉(Honiara)' + Hudson Bay: '哈德逊湾(Hudson Bay)' + Hungary: '匈牙利(Hungary)' + Iceland: '冰岛(Iceland)' + 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': '冰岛(蓝底,红白交叉),挪威(红底,蓝白交叉)' + 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': '冰岛(蓝底,红十字),法罗群岛(白底,红蓝交叉)' + Independent state claimed by Georgia.: '格鲁吉亚(Georgia)宣称拥有主权的独立国家。' + Independent state claimed by Moldova.: '摩尔多瓦(Moldova)宣称拥有主权的独立国家。' + Independent state claimed by Somalia.: '索马里(Somalia)宣称拥有主权的独立国家。' + India: '印度(India)' + Indian Ocean: '印度洋(Indian Ocean)' + Indonesia: '印度尼西亚(Indonesia)' + 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': '印度尼西亚(白色和红色翻转,红色更亮),摩纳哥(白色和红色翻转,更窄)' + 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': '印度尼西亚(更宽,红色更亮),波兰(红色和白色翻转,更宽)' + Iran: '伊朗(Iran)' + Iraq: '伊拉克(Iraq)' + 'Iraq (text instead of emblem), Yemen (no emblem)': '伊拉克(文字代替徽记)、也门(无徽记)' + Ireland: '爱尔兰(Ireland)' + 'Ireland (orange and green flipped, wider)': '爱尔兰(橙色和绿色翻转,更宽)' + Islamabad: '伊斯兰堡(Islamabad)' + Island of Indonesia.: '印度尼西亚(Indonesia)的岛屿之一。' + Isle of Man: '马恩岛(Isle of Man)' + Israel: '以色列(Israel)' + Italy: '意大利(Italy)' + Ivory Coast: '科特迪瓦(Ivory Coast)' + 'Ivory Coast (green and orange flipped, narrower)': '科特迪瓦(绿色和橙色翻转,更窄)' + Jakarta: '雅加达(Jakarta)' + Jamaica: '牙买加(Jamaica)' + Japan: '日本(Japan)' + Java: '爪哇岛(Java)' + Jeju: '济州岛(Jeju)' + Jersey: '泽西岛(Jersey)' + Jerusalem: '耶路撒冷(Jerusalem)' + Jordan: '约旦(Jordan)' + Juba: '朱巴(Juba)' + Kabul: '喀布尔(Kabul)' + Kaliningrad Oblast: '加里宁格勒州(Kaliningrad Oblast)' + Kampala: '坎帕拉(Kampala)' + Kathmandu: '加德满都(Kathmandu)' + Kazakhstan: '哈萨克斯坦(Kazakhstan)' + Kenya: '肯尼亚(Kenya)' + Khartoum: '喀土穆(Khartoum)' + Kigali: '基加利(Kigali)' + Kingston: '金斯敦(Kingston)' + Kingstown: '金斯敦(Kingstown)' + Kinshasa: '金沙萨(Kinshasa)' + Kiribati: '基里巴斯(Kiribati)' + Known as Nur-Sultan between 2019 and 2022: '在 2019 年至 2022 年间称为努尔苏丹 (Nur-Sultan)' + Known as Swaziland until 2018.: '2018年之前被称为斯威士兰(Swaziland)。' + Kosovo: '科索沃(Kosovo)' + Kuala Lumpur: '吉隆坡(Kuala Lumpur)' + Kuwait: '科威特(Kuwait)' + Kuwait City: '科威特城(Kuwait City)' + Kyiv: '基辅(Kyiv)' + Kyrgyzstan: '吉尔吉斯斯坦(Kyrgyzstan)' + Laayoune: '阿尤恩(Laayoune)' + Labrador Sea: '拉布拉多海(Labrador Sea)' + Laos: '老挝(Laos)' + Latvia: '拉脱维亚(Latvia)' + 'Latvia (darker red, narrower white band)': '拉脱维亚(较深的红色,较窄的白色带)' + Lebanon: '黎巴嫩(Lebanon)' + Lesotho: '莱索托(Lesotho)' + Liberia: '利比里亚(Liberia)' + Libreville: '利伯维尔(Libreville)' + Libya: '利比亚(Libya)' + Liechtenstein: '列支敦士登(Liechtenstein)' + Lilongwe: '利隆圭(Lilongwe)' + Lima: '利马(Lima)' + Lisbon: '里斯本(Lisbon)' + Lithuania: '立陶宛(Lithuania)' + Ljubljana: '卢布尔雅那(Ljubljana)' + 'Lomé': '洛美(Lomé)' + London: '伦敦(London)' + Luanda: '罗安达(Luanda)' + Lusaka: '卢萨卡(Lusaka)' + Luxembourg: '卢森堡(Luxembourg)' + 'Luxembourg (lighter blue)': '卢森堡(浅蓝色)' + Luxembourg City: '卢森堡市(Luxembourg City)' + Macau: '澳门(Macau)' + Madagascar: '马达加斯加(Madagascar)' + Madeira: '马德拉(Madeira)' + Madrid: '马德里(Madrid)' + Majuro: '马朱罗(Majuro)' + Malawi: '马拉维(Malawi)' + Malaysia: '马来西亚(Malaysia)' + Maldives: '马尔代夫(Maldives)' + Mali: '马里(Mali)' + 'Mali (red and green flipped, slightly brighter green)': '马里(红绿翻转,绿色略亮)' + Malta: '马耳他(Malta)' + 'Malé': '马累(Malé)' + Managua: '马那瓜(Managua)' + Manama: '麦纳麦(Manama)' + Manila: '马尼拉(Manila)' + Maputo: '马普托(Maputo)' + Mariehamn: '玛丽港(Mariehamn)' + Marshall Islands: '马绍尔群岛(Marshall Islands)' + Martinique: '马提尼克(Martinique)' + Maseru: '马塞卢(Maseru)' + Mauritania: '毛里塔尼亚(Mauritania)' + Mauritius: '毛里求斯(Mauritius)' + Mayotte: '马约特岛(Mayotte)' + Mbabane: '姆巴巴内(Mbabane)' + Mediterranean Sea: '地中海(Mediterranean Sea)' + Melanesia: '美拉尼西亚(Melanesia)' + Mexico: '墨西哥(Mexico)' + Mexico City: '墨西哥城(Mexico City)' + Micronesia: '密克罗尼西亚(Micronesia)' + Minsk: '明斯克(Minsk)' + Mogadishu: '摩加迪沙(Mogadishu)' + Moldova: '摩尔多瓦(Moldova)' + 'Moldova (wider, coat of arms with eagle)': '摩尔多瓦(更宽,鹰纹)' + Monaco: '摩纳哥(Monaco)' + 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': '摩纳哥(更窄,红色更深),波兰(红白翻转,红色更深)' + Mongolia: '蒙古(Mongolia)' + Monrovia: '蒙罗维亚(Monrovia)' + Montenegro: '黑山(Montenegro)' + Montevideo: '蒙得维的亚(Montevideo)' + Morocco: '摩洛哥(Morocco)' + Moroni: '莫罗尼(Moroni)' + Moscow: '莫斯科(Moscow)' + Mozambique: '莫桑比克(Mozambique)' + Muscat: '马斯喀特(Muscat)' + Myanmar: '缅甸(Myanmar)' + 'N''Djamena': '恩贾梅纳(N''Djamena)' + Nairobi: '内罗毕(Nairobi)' + Namibia: '纳米比亚(Namibia)' + Nassau: '拿骚(Nassau)' + Nauru: '瑙鲁(Nauru)' + 'Nauru (single star below yellow band)': '瑙鲁(黄带下方的单星)' + 'Nauru has no official capital; the Yaren District is the de facto capital.': '瑙鲁(Nauru)无正式首都,政府机关设在亚伦区(the Yaren District)。' + Naypyidaw: '内比都(Naypyidaw)' + Nepal: '尼泊尔(Nepal)' + Netherlands: '荷兰(Netherlands)' + 'Netherlands (darker blue)': '荷兰(深蓝色)' + New Caledonia: '新喀里多尼亚(New Caledonia)' + New Delhi: '新德里(New Delhi)' + New Zealand: '新西兰(New Zealand)' + 'New Zealand (red stars, two fewer stars)': '新西兰(红星,少两颗星)' + Ngerulmud: '恩格鲁穆德(Ngerulmud)' + Niamey: '尼亚美(Niamey)' + Nicaragua: '尼加拉瓜(Nicaragua)' + 'Nicaragua (different coat of arms, slightly lighter blue)': '尼加拉瓜(不同的纹章,略浅的蓝色)' + Nicosia: '尼科西亚(Nicosia)' + Niger: '尼日尔(Niger)' + Nigeria: '尼日利亚(Nigeria)' + Niue: '纽埃(Niue)' + North America: '北美洲(North America)' + North Korea: '朝鲜(North Korea)' + North Macedonia: '北马其顿(North Macedonia)' + North Nicosia: '北尼科西亚(North Nicosia)' + North Sea: '北海(North Sea)' + Northern Cyprus: '北塞浦路斯(Northern Cyprus)' + Northern Ireland: '北爱尔兰(Northern Ireland)' + Northern Mariana Islands: '北马里亚纳群岛(Northern Mariana Islands)' + Norway: '挪威(Norway)' + 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': '挪威(红底蓝十字)、法罗群岛(白底红蓝十字)' + Norwegian Sea: '挪威海(Norwegian Sea)' + Not a sovereign country: '不是主权国家' + Nouakchott: '努瓦克肖特(Nouakchott)' + 'Nouméa': '努美阿(Nouméa)' + 'Nukuʻalofa': '努库阿洛法(Nukuʻalofa)' + Nuuk: '努克(Nuuk)' + 'Oblast (administrative region) of the Russian Federation.': '俄罗斯(Russia)联邦州(行政区)。' + Oceania: '大洋洲(Oceania)' + Official capital was moved from Bujumbura to Gitega in 2019.: '2019 年官方首都从布琼布拉(Bujumbura)迁至基特加(Gitega)。' + Official capital was moved from Malabo to Ciudad de la Paz in 2026.: '2026 年官方首都从马拉博(Malabo)迁至拉巴斯城(Ciudad de la Paz)。' + 'Officially Côte d''Ivoire.': '官方名为科特迪瓦(Côte d''Ivoire), 全称科特迪瓦共和国(The Republic of Côte d''Ivoire)' + Officially Luxembourg.: '官方称为卢森堡(Luxembourg)。' + Oman: '阿曼(Oman)' + Oranjestad: '奥拉涅斯塔德(Oranjestad)' + Oslo: '奥斯陆(Oslo)' + Ottawa: '渥太华(Ottawa)' + Ouagadougou: '瓦加杜古(Ouagadougou)' + Overseas department of France.: '法国(France)的海外省份。' + Overseas territory of France.: '法国(France)的海外领土。' + Overseas territory of the United Kingdom.: '英国(U.K.)的海外领土。' + Pacific Ocean: '太平洋(Pacific Ocean)' + Pakistan: '巴基斯坦(Pakistan)' + Palau: '帕劳(Palau)' + Palestine: '巴勒斯坦(Palestine)' + 'Palestine (black/white/green, red arrow)': '巴勒斯坦(黑/白/绿,红色箭头)' + 'Palestine (no symbol)': '巴勒斯坦(无符号)' + Palikir: '帕利基尔(Palikir)' + Panama: '巴拿马(Panama)' + Panama City: '巴拿马城(Panama City)' + Papeete: '帕皮提(Papeete)' + Papua New Guinea: '巴布亚新几内亚(Papua New Guinea)' + Paraguay: '巴拉圭(Paraguay)' + Paramaribo: '帕拉马里博(Paramaribo)' + Paris: '巴黎(Paris)' + Partially recognised state claimed by China.: '中国宣称对台湾拥有主权,但仅被部分国家承认' + Partially recognised state claimed by Morocco. Also known as Western Sahara.: '摩洛哥(Morocco)宣称拥有主权但仅被部分国家承认的地区。' + Partially recognised state claimed by Serbia.: '塞尔维亚(Serbia)宣称拥有主权但仅被部分国家承认的地区。' + Persian Gulf: '波斯湾(Persian Gulf)' + Peru: '秘鲁(Peru)' + Philippine Sea: '菲律宾海(Philippine Sea)' + Philippines: '菲律宾(Philippines)' + Phnom Penh: '金边(Phnom Penh)' + Podgorica: '波德戈里察(Podgorica)' + Poland: '波兰(Poland)' + Polynesia: '波利尼西亚(Polynesia)' + Port Louis: '路易港(Port Louis)' + Port Moresby: '莫尔兹比港(Port Moresby)' + Port Vila: '维拉港(Port Vila)' + Port of Spain: '西班牙港(Port of Spain)' + Port-au-Prince: '太子港(Port-au-Prince)' + Porto-Novo: '波多诺伏(Porto-Novo, 也被称之为“新港”)' + Portugal: '葡萄牙(Portugal)' + Prague: '布拉格(Prague)' + Praia: '普拉亚(Praia)' + Pretoria, Cape Town, Bloemfontein: '比勒陀利亚(Pretoria)、开普敦(Cape Town)、布隆方丹(Bloemfontein)' + Pristina: '普里什蒂纳(Pristina)' + Puerto Rico: '波多黎各(Puerto Rico)' + 'Puerto Rico (blue triangle, red stripes)': '波多黎各(蓝色三角形,红色条纹)' + Pyongyang: '平壤(Pyongyang)' + Qatar: '卡塔尔(Qatar)' + 'Qatar (wider, more serrated edges, maroon)': '卡塔尔(更宽,锯齿状边缘,栗色)' + Quito: '基多(Quito)' + Rabat: '拉巴特(Rabat)' + Red Sea: '红海(Red Sea)' + Region of France.: '法国(France)地区。' + Republic of the Congo: '刚果共和国(Republic of the Congo)' + 'Reykjavík': '雷克雅未克(Reykjavík)' + Riga: '里加(Riga)' + Riyadh: '利雅得(Riyadh)' + Romania: '罗马尼亚(Romania)' + 'Romania (slightly lighter blue)': '罗马尼亚(略浅蓝色)' + Rome: '罗马(Rome)' + Roseau: '罗索(Roseau)' + Russia: '俄罗斯(Russia)' + 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': '俄罗斯(无国徽)、斯洛文尼亚(更宽、更小的国徽)' + Rwanda: '卢旺达(Rwanda)' + 'Réunion': '留尼汪岛(Réunion)' + Sahrawi Arab Democratic Republic: '阿拉伯撒哈拉民主共和国(Sahrawi Arab Democratic Republic)' + Saint Kitts and Nevis: '圣基茨和尼维斯(Saint Kitts and Nevis)' + Saint Lucia: '圣卢西亚(Saint Lucia)' + Saint Martin: '圣马丁(Saint Martin)' + Saint Vincent and the Grenadines: '圣文森特和格林纳丁斯(Saint Vincent and the Grenadines)' + Samoa: '萨摩亚(Samoa)' + 'San José': '圣何塞(San José)' + San Juan: '圣胡安(San Juan)' + San Marino: '圣马力诺(San Marino)' + San Salvador: '圣萨尔瓦多(San Salvador)' + Sanaa: '萨那(Sanaa)' + Santiago: '圣地亚哥(Santiago)' + Santo Domingo: '圣多明各(Santo Domingo)' + Sarajevo: '萨拉热窝(Sarajevo)' + Sardinia: '撒丁岛(Sardinia)' + Saudi Arabia: '沙特阿拉伯(Saudi Arabia)' + Scandinavia: '斯堪的纳维亚(Scandinavia)' + Scotland: '苏格兰(Scotland)' + Sea of Galilee: '加利利海(Sea of Galilee)' + Sea of Japan: '日本海(Sea of Japan)' + Sea of Okhotsk: '鄂霍次克海(Sea of Okhotsk)' + Semi-autonomous region of Tanzania.: '坦桑尼亚(Tanzania)的半自治区。' + Senegal: '塞内加尔(Senegal)' + 'Senegal (green/yellow/red, green star)': '塞内加尔(绿/黄/红,绿星)' + Seoul: '首尔(Seoul)' + Serbia: '塞尔维亚(Serbia)' + Seychelles: '塞舌尔(Seychelles)' + Sicily: '西西里岛(Sicily)' + Sierra Leone: '塞拉利昂(Sierra Leone)' + Singapore: '新加坡(Singapore)' + Sint Maarten: '圣马丁岛(Sint Maarten)' + Skopje: '斯科普里(Skopje)' + Slovakia: '斯洛伐克(Slovakia)' + 'Slovakia (narrower, bigger coat of arms)': '斯洛伐克(更窄、更大的国徽)' + 'Slovakia (with coat of arms)': '斯洛伐克(有国徽)' + Slovenia: '斯洛文尼亚(Slovenia)' + Sofia: '索菲亚(Sofia)' + Solomon Islands: '所罗门群岛(Solomon Islands)' + Somalia: '索马里(Somalia)' + Somaliland: '索马里兰(Somaliland)' + South Africa: '南非(South Africa)' + 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': '南非没有法定首都,政府部门分为三个城市:行政首都(中央政府所在地)为比勒陀利亚(Pretoria, 2005年起讨论改为 茨瓦内/Tshwane 但至今没有实施),立法首都(议会所在地)为开普敦(Cape Town),司法首都(最高法院所在地)为布隆方丹(Bloemfontein)。' + South America: '南美洲(South America)' + South China Sea: '南海(South China Sea)' + South Korea: '韩国(South Korea)' + South Ossetia: '南奥塞梯(South Ossetia)' + South Sudan: '南苏丹(South Sudan)' + South Tarawa: '南塔拉瓦(South Tarawa)' + Southern Ocean: '南大洋(Southern Ocean)' + Sovereign country: '主权国家' + Spain: '西班牙(Spain)' + Special Administrative Region of China.: '中国特别行政区。' + Sri Jayawardenepura Kotte: '斯里贾亚瓦德纳普拉科特(Sri Jayawardenepura Kotte),亦称科特' + Sri Lanka: '斯里兰卡(Sri Lanka)' + 'St. George''s': '圣乔治(St. George''s)' + 'St. John''s': '圣约翰(St. John''s)' + State of the United States.: '美国(U.S.)的州。' + State recognised only by Turkey and claimed by Cyprus.: '塞浦路斯(Cyprus)声称拥有主权的地区但仅被土耳其(Turkey)承认。' + Stockholm: '斯德哥尔摩(Stockholm)' + Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: '大洋洲(Oceania)的一个区域,由太平洋(Pacific Ocean)中部和南部的数千个小岛组成。' + Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: '大洋洲的一个区域包括西太平洋的数千个小岛。' + Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: '大洋洲(Oceania)的一个区域,包括瓦努阿图(Vanuatu)、所罗门群岛(Solomon Islands)、斐济(Fiji)和巴布亚新几内亚(Papua New Guinea)。' + Sucre: '苏克雷(Sucre)' + Sudan: '苏丹(Sudan)' + 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': '苏丹(红/白/黑、绿箭头)、阿拉伯撒哈拉民主共和国(有星形和新月形)' + Sukhumi: '苏呼米(Sukhumi)' + Sumatra: '苏门答腊岛(Sumatra)' + Suriname: '苏里南(Suriname)' + Suva: '苏瓦(Suva)' + Svalbard: '斯瓦尔巴群岛(Svalbard)' + Sweden: '瑞典(Sweden)' + Switzerland: '瑞士(Switzerland)' + 'Switzerland has no official capital; Bern is the de facto capital.': '瑞士没有官方首都;伯尔尼(Bern)是实际首都。' + Syria: '叙利亚(Syria)' + 'São Tomé': '圣多美(São Tomé)' + 'São Tomé and Príncipe': '圣多美和普林西比(São Tomé and Príncipe)' + Taipei: '台北(Taipei)' + Taiwan: '台湾(Taiwan)' + Tajikistan: '塔吉克斯坦(Tajikistan)' + Tallinn: '塔林(Tallinn)' + Tanzania: '坦桑尼亚(Tanzania)' + Tashkent: '塔什干(Tashkent)' + Tasman Sea: '塔斯曼海(Tasman Sea)' + Tbilisi: '第比利斯(Tbilisi)' + Tegucigalpa: '特古西加尔巴(Tegucigalpa)' + Tehran: '德黑兰(Tehran)' + Thailand: '泰国(Thailand)' + The Bahamas: '巴哈马(The Bahamas)' + The Gambia: '冈比亚(The Gambia)' + Thimphu: '廷布(Thimphu)' + Timor Sea: '帝汶海(Timor Sea)' + Timor-Leste: '东帝汶(Timor-Leste)' + Tirana: '地拉那(Tirana)' + Tiraspol: '蒂拉斯波尔(Tiraspol)' + Togo: '多哥(Togo)' + Tokyo: '东京(Tokyo)' + Tonga: '汤加(Tonga)' + Transnistria: '德涅斯特河沿岸(Transnistria)' + Trinidad and Tobago: '特立尼达和多巴哥(Trinidad and Tobago)' + Tripoli: '的黎波里(Tripoli)' + Tskhinvali: '茨欣瓦利(Tskhinvali)' + Tunis: '突尼斯(Tunis)' + Tunisia: '突尼斯(Tunisia)' + Turkey: '土耳其(Turkey)' + Turkmenistan: '土库曼斯坦(Turkmenistan)' + Turks and Caicos Islands: '特克斯和凯科斯群岛(Turks and Caicos Islands)' + Tuvalu: '图瓦卢(Tuvalu)' + 'Tórshavn': '托尔斯港(Tórshavn)' + Uganda: '乌干达(Uganda)' + Ukraine: '乌克兰(Ukraine)' + Ulaanbaatar: '乌兰巴托(Ulaanbaatar)' + Unincorporated internal area of Norway.: '挪威(Norway)的无建制内部区域。' + Unincorporated territory of the United States.: '美国(U.S.)无建制属地。' + United Arab Emirates: '阿拉伯联合酋长国(United Arab Emirates)' + United Kingdom: '英国(United Kingdom)' + United States Virgin Islands: '美属维尔京群岛(United States Virgin Islands)' + United States of America: '美国(United States of America, 全称美利坚合众国)' + Uruguay: '乌拉圭(Uruguay)' + Uzbekistan: '乌兹别克斯坦(Uzbekistan)' + Vaduz: '瓦杜兹(Vaduz)' + Valletta: '瓦莱塔(Valletta)' + Vanuatu: '瓦努阿图(Vanuatu)' + Vatican City: + notes.note.vatican-city.fields.field.capital: '梵蒂冈城(Vatican City)' + notes.note.vatican-city.fields.field.country: '梵蒂冈(Vatican City)' + Venezuela: '委内瑞拉(Venezuela)' + Victoria: '维多利亚(Victoria)' + Vienna: '维也纳(Vienna)' + Vientiane: '万象(Vientiane)' + Vietnam: '越南(Vietnam)' + Vilnius: '维尔纽斯(Vilnius)' + Wales: '威尔士(Wales)' + Wallis and Futuna: '瓦利斯和富图纳群岛(Wallis and Futuna)' + Warsaw: '华沙(Warsaw)' + Washington, D.C.: '华盛顿特区(Washington, D.C.)' + Wellington: '惠灵顿(Wellington)' + While Amsterdam is the official capital, The Hague is the seat of the executive and legislative branches.: '虽然阿姆斯特丹(Amsterdam)是官方首都,但海牙(Hague)是行政和立法部门的所在地。' + While Dodoma is the official capital, Dar es Salaam is the de facto seat of government.: '虽然多多马(Dodoma)是官方首都,但达累斯萨拉姆(Dar es Salaam)是实际政府所在地。' + 'While Laayoune, also known as El Aaiún, is the declared capital, Tifariti is the de facto seat of government.': '虽然阿尤恩(Laayoune,西班牙语称为El Aaiún,中文含义是泉水),是官方宣称的首都,但提法里提(Tifariti)是实际政府所在地。' + While Mbabane is the official, executive capital, Lobamba is the traditional, spiritual and legislative capital.: '姆巴巴内(Mbabane)是官方的行政首都,而洛班巴(Lobamba)是传统观念中的首都、精神首都和立法首都。' + While Porto-Novo is the official capital, Cotonou is the de facto seat of government.: '虽然波多诺伏(Porto-Novo)是官方首都,但科托努(Cotonou)是实际政府所在地。' + While Sucre is the constitutional capital, La Paz is the seat of government.: '虽然苏克雷(Sucre)是宪法认定的行政首都,但拉巴斯(La Paz)是政府所在地和财政中心。' + While Yamoussoukro is the official capital, Abidjan is the de facto seat of government.: '虽然亚穆苏克罗(Yamoussoukro)是官方首都,但阿比让(Abidjan)是实际政府所在地。' + White Sea: '白海(White Sea)' + Willemstad: '威廉斯塔德(Willemstad)' + Windhoek: '温得和克(Windhoek)' + World region covering the Australian continent and most of the islands in the Pacific Ocean.: '包括澳大利亚大陆(Australian continent)和太平洋(Pacific Ocean)大部分岛屿的世界大洲。' + Yamoussoukro: '亚穆苏克罗(Yamoussoukro)' + 'Yaoundé': '雅温得(Yaoundé)' + Yaren: '亚伦(Yaren)' + Yellow Sea: '黄海(Yellow Sea)' + Yemen: '也门(Yemen)' + Yerevan: '埃里温(Yerevan)' + Zagreb: '萨格勒布(Zagreb)' + Zambia: '赞比亚(Zambia)' + Zanzibar: '桑给巴尔(Zanzibar)' + Zimbabwe: '津巴布韦(Zimbabwe)' + 'Åland Islands': '奥兰群岛(Åland Islands)' + additions: + notes.note.myanmar.fields.field.capital-info: '缅甸于2005年11月6日首都从仰光(Yangon)迁往内比都(Naypyidaw)。' + notes.note.netherlands.fields.field.country-info: '现名尼德兰(Netherlands),中文通译为荷兰(Holanda)' + notes.note.yellow-sea.fields.field.country-info: '虽然国际上普遍认为黄海(Yellow Sea)包含了渤海(Bohai Sea),但在中国(China),它们通常被视为两个独立的海域。' + variables: + label.capital: + Capital: '首都' + label.capital-hint.answer: + 'Hint: {{Capital hint}}': '提示:{{Capital hint}}' + label.flag: + Flag: '旗帜' + label.location: + Location: '位置' + note-type.name: + Ultimate Geography: 'Ultimate Geography [ZH]' + sentence.flag-similar: + 'Flag similar to {{Flag similarity}}.': '旗帜类似于:{{Flag similarity}}。' + adapter_ids: + crowdanki:guid: + ')Y}Qy.3GK': 'jgG[|g62bu' + '5JJ2]i)p4': 'caU' + 'AES],s:8=': 'Q*@?lCT!6Y' + 'B:x`;1ezpn' + 'J%,fuysl*&': 'CV)zM-W' + 'M$bc0[jYf0': 'kLO=|#$+U:' + 'O~pXkAbd3': 'b&jm5o,1&}' + 'QSq%~rFaP-': 'm,bcMekm)F' + 'Qlib4)q2Hk': 'KaG.-9WVf?' + 'SlOEXfq#|': 'bDY>zGS#mC' + 'W5?6yQZD2': 'lgk=]hEdgU' + '_]DH+g9!$': 'zzE~LA%s{3' + 'b@M[XTc?}5': 'hyd{qAD}`v' + 'bD?R}i8_[c': 'q5WM.]hP/0D#' + 'bd~Ght5tLR': 'u]nUNO+ITf' + 'bk2ByZIeU+': 'd1TE(!31ZM' + 'bnEK#)[;qT': 'bY]b2xX@do' + 'c(,2{yXI#E': 'w5xWQO/x*-' + 'c,5(mUGzYT': 'IJx`PW+GwM' + 'cAlyc~G.y%': 'lVC}2]rXGx' + 'cEO^y)Md[G': 'ka~R7f}F%C' + 'cL?8!xP#Wj': 'e,fTnR*#azp' + 'cYKvF#ptF7': 'GTU:+%4s@|' + 'c^,~EC6Pb2': 'b|pv4=f4>n' + 'ck!6;xK$/^': 'P@BL~hgv1=' + 'cv+Nq9:a/N^[' + 'd3c`&/1Y^D': 'rY(r:vfR-a' + 'd5m8%D6,;u': 'wr%uNr%3yR' + 'dY96`C-f(_': 'K4=tg/ma1*' + 'd``B*6:eCx': 'lLRpv)^=-$' + 'dbP/Cj}sB+': 'r^Ba54xJuo' + 'de=9c:,@j9': 's:lS}~kTP7' + 'dlU7VG|3Gd': 'L0pY~Sl++C' + 'd}R:qHj1,7': 'B=Vz){?|~&' + 'e!KSebP}pt': 'Ne?2/nxwzz' + 'e+/O]%*qfk': 'Nwl0,aw[|b' + 'e.t$Vi5,>f': CpvA2cf_vf + 'e42BpV,P}C': 'OGFso_;Eyd' + 'e5P*D#I{m{': 'OKGkn:78,$' + 'eHH?U': 'P+Aat{#Q`@' + 'euW0jm=wfR': 'Ks:+^W@?f;' + 'e{e8vi@^PY': 'Fa]aBu=L1z' + 'f#4-L[mp5*': 'HJiVX@m]Bz' + 'f*2@qiHh_1': 'tXc}dmX&<>' + 'f*LO~>!0PN': 'j@1%Q{#*[u' + 'f21=-A~W;~': 'e<)log]FU?' + 'f>c7x2E]Q6': 'h=:xts:/of' + 'fRSM{K+;[w': bd/jbWJUgZ + 'fT!w8R#>dg': 'I6g)*h8Ip<' + 'fV`&p81%d(': 'cxf-&1_fEY' + fkq7lhLkdH: 'jjhGh!2yXE' + 'fxAN),FAc$': '6eRH0': 'r4vN]%uAKT' + 'gRVZ]qJ#>O': 'HRgL]mMe{v' + 'gYIoR`|AxW': 'FQ&H#hn98~V1': 'KpAD6q(C@6' + 'h9Jr(H[=1~': 'I]xI>B~d|i' + 'h?a*UKfT_M': 'JHYQ@rcDWA' + 'hE5j*e,Q?[': 'k_crZ6<6H@' + 'hNE9}>;Q&-': 'D`cK5{Bf(v' + 'hXNTVF<:SH': 'H@*7Gwq;c+' + 'hZ/V/D&rO`': 'o>gTQqW#WH' + 'h_9=mkZXAH': 'jKHmpl~?LE' + hoHandbYAy: 'g)E]': 'C8f6MF_v~q' + 'h}lnSN_f$_': 'FLi~`:rK*C' + 'h~5xz+=ke~': 's~>jc!QM+^' + 'i)]+09JUR&': 'MQ8`{2J;$l' + 'i2]v|D3@:j': 'E_he|O^6Oy' + 'iB-8Br~onq': Q75rz,Y9Lx + 'iIS#Nw]tr]': 'Pvc}L-U<4u' + 'iR;Um(L/~c=': 'dD4#OU[O?X' + 'i^J8&$(1]|': 'xy4vZ[O~^)' + 'ia8as9sCY|': OMmv.9i-m0 + 'igiK,ff[eO': 'CKTDS>^z.J' + 'ijP>aJBT!9': 'vlG>hjq6pi' + 'ilGtw#=asM': 'p@-D*C4Kbm' + 'j%*8gN)q>d': 'sv0@wkmk6L' + 'j)i)pB*HJ,': 'OQ1fL&%bu.' + 'j2IQ=f=w3@': 'K#d=_hIZKE' + j7eEZkzsCZ: cwo0tsYhFy + 'j9:K,~DQ2v': 'I}a6d8q1zJ' + 'jg|}': 'spE8=x-*=s' + 'jCd]`-=k,:': 'J.Jb{*5W&x' + 'jDA-V?/hVj': 'ilyPETFGW*D4)' + 'jcu!gLw/&r': 'tUSX;Ir|#b' + 'jrM28*HbyG': 'lh63b}7+i*' + 'j|z@PMgdx,': 'Ev>k]3}Q,)' + 'k$1_yaF?9#': 'mtvJwCsa{r' + 'k/u1:B%DJH': 'Dm&Q1+grS7' + 'k0{O[6l]bH': 'O--6u|WoWh' + 'k1]FH+s8j@': 'k}!h>.GGM}' + 'k9E.p^Ki!j#v+': 'nq~gisgjC/' + 'l3Ly8PBxt(': 'Kz0j{/>Yf;' + 'lAxH.CfJB@': 'L8k_N_5Y[s' + 'lH^2D]w;[H': 'D%X|~3..Zn' + 'lU@C37T|g>': 'puuJ#xat%v' + 'mz[>>AxIr4': 'nL!|kr$O/~' + 'm{noqE29%I': 'PS+ir>Fe)M' + 'm{oM,jr>C#': 'e%9U7YA-t6' + 'n&>e`2FO/S': 'lvc.@k4vE}' + 'n+t>2`:;P/': 'gbrfcUD:c-' + 'n:7&>YT5A=': 'QiSNJi)a#e' + 'nIfRnj' + 'nsS59c`VL,': 'mF&WTyq~08' + 'nuU8A_$i2M': 'e?vaE69=en' + 'n{{o2K1:dw': 'eyK$*+kkTP' + 'o#&RMA_REo': 'M2da>YCqC' + 'o3VS!KWb/@': 'fv#=q%4>c3' + 'o9{x/yz': 'kUnN|-Yeg2' + 'oa87&PB&Bm': 'zA3)&;T.So' + 'onMKID2d;D': 'EC?WTwipgP' + 'o~}KVA:VbI': 'o#V]PfbsJg' + 'p#R$-mG/L': 'uM1!Wa7;Z8' + 'p.yv>BQ470': 'I(kYtUqNp0' + 'p:SPZ:e/*[': 'x.-f`#aRav' + 'pDDrFhd%u': 'dg': 'F/1cJ>Fv&k' + 'q2#Yv[O0:|': 'm(Ep%@|]8b' + 'q7do-wLx%E': 'QJlzFH~z_P' + 'q8][)Ri}=q': 'ynaq{(em+.' + 'q8fF$4,c6_': zdjM-aCLo_ + 'q:XyGf#>Wg': 'IAa]E=O/{E' + 'q=w[fB$d+$': v6tcadB/em + 'r(h9&#wc~B': 'D!;%*R6J&o' + 'r/B|ra%L,.': 'EU8;wgRtD*' + 'r5C646H*+*': 'y0~OHJtwl7' + 'r6BV%|..*5': 'N/%+2}mGrG' + 'raI-$$|wT': 'JPoz};:Ba!' + 'r>g<|,FWi%': 'pW1W#4,|[C' + 'r?E$4f-d0/': 'rXXpo0h~v.' + 'rPekx@n9:L': 'yn:3FCAjQ@' + 'rSneU!3}bn': 'v^^9nD/1[^' + 'r]*Ioa%$bK': 'D': 'pT%UNItd:G' + 's9-GL*@AXD': 'D[xLjx,P&[' + 's<_WLSB3I/': 'p}s&{JwwI+' + 's>f40rn,O]': 'EwsGS_!k)u' + 'sB7rZQsR@9w~i^p': 'o1*G=hagzj' + 'skOM(?n$GG': FnkCCScYEu + spKoFRM4if: 'FlA8Y*/98r' + 'ssbyA9}]QP': 'vNk8~iA!~6' + 'stRa/S(0$F': 'Lr{~p.Jk<_' + 'sxOMs+bE8@': 'H/0e[]~?ap' + 'sx|l+io`B@': 'FTq&ip7kv.' + 's|K&2S,UP|': 'rrK]w=O%&;' + 't$y1C2R-Av': 's,r3*=P3-/' + 't+v9b--,qs': 'lzJ?#,5I62' + 't-0>F]bu>P': 'n0?y%]P19<' + 't2&ri|[Un:': 'HkZ${rRt2w' + t3aZXtpNyi: 'um^^I*6+xA' + 't50y/[AEhF': 'K$Iv_pVvA$' + 'tMMtBUpb$&': 'n-MjqM#r' + 't`cagaWApo': 'B;/=;Z5Hf>' + 'tcneuK7v%`': 'eYe;odSry.' + tfCY4j6KEZ: 'D9@;gV0`._' + 'tj&bK}%P!F': 'rlB,Mw_{9u' + 'twrhzJ[k:Z': 'pCEgO0#[W7' + 't|h3|;E4g{': 'rs3JWjn9}q' + 'u-`cspdp)D': 'le]C{9L5gP' + 'u4^lH.xem5': 'ikqk(b.J~O' + 'u;|vR#mVMW': 'my>[7JB9|d' + 'uH*Q(mcRt5': 'Q*lz)cq4AX' + 'uRo^w#p5Be': 'ypD(;&(hS}' + 'uc61ToT%gt': 'fi3!(|*ko:' + 'ui3>b-?a@m': 'f~6bdns@H]' + upiX0g1quR: 'J~N,woIJ(]' + 'uv1fGXHN@)': 'gA!f|vb<.t' + 'u|I}oElaH)': DBaFxxcqE1 + 'vE[T$`n9T)': 'y3XH$Vv!}O' + 'vKx<0Qa!aP': 'drOR)V?/DQ' + 'vNiD6A.v#s': 'o)}JIV-g[+' + 'va/ky=f5pK': 'C9Yd9s9x^4' + 'vf)G?[_?Pf': 'BJ.AT#.[rO' + 'vgdj#X?8dB': vKqtjIB/vI + 'vpOTK6tMr@': 'qL~S9=NS(@<' + 'wa>{k!2cXc': 'BuC717Wq+L' + x2d4n4ADcP: 'had@3^QMe<' + 'xb&P*pH(Q': 'w:QQuWvDwe' + 'xqrJ^d[.(O': 'kHR&Uv%`gZ' + 'yX:tmrd5;]': 'Fp7TupO=4T' + 'z:*d{z(~V2': 'Lw@Xd3XL=}' + 'zSc]-^U=0=': 'NlR(^Q+:^E' + crowdanki:uuid: + 43c5ba66-9a65-11e8-90c9-a0481cc15658: 6c995ee1-4b62-4019-a033-de0ef8651c83 + 43e2586a-9a65-11e8-a777-a0481cc15658: 30c7ff39-56b5-4693-8e16-aa862eb7a619 +deck: + name: + intent: replace + value: 'Ultimate Geography [ZH]' + expected_base: + value: Ultimate Geography + description: + intent: replace + value: | + 完整说明 | 发行说明 | 贡献指南 + + Ultimate Geography v5.3 特性: + + - 全世界 205 个主权国家 (820 张卡片) + - 59 个领土、世界区域和其他实体 (103 张卡片) + - 48 个海洋 (48 张卡片,仅地图) + - 7 个大陆 (7 张卡片,仅地图) + - 总计 319 个独立笔记, 978 张卡片, 221 个旗帜 以及 319 个地图. + + 本卡组 支持 英语(English), 德语(German), 西班牙语(Spanish), 法语(French), 挪威语(Norwegian), 捷克语(Czech), 俄语(Russian), 荷兰语(Dutch), 瑞典语(Swedish), 葡萄牙语(Portuguese), 简化字(Simplified Chinese/ZH), 正體字(Traditional Chinese/ZH-TW), 波兰语(Polish), 意大利语(Italian)丹麦语(Danish)。 同时 每种语言均支持拓展版本。 为了帮助记忆并在学习时提供上下文,一些笔记带有额外的信息,例如类似的旗帜、治理信息、替代国家名称等。 + + 你还能使用Anki的过滤牌组功能 , 将您的研究重点放在套牌的子集上, 例如主权州、单个笔记模板(例如地图→国家/地区)或特定大陆(例如欧洲)。 + + 这个卡组在 GitHub上维护。如果您发现错误、有建议或想要帮助,请不要犹豫打开一个问题(Issue)。 想要随时了解新版本? 观看(Watch)我们的 GitHub 仓库(Repository)或者订阅(Subscribe)发布提要(releases feed)! + expected_base: + value: | + FULL DESCRIPTION | RELEASE NOTES | CONTRIBUTING + + Ultimate Geography v5.3 features: + + - the world's 205 sovereign states (820 cards) + - 59 territories, world regions, and other entities (103 cards) + - 48 oceans and seas (48 cards, maps only) + - 7 continents (7 cards, maps only) + - for a total of 319 unique notes, 978 cards, 221 flags and 319 maps. + + The deck is available in English, German, Spanish, French, Norwegian, Czech, Russian, Dutch, Swedish, Portuguese, Chinese (simplified and traditional), Polish, Italian and Danish. An extended version is also available in each language. To help with memorisation and provide context while learning, some notes include extra information such as similar flags, governance information, alternative country names, etc. + + You can use Anki's filtered deck feature to focus your study on a subset of the deck, such as sovereign states, a single note template (e.g. map to country), or a specific continent (e.g. Europe). + + This deck is maintained on GitHub. If you spot a mistake, have a suggestion or want to help, please don't hesitate to open an issue. Want to stay informed of new releases? Watch the GitHub repository or subscribe to the releases feed! diff --git a/fixtures/ultimate-geography/overlays/variants/experimental.yaml b/fixtures/ultimate-geography/overlays/variants/experimental.yaml new file mode 100644 index 0000000..fd78b2d --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental.yaml @@ -0,0 +1,745 @@ +id: overlay.variant.experimental +kind: extension +field_additions: + note-type.ultimate-geography: + fields: + field.region-code: Region code + values: + note.abkhazia: + field.region-code: '' + note.adriatic-sea: + field.region-code: '' + note.aegean-sea: + field.region-code: '' + note.afghanistan: + field.region-code: AF + note.africa: + field.region-code: '' + note.akrotiri-and-dhekelia: + field.region-code: '' + note.alaska: + field.region-code: '' + note.albania: + field.region-code: AL + note.algeria: + field.region-code: DZ + note.american-samoa: + field.region-code: '' + note.andorra: + field.region-code: AD + note.angola: + field.region-code: AO + note.anguilla: + field.region-code: '' + note.antarctica: + field.region-code: '' + note.antigua-and-barbuda: + field.region-code: AG + note.arabian-sea: + field.region-code: '' + note.aral-sea: + field.region-code: '' + note.arctic-ocean: + field.region-code: '' + note.argentina: + field.region-code: AR + note.armenia: + field.region-code: AM + note.aruba: + field.region-code: '' + note.asia: + field.region-code: '' + note.atlantic-ocean: + field.region-code: '' + note.australia: + field.region-code: AU + note.austria: + field.region-code: AT + note.azerbaijan: + field.region-code: AZ + note.azores: + field.region-code: '' + note.bahrain: + field.region-code: BH + note.bali: + field.region-code: '' + note.balkan-peninsula: + field.region-code: '' + note.baltic-sea: + field.region-code: '' + note.banda-sea: + field.region-code: '' + note.bangladesh: + field.region-code: BD + note.barbados: + field.region-code: BB + note.barents-sea: + field.region-code: '' + note.bay-of-bengal: + field.region-code: '' + note.bay-of-biscay: + field.region-code: '' + note.belarus: + field.region-code: BY + note.belgium: + field.region-code: BE + note.belize: + field.region-code: BZ + note.benin: + field.region-code: BJ + note.bering-strait: + field.region-code: '' + note.bermuda: + field.region-code: '' + note.bhutan: + field.region-code: BT + note.black-sea: + field.region-code: '' + note.bolivia: + field.region-code: BO + note.bosnia-and-herzegovina: + field.region-code: BA + note.botswana: + field.region-code: BW + note.bougainville: + field.region-code: '' + note.brazil: + field.region-code: BR + note.british-virgin-islands: + field.region-code: '' + note.brunei: + field.region-code: BN + note.bulgaria: + field.region-code: BG + note.burkina-faso: + field.region-code: BF + note.burundi: + field.region-code: BI + note.cambodia: + field.region-code: KH + note.cameroon: + field.region-code: CM + note.canada: + field.region-code: CA + note.canary-islands: + field.region-code: '' + note.cape-verde: + field.region-code: CV + note.caribbean-sea: + field.region-code: '' + note.caspian-sea: + field.region-code: '' + note.cayman-islands: + field.region-code: '' + note.celebes-sea: + field.region-code: '' + note.celtic-sea: + field.region-code: '' + note.central-african-republic: + field.region-code: CF + note.chad: + field.region-code: TD + note.chile: + field.region-code: CL + note.china: + field.region-code: CN + note.colombia: + field.region-code: CO + note.comoros: + field.region-code: KM + note.cook-islands: + field.region-code: '' + note.coral-sea: + field.region-code: '' + note.corsica: + field.region-code: '' + note.costa-rica: + field.region-code: CR + note.croatia: + field.region-code: HR + note.cuba: + field.region-code: CU + note.cura-ao: + field.region-code: '' + note.cyprus: + field.region-code: CY + note.czech-republic: + field.region-code: CZ + note.dead-sea: + field.region-code: '' + note.democratic-republic-of-the-congo: + field.region-code: CD + note.denmark: + field.region-code: DK + note.denmark-strait: + field.region-code: '' + note.djibouti: + field.region-code: DJ + note.dominica: + field.region-code: DM + note.dominican-republic: + field.region-code: DO + note.east-china-sea: + field.region-code: '' + note.east-siberian-sea: + field.region-code: '' + note.ecuador: + field.region-code: EC + note.egypt: + field.region-code: EG + note.el-salvador: + field.region-code: SV + note.england: + field.region-code: '' + note.english-channel: + field.region-code: '' + note.equatorial-guinea: + field.region-code: GQ + note.eritrea: + field.region-code: ER + note.estonia: + field.region-code: EE + note.eswatini: + field.region-code: SZ + note.ethiopia: + field.region-code: ET + note.europe: + field.region-code: '' + note.european-union: + field.region-code: '' + note.falkland-islands: + field.region-code: FK + note.faroe-islands: + field.region-code: FO + note.federated-states-of-micronesia: + field.region-code: '' + note.fiji: + field.region-code: FJ + note.finland: + field.region-code: FI + note.france: + field.region-code: FR + note.french-guiana: + field.region-code: GF + note.french-polynesia: + field.region-code: '' + note.gabon: + field.region-code: GA + note.georgia: + field.region-code: GE + note.germany: + field.region-code: DE + note.ghana: + field.region-code: GH + note.gibraltar: + field.region-code: '' + note.greece: + field.region-code: GR + note.greenland: + field.region-code: GL + note.grenada: + field.region-code: '' + note.guadeloupe: + field.region-code: GP + note.guam: + field.region-code: '' + note.guatemala: + field.region-code: GT + note.guernsey: + field.region-code: '' + note.guinea: + field.region-code: GN + note.guinea-bissau: + field.region-code: GW + note.gulf-of-alaska: + field.region-code: '' + note.gulf-of-california: + field.region-code: '' + note.gulf-of-carpentaria: + field.region-code: '' + note.gulf-of-guinea: + field.region-code: '' + note.gulf-of-mexico: + field.region-code: '' + note.gulf-of-thailand: + field.region-code: '' + note.guyana: + field.region-code: GY + note.haiti: + field.region-code: HT + note.hawaii: + field.region-code: '' + note.honduras: + field.region-code: HN + note.hong-kong: + field.region-code: HK + note.hudson-bay: + field.region-code: '' + note.hungary: + field.region-code: HU + note.iceland: + field.region-code: IS + note.india: + field.region-code: IN + note.indian-ocean: + field.region-code: '' + note.indonesia: + field.region-code: ID + note.iran: + field.region-code: IR + note.iraq: + field.region-code: IQ + note.ireland: + field.region-code: IE + note.isle-of-man: + field.region-code: IM + note.israel: + field.region-code: IL + note.italy: + field.region-code: IT + note.ivory-coast: + field.region-code: CI + note.jamaica: + field.region-code: JM + note.japan: + field.region-code: JP + note.java: + field.region-code: '' + note.jeju: + field.region-code: '' + note.jersey: + field.region-code: '' + note.jordan: + field.region-code: JO + note.kaliningrad-oblast: + field.region-code: '' + note.kazakhstan: + field.region-code: KZ + note.kenya: + field.region-code: KE + note.kiribati: + field.region-code: '' + note.kosovo: + field.region-code: '' + note.kuwait: + field.region-code: KW + note.kyrgyzstan: + field.region-code: KG + note.labrador-sea: + field.region-code: '' + note.land-islands: + field.region-code: AX + note.laos: + field.region-code: LA + note.latvia: + field.region-code: LV + note.lebanon: + field.region-code: LB + note.lesotho: + field.region-code: LS + note.liberia: + field.region-code: LR + note.libya: + field.region-code: LY + note.liechtenstein: + field.region-code: '' + note.lithuania: + field.region-code: LT + note.luxembourg: + field.region-code: LU + note.macau: + field.region-code: '' + note.madagascar: + field.region-code: MG + note.madeira: + field.region-code: '' + note.malawi: + field.region-code: MW + note.malaysia: + field.region-code: MY + note.maldives: + field.region-code: '' + note.mali: + field.region-code: ML + note.malta: + field.region-code: '' + note.marshall-islands: + field.region-code: '' + note.martinique: + field.region-code: MQ + note.mauritania: + field.region-code: MR + note.mauritius: + field.region-code: MU + note.mayotte: + field.region-code: YT + note.mediterranean-sea: + field.region-code: '' + note.melanesia: + field.region-code: '' + note.mexico: + field.region-code: MX + note.micronesia: + field.region-code: '' + note.moldova: + field.region-code: MD + note.monaco: + field.region-code: '' + note.mongolia: + field.region-code: MN + note.montenegro: + field.region-code: ME + note.morocco: + field.region-code: MA + note.mozambique: + field.region-code: MZ + note.myanmar: + field.region-code: MM + note.namibia: + field.region-code: NA + note.nauru: + field.region-code: '' + note.nepal: + field.region-code: NP + note.netherlands: + field.region-code: NL + note.new-caledonia: + field.region-code: NC + note.new-zealand: + field.region-code: NZ + note.nicaragua: + field.region-code: NI + note.niger: + field.region-code: NE + note.nigeria: + field.region-code: NG + note.niue: + field.region-code: '' + note.north-america: + field.region-code: '' + note.north-korea: + field.region-code: KP + note.north-macedonia: + field.region-code: MK + note.north-sea: + field.region-code: '' + note.northern-cyprus: + field.region-code: '' + note.northern-ireland: + field.region-code: '' + note.northern-mariana-islands: + field.region-code: '' + note.norway: + field.region-code: NO + note.norwegian-sea: + field.region-code: '' + note.oceania: + field.region-code: '' + note.oman: + field.region-code: OM + note.pacific-ocean: + field.region-code: '' + note.pakistan: + field.region-code: PK + note.palau: + field.region-code: '' + note.palestine: + field.region-code: PS + note.panama: + field.region-code: PA + note.papua-new-guinea: + field.region-code: PG + note.paraguay: + field.region-code: PY + note.persian-gulf: + field.region-code: '' + note.peru: + field.region-code: PE + note.philippine-sea: + field.region-code: '' + note.philippines: + field.region-code: PH + note.poland: + field.region-code: PL + note.polynesia: + field.region-code: '' + note.portugal: + field.region-code: PT + note.puerto-rico: + field.region-code: PR + note.qatar: + field.region-code: QA + note.r-union: + field.region-code: RE + note.red-sea: + field.region-code: '' + note.republic-of-the-congo: + field.region-code: CG + note.romania: + field.region-code: RO + note.russia: + field.region-code: RU + note.rwanda: + field.region-code: RW + note.s-o-tom-and-pr-ncipe: + field.region-code: ST + note.sahrawi-arab-democratic-republic: + field.region-code: '' + note.saint-kitts-and-nevis: + field.region-code: '' + note.saint-lucia: + field.region-code: '' + note.saint-martin: + field.region-code: '' + note.saint-vincent-and-the-grenadines: + field.region-code: '' + note.samoa: + field.region-code: '' + note.san-marino: + field.region-code: '' + note.sardinia: + field.region-code: '' + note.saudi-arabia: + field.region-code: SA + note.scandinavia: + field.region-code: '' + note.scotland: + field.region-code: '' + note.sea-of-galilee: + field.region-code: '' + note.sea-of-japan: + field.region-code: '' + note.sea-of-okhotsk: + field.region-code: '' + note.senegal: + field.region-code: SN + note.serbia: + field.region-code: RS + note.seychelles: + field.region-code: '' + note.sicily: + field.region-code: '' + note.sierra-leone: + field.region-code: SL + note.singapore: + field.region-code: SG + note.sint-maarten: + field.region-code: '' + note.slovakia: + field.region-code: SK + note.slovenia: + field.region-code: SI + note.solomon-islands: + field.region-code: SB + note.somalia: + field.region-code: SO + note.somaliland: + field.region-code: '' + note.south-africa: + field.region-code: ZA + note.south-america: + field.region-code: '' + note.south-china-sea: + field.region-code: '' + note.south-korea: + field.region-code: KR + note.south-ossetia: + field.region-code: '' + note.south-sudan: + field.region-code: SS + note.southern-ocean: + field.region-code: '' + note.spain: + field.region-code: ES + note.sri-lanka: + field.region-code: LK + note.sudan: + field.region-code: SD + note.sumatra: + field.region-code: '' + note.suriname: + field.region-code: SR + note.svalbard: + field.region-code: SJ + note.sweden: + field.region-code: SE + note.switzerland: + field.region-code: CH + note.syria: + field.region-code: SY + note.taiwan: + field.region-code: TW + note.tajikistan: + field.region-code: TJ + note.tanzania: + field.region-code: TZ + note.tasman-sea: + field.region-code: '' + note.thailand: + field.region-code: TH + note.the-bahamas: + field.region-code: BS + note.the-gambia: + field.region-code: GM + note.timor-leste: + field.region-code: TL + note.timor-sea: + field.region-code: '' + note.togo: + field.region-code: TG + note.tonga: + field.region-code: '' + note.transnistria: + field.region-code: '' + note.trinidad-and-tobago: + field.region-code: TT + note.tunisia: + field.region-code: TN + note.turkey: + field.region-code: TR + note.turkmenistan: + field.region-code: TM + note.turks-and-caicos-islands: + field.region-code: '' + note.tuvalu: + field.region-code: '' + note.uganda: + field.region-code: UG + note.ukraine: + field.region-code: UA + note.united-arab-emirates: + field.region-code: AE + note.united-kingdom: + field.region-code: GB + note.united-states-of-america: + field.region-code: US + note.united-states-virgin-islands: + field.region-code: '' + note.uruguay: + field.region-code: UY + note.uzbekistan: + field.region-code: UZ + note.vanuatu: + field.region-code: VU + note.vatican-city: + field.region-code: '' + note.venezuela: + field.region-code: VE + note.vietnam: + field.region-code: VN + note.wales: + field.region-code: '' + note.wallis-and-futuna: + field.region-code: '' + note.white-sea: + field.region-code: '' + note.yellow-sea: + field.region-code: '' + note.yemen: + field.region-code: YE + note.zambia: + field.region-code: ZM + note.zanzibar: + field.region-code: '' + note.zimbabwe: + field.region-code: ZW +note_types: + note-type.ultimate-geography: + intent: merge + variables: + variant.name-suffix: + intent: override + value: ' [Experimental]' + expected_base: + value: ' [Extended]' + card_templates: + template.country-map: + intent: merge + question_format: + intent: merge + value: |- + + +
{{Country}}
+
+ +
${label.location}
+ +
+ + + +
+ + + + + + + + answer_format: + intent: merge + value: | + +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.location}
+ +
{{Map}}
+ + + + + +media: + media.interactive-map-config-js: + intent: add + path: _ug-interactive_map_config.js + sha256: '' + media.interactive-map-init-js: + intent: add + path: _ug-interactive_map_init.js + sha256: '' + media.jsvectormap-css: + intent: add + path: _ug-jsvectormap.min.css + sha256: '' + media.jsvectormap-js: + intent: add + path: _ug-jsvectormap.js + sha256: '' + media.world-js: + intent: add + path: _ug-world.js + sha256: '' diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/cs.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/cs.yaml new file mode 100644 index 0000000..52dc35d --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/cs.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.cs +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: d7743e0d-0979-493a-83fa-67172ea63c07 + expected_base: + value: 47d13c81-b471-4471-8e87-ebad53fa7307 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/da.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/da.yaml new file mode 100644 index 0000000..58eaf8d --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/da.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.da +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 85d39aa7-cfd5-41aa-8575-f39d3d5cd55b + expected_base: + value: 15daaec7-4097-4d97-ba94-9db44f8b8f51 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/de.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/de.yaml new file mode 100644 index 0000000..7b18423 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/de.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.de +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 98c67202-2209-48c1-b3de-475fa58372af + expected_base: + value: cb3de16f-7c9c-4944-994a-e17ec2018c28 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/en.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/en.yaml new file mode 100644 index 0000000..2822719 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/en.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.en +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 2e98b530-7583-5551-1fd7-51e6969d58ed + expected_base: + value: 43e2586a-9a65-11e8-a777-a0481cc15658 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/es.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/es.yaml new file mode 100644 index 0000000..2f35ef2 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/es.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.es +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: b7610e2c-c28f-402a-b86a-57a44fdf11f7 + expected_base: + value: 0bacddfd-1e81-4e62-9152-bdff33db0374 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/fr.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/fr.yaml new file mode 100644 index 0000000..30c389b --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/fr.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.fr +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 15113ac3-6e6b-4ad3-9dca-e963ce4d0cf4 + expected_base: + value: fd72d808-58d4-43ea-97db-24196747f24c diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/it.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/it.yaml new file mode 100644 index 0000000..744c831 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/it.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.it +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: cf4488cc-2747-4b48-86a5-fa86ef99ab53 + expected_base: + value: cd65a274-7341-4e72-8de5-5d3d83ea4537 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/nb.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/nb.yaml new file mode 100644 index 0000000..3733642 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/nb.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.nb +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: f5b475fc-3c81-4644-b17f-500d1cf755d9 + expected_base: + value: 8ccdc1f6-b042-4d55-8fcd-9e08b5b71dc7 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/nl.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/nl.yaml new file mode 100644 index 0000000..111f5ca --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/nl.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.nl +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 6bce5589-39ce-496d-86e2-1148d9b19405 + expected_base: + value: 8263244e-6f5e-457a-bb65-a836a6c058a3 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/pl.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/pl.yaml new file mode 100644 index 0000000..1306f8c --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/pl.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.pl +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 7ce20479-556c-4ef2-a1d4-9573a00a70fc + expected_base: + value: 84786477-e28a-490e-958d-7b35946d7b11 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/pt.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/pt.yaml new file mode 100644 index 0000000..ca392c7 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/pt.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.pt +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 6a7fe240-f6fb-4571-9f2a-fd30a9cf7709 + expected_base: + value: 3da2a851-258c-447f-9d16-91a663663675 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/ru.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/ru.yaml new file mode 100644 index 0000000..7947706 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/ru.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.ru +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 1511e9fe-58e6-4d9a-89b2-1423b87c44b8 + expected_base: + value: 2aa62e36-601e-4e4c-a124-5a79b14f8697 diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/sv.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/sv.yaml new file mode 100644 index 0000000..1e7d502 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/sv.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.sv +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: ce42a853-79d7-4b8f-85cc-61963effdb9c + expected_base: + value: 3090ce89-5fd8-4107-86df-3e7a74ec288f diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/zh-tw.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/zh-tw.yaml new file mode 100644 index 0000000..fa27b08 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/zh-tw.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.zh-tw +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 4697c375-ef04-467a-8838-edecadd201d1 + expected_base: + value: d75ad494-71f3-4909-beda-b80ec04c7a0d diff --git a/fixtures/ultimate-geography/overlays/variants/experimental/zh.yaml b/fixtures/ultimate-geography/overlays/variants/experimental/zh.yaml new file mode 100644 index 0000000..58fc3c6 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/experimental/zh.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.experimental.zh +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 10ca3bef-1f66-445c-adf2-fd0f55bf0762 + expected_base: + value: 30c7ff39-56b5-4693-8e16-aa862eb7a619 diff --git a/fixtures/ultimate-geography/overlays/variants/extended.yaml b/fixtures/ultimate-geography/overlays/variants/extended.yaml new file mode 100644 index 0000000..62f75d4 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended.yaml @@ -0,0 +1,80 @@ +id: overlay.variant.extended +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + variables: + variant.name-suffix: + intent: replace + value: ' [Extended]' + expected_base: + value: '' + card_templates: + template.country-flag: + intent: add + insert_after: template.capital-country + template: + name: Country - Flag + question_format: |- + {{#Flag}} +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.flag}
+
+ + + +
+ {{/Flag}} + answer_format: | +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.flag}
+
{{Flag}}
+ {{#Flag similarity}}
${sentence.flag-similar}
{{/Flag similarity}} + adapter_ids: {} + template.country-map: + intent: add + insert_after: template.flag-country + template: + name: Country - Map + question_format: |- + {{#Map}} +
{{Country}}
+ +
+ +
${label.location}
+
+ + + +
+ {{/Map}} + answer_format: | +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.location}
+
{{Map}}
+ adapter_ids: {} diff --git a/fixtures/ultimate-geography/overlays/variants/extended/cs.yaml b/fixtures/ultimate-geography/overlays/variants/extended/cs.yaml new file mode 100644 index 0000000..d75eebb --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/cs.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.cs +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 5ef3528d-e7c3-4d49-81e8-6c1c0a609a77 + expected_base: + value: 47d13c81-b471-4471-8e87-ebad53fa7307 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/da.yaml b/fixtures/ultimate-geography/overlays/variants/extended/da.yaml new file mode 100644 index 0000000..3037b1e --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/da.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.da +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 412504c7-cde6-426d-8110-454bb2b711c7 + expected_base: + value: 15daaec7-4097-4d97-ba94-9db44f8b8f51 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/de.yaml b/fixtures/ultimate-geography/overlays/variants/extended/de.yaml new file mode 100644 index 0000000..f2d4fde --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/de.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.de +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: dc6c1ca9-b55f-429e-99dc-c61c35a20209 + expected_base: + value: cb3de16f-7c9c-4944-994a-e17ec2018c28 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/en.yaml b/fixtures/ultimate-geography/overlays/variants/extended/en.yaml new file mode 100644 index 0000000..77f3d4e --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/en.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.en +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 0a39f994-daaf-11e8-b984-a0481cc15658 + expected_base: + value: 43e2586a-9a65-11e8-a777-a0481cc15658 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/es.yaml b/fixtures/ultimate-geography/overlays/variants/extended/es.yaml new file mode 100644 index 0000000..de2d0f7 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/es.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.es +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 03766b2c-0925-41d0-9a14-9bca36a3b605 + expected_base: + value: 0bacddfd-1e81-4e62-9152-bdff33db0374 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/fr.yaml b/fixtures/ultimate-geography/overlays/variants/extended/fr.yaml new file mode 100644 index 0000000..4a70217 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/fr.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.fr +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: d8f4db0e-310f-45d4-9143-a9c93dd658bb + expected_base: + value: fd72d808-58d4-43ea-97db-24196747f24c diff --git a/fixtures/ultimate-geography/overlays/variants/extended/it.yaml b/fixtures/ultimate-geography/overlays/variants/extended/it.yaml new file mode 100644 index 0000000..a8c5f53 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/it.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.it +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 996b5985-fb94-479f-a468-67284891fa5d + expected_base: + value: cd65a274-7341-4e72-8de5-5d3d83ea4537 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/nb.yaml b/fixtures/ultimate-geography/overlays/variants/extended/nb.yaml new file mode 100644 index 0000000..f35494d --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/nb.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.nb +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: a5caadfb-a5b4-41f2-a176-9e029e581368 + expected_base: + value: 8ccdc1f6-b042-4d55-8fcd-9e08b5b71dc7 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/nl.yaml b/fixtures/ultimate-geography/overlays/variants/extended/nl.yaml new file mode 100644 index 0000000..ad2d0df --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/nl.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.nl +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 666fd740-3670-4a45-98e5-dff3a7568ad5 + expected_base: + value: 8263244e-6f5e-457a-bb65-a836a6c058a3 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/pl.yaml b/fixtures/ultimate-geography/overlays/variants/extended/pl.yaml new file mode 100644 index 0000000..5ee7b19 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/pl.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.pl +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: d0fa593e-cf19-4ac9-a7f3-005ed6196957 + expected_base: + value: 84786477-e28a-490e-958d-7b35946d7b11 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/pt.yaml b/fixtures/ultimate-geography/overlays/variants/extended/pt.yaml new file mode 100644 index 0000000..d71d48a --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/pt.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.pt +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 9028894e-3885-495f-a608-1e4e8e2e70df + expected_base: + value: 3da2a851-258c-447f-9d16-91a663663675 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/ru.yaml b/fixtures/ultimate-geography/overlays/variants/extended/ru.yaml new file mode 100644 index 0000000..ec1e4c0 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/ru.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.ru +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 03edf31b-c8ef-4fdd-855a-25846f1e1c13 + expected_base: + value: 2aa62e36-601e-4e4c-a124-5a79b14f8697 diff --git a/fixtures/ultimate-geography/overlays/variants/extended/sv.yaml b/fixtures/ultimate-geography/overlays/variants/extended/sv.yaml new file mode 100644 index 0000000..3cbd312 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/sv.yaml @@ -0,0 +1,60 @@ +id: overlay.variant.extended.sv +kind: extension +deck: + name: + intent: override + value: Ultimate Geography + expected_base: + value: 'Ultimate Geography [SV]' + description: + intent: override + value: | + FULL DESCRIPTION | RELEASE NOTES | CONTRIBUTING + + Ultimate Geography v5.3 features: + + - the world's 205 sovereign states (820 cards) + - 59 territories, world regions, and other entities (103 cards) + - 48 oceans and seas (48 cards, maps only) + - 7 continents (7 cards, maps only) + - for a total of 319 unique notes, 978 cards, 221 flags and 319 maps. + + The deck is available in English, German, Spanish, French, Norwegian, Czech, Russian, Dutch, Swedish, Portuguese, Chinese (simplified and traditional), Polish, Italian and Danish. An extended version is also available in each language. To help with memorisation and provide context while learning, some notes include extra information such as similar flags, governance information, alternative country names, etc. + + You can use Anki's filtered deck feature to focus your study on a subset of the deck, such as sovereign states, a single note template (e.g. map to country), or a specific continent (e.g. Europe). + + This deck is maintained on GitHub. If you spot a mistake, have a suggestion or want to help, please don't hesitate to open an issue. Want to stay informed of new releases? Watch the GitHub repository or subscribe to the releases feed! + expected_base: + value: | + FULLSTÄNDIG BESKRIVNING | UTGIVNINGSANMÄRKNINGAR | BIDRA + + Ultimate Geography v5.3 innehåller: + + - världens 205 självständiga stater (820 kort) + - 59 territorum, världsregioner och andra enheter (103 kort) + - 48 sjöar och hav (48 kort, enbart kartor) + - 7 kontinenter (7 kort, enbart kartor) + - sammanlagt 319 unika noter, 978 kort, 221 flaggor och 319 kartor. + + Kortleken finns tillgänglig på fjorton olika språk: engelska, tyska, spanska, franska, norska, tjeckiska, ryska, nederländska, svenska, portugisiska, kinesiska, polska, italienska och danska. För vardera språk finns dessutom en utökad version. + + För att underlätta inlärningen så har vissa kort en extra informationsrad, med exempelvis information om att flaggan på kortet liknar en annan flagga, politisk styrelse eller ett alternativt namn på ett land. + + Du kan skapa en filtrerad kortlek om du vill fokusera på specifika delar av kortleken. Till exempel kan du på så sätt välja att enbart studera suveräna stater, endast en specifik korttyp (såsom "karta → land") eller en specifik världsdel (till exempel Europa). + + Den här kortleken administreras på GitHub. Om du upptäcker ett fel, har förslag eller vill hjälpa till, tveka inte att öppna ett ärende. Vill du bli informerad om uppdateringar av kortleken? Bevaka källlkodskatalogen på GitHub eller prenumerera på feeden för utgåvor! + adapter_ids: + crowdanki:uuid: + intent: override + value: 43c5ba66-9a65-11e8-90c9-a0481cc15658 + expected_base: + value: 75bfcdb5-0ff3-4038-83cb-3e6ed974f439 +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: a8103c3e-f0a3-4943-98fc-492159b0944e + expected_base: + value: 3090ce89-5fd8-4107-86df-3e7a74ec288f diff --git a/fixtures/ultimate-geography/overlays/variants/extended/zh-tw.yaml b/fixtures/ultimate-geography/overlays/variants/extended/zh-tw.yaml new file mode 100644 index 0000000..a92271d --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/zh-tw.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.zh-tw +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: 4f11a863-ba7e-4c78-8426-9cd0f64a7b3a + expected_base: + value: d75ad494-71f3-4909-beda-b80ec04c7a0d diff --git a/fixtures/ultimate-geography/overlays/variants/extended/zh.yaml b/fixtures/ultimate-geography/overlays/variants/extended/zh.yaml new file mode 100644 index 0000000..f77dc53 --- /dev/null +++ b/fixtures/ultimate-geography/overlays/variants/extended/zh.yaml @@ -0,0 +1,11 @@ +id: overlay.variant.extended.zh +kind: extension +note_types: + note-type.ultimate-geography: + intent: merge + adapter_ids: + crowdanki:uuid: + intent: override + value: d1c5ee7f-4a7a-4cff-939f-a06f865d68d2 + expected_base: + value: 30c7ff39-56b5-4693-8e16-aa862eb7a619 diff --git a/scripts/fetch_ug_release_oracle.py b/scripts/fetch_ug_release_oracle.py new file mode 100755 index 0000000..c3a72b3 --- /dev/null +++ b/scripts/fetch_ug_release_oracle.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Fetch Ultimate Geography release CrowdAnki deck.json files as a parity oracle. + +This is intentionally a small, project-specific helper for the UG migration proof. +It is not a general Brain Brew package downloader. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import sys +import urllib.request +import zipfile +from pathlib import Path +from tempfile import TemporaryDirectory + +REPO = "anki-geo/ultimate-geography" +DEFAULT_TAG = "v5.3" +LANGUAGES = [ + "cs", + "da", + "de", + "en", + "es", + "fr", + "it", + "nb", + "nl", + "pl", + "pt", + "ru", + "sv", + "zh", + "zh-tw", +] +VARIANTS = ["standard", "extended"] + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--tag", default=DEFAULT_TAG, help="UG release tag to fetch") + parser.add_argument( + "--out", + type=Path, + help="Output directory (default: .cache/brainbrew/ug-release-oracle/)", + ) + parser.add_argument( + "--target", + action="append", + help="Target to fetch, e.g. en-standard. Repeatable. Defaults to all release targets.", + ) + parser.add_argument( + "--force", + action="store_true", + help="Download even when the target deck.json already exists.", + ) + args = parser.parse_args() + + out = args.out or Path(".cache/brainbrew/ug-release-oracle") / args.tag + targets = args.target or all_targets() + validate_targets(targets) + + records = {} + crowdanki_root = out / "crowdanki" + crowdanki_root.mkdir(parents=True, exist_ok=True) + + with TemporaryDirectory(prefix="brainbrew-ug-oracle-") as temp: + temp_dir = Path(temp) + for target in targets: + spec = target_spec(args.tag, target) + deck_json_path = crowdanki_root / spec["deck_folder"] / "deck.json" + if deck_json_path.exists() and not args.force: + deck_bytes = deck_json_path.read_bytes() + records[target] = { + **spec, + "asset_sha256": None, + "deck_json_sha256": sha256_bytes(deck_bytes), + "deck_json": str(deck_json_path.relative_to(out)), + "downloaded": False, + } + print(f"already present {target}: {deck_json_path}") + continue + + zip_path = temp_dir / spec["asset"] + print(f"downloading {target}: {spec['url']}") + urllib.request.urlretrieve(spec["url"], zip_path) + asset_bytes = zip_path.read_bytes() + asset_sha256 = sha256_bytes(asset_bytes) + + with zipfile.ZipFile(zip_path) as archive: + member = f"{spec['deck_folder']}/deck.json" + try: + deck_bytes = archive.read(member) + except KeyError as error: + raise SystemExit(f"{spec['asset']} does not contain {member}") from error + + deck_json_path.parent.mkdir(parents=True, exist_ok=True) + deck_json_path.write_bytes(deck_bytes) + records[target] = { + **spec, + "asset_sha256": asset_sha256, + "deck_json_sha256": sha256_bytes(deck_bytes), + "deck_json": str(deck_json_path.relative_to(out)), + "downloaded": True, + } + print(f"extracted {target}: {deck_json_path}") + + manifest = { + "repo": REPO, + "tag": args.tag, + "targets": records, + } + manifest_path = out / "oracle-manifest.json" + manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n") + print(f"wrote {manifest_path}") + return 0 + + +def all_targets() -> list[str]: + return [f"{language}-{variant}" for language in LANGUAGES for variant in VARIANTS] + + +def validate_targets(targets: list[str]) -> None: + valid = set(all_targets()) + invalid = sorted(set(targets) - valid) + if invalid: + raise SystemExit(f"unknown target(s): {', '.join(invalid)}") + + +def target_spec(tag: str, target: str) -> dict[str, str]: + language, variant = target_language_and_variant(target) + release_code = language.upper() + variant_suffix = "_EXTENDED" if variant == "extended" else "" + folder_suffix = " [Extended]" if variant == "extended" else "" + asset = f"Ultimate_Geography_{tag}_{release_code}{variant_suffix}.zip" + deck_folder = f"Ultimate Geography [{release_code}]{folder_suffix}" + return { + "asset": asset, + "url": f"https://github.com/{REPO}/releases/download/{tag}/{asset}", + "deck_folder": deck_folder, + } + + +def target_language_and_variant(target: str) -> tuple[str, str]: + if target.startswith("zh-tw-"): + return "zh-tw", target.removeprefix("zh-tw-") + language, variant = target.split("-", 1) + return language, variant + + +def sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +if __name__ == "__main__": + sys.exit(main()) From 25ebf007f90b4cb486502e31bde34960d68497ef Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 25 May 2026 09:51:55 +0200 Subject: [PATCH 004/142] docs: add Brain Brew federation guidance --- .gitignore | 3 + AGENTS.md | 46 + CONTEXT.md | 239 + README.md | 279 +- documentation/README.md | 18 + documentation/docs/authoring/diff-explain.md | 58 + documentation/docs/authoring/extensions.md | 81 + documentation/docs/authoring/field-fills.md | 63 + .../docs/authoring/manifests-targets.md | 95 + .../docs/authoring/packages-locking.md | 84 + documentation/docs/authoring/translations.md | 98 + documentation/docs/authoring/verify-export.md | 87 + documentation/docs/authoring/workspace.md | 65 + documentation/docs/concepts/canonical-deck.md | 72 + documentation/docs/concepts/identity.md | 62 + documentation/docs/concepts/media.md | 59 + documentation/docs/concepts/overlays.md | 101 + .../docs/concepts/what-is-federation.md | 52 + .../docs/examples/downstream-package.md | 66 + .../docs/examples/hardcore-geography.md | 68 + .../docs/examples/ultimate-geography.md | 76 + .../docs/getting-started/cli-tour.md | 79 + documentation/docs/getting-started/install.md | 70 + .../docs/getting-started/quickstart.md | 96 + documentation/docs/intro.md | 39 + documentation/docs/reference/cli.md | 100 + ...ow-scope-to-local-first-deck-federation.md | 23 + ...use-canonical-deck-as-federation-format.md | 23 + ...0013-use-stable-ids-as-primary-identity.md | 23 + ...llow-overlays-to-target-any-deck-entity.md | 23 + ...d-overlay-stack-with-explicit-conflicts.md | 23 + ...lays-as-sparse-canonical-deck-fragments.md | 23 + ...stable-canonicalized-source-round-trips.md | 23 + ...-canonical-deck-file-as-source-of-truth.md | 23 + ...canonical-yaml-for-canonical-deck-files.md | 23 + .../0020-use-rust-for-core-and-cli.md | 23 + ...ure-rust-workspace-around-reusable-core.md | 23 + ...preserve-anki-compatible-deck-semantics.md | 23 + ...xclude-review-state-from-canonical-deck.md | 23 + ...edia-as-external-assets-with-references.md | 23 + .../0025-use-strict-validation-by-default.md | 23 + .../0026-fail-on-unsupported-adapter-data.md | 23 + ...ed-base-for-destructive-overlay-changes.md | 23 + ...pe-system-until-cli-semantics-stabilize.md | 23 + ...le-stable-ids-with-separate-adapter-ids.md | 23 + ...view-suggested-stable-ids-during-import.md | 23 + ...ey-canonical-deck-entities-by-stable-id.md | 23 + .../0032-represent-removals-as-tombstones.md | 23 + ...domain-pure-with-separate-format-codecs.md | 23 + ...034-use-minimal-federated-deck-manifest.md | 55 + ...utral-stable-ids-for-translated-targets.md | 45 + ...del-deck-variants-as-extension-overlays.md | 40 + ...source-importers-for-initial-federation.md | 42 + ...fest-package-metadata-and-target-checks.md | 66 + ...-variables-and-translation-dictionaries.md | 47 + ...040-use-locked-federated-package-inputs.md | 88 + .../0041-continue-as-rust-based-brain-brew.md | 46 + .../docs/reference/decisions/README.md | 64 + .../0001-use-gren-as-primary-language.md | 41 + ...anonical-note-as-single-source-of-truth.md | 39 + ...3-recipe-system-based-on-nix-philosophy.md | 41 + ...ync-using-nucleus-inspired-architecture.md | 44 + ...ment-with-multiple-distribution-options.md | 45 + .../0006-adapter-interface-standardization.md | 49 + ...7-content-based-identification-for-sync.md | 48 + ...08-json-over-protobuf-for-data-exchange.md | 39 + ...cture-through-canonicalnote-composition.md | 166 + ...-level-sync-using-dropbox-nucleus-model.md | 211 + .../reference/decisions/archive/README.md | 5 + .../decisions/archive/_category_.json | 8 + documentation/docs/reference/glossary.md | 85 + documentation/docs/reference/lockfile.md | 79 + documentation/docs/reference/project-scope.md | 56 + .../docs/reference/release-oracle.md | 45 + .../research/iced-and-anki-add-on.md | 101 + documentation/docs/reference/yaml.md | 137 + documentation/docusaurus.config.js | 85 + documentation/package-lock.json | 18383 ++++++++++++++++ documentation/package.json | 21 + documentation/sidebars.js | 73 + documentation/src/css/custom.css | 23 + skills/federated-deck-extensions/SKILL.md | 221 + 82 files changed, 23002 insertions(+), 221 deletions(-) create mode 100644 AGENTS.md create mode 100644 CONTEXT.md create mode 100644 documentation/README.md create mode 100644 documentation/docs/authoring/diff-explain.md create mode 100644 documentation/docs/authoring/extensions.md create mode 100644 documentation/docs/authoring/field-fills.md create mode 100644 documentation/docs/authoring/manifests-targets.md create mode 100644 documentation/docs/authoring/packages-locking.md create mode 100644 documentation/docs/authoring/translations.md create mode 100644 documentation/docs/authoring/verify-export.md create mode 100644 documentation/docs/authoring/workspace.md create mode 100644 documentation/docs/concepts/canonical-deck.md create mode 100644 documentation/docs/concepts/identity.md create mode 100644 documentation/docs/concepts/media.md create mode 100644 documentation/docs/concepts/overlays.md create mode 100644 documentation/docs/concepts/what-is-federation.md create mode 100644 documentation/docs/examples/downstream-package.md create mode 100644 documentation/docs/examples/hardcore-geography.md create mode 100644 documentation/docs/examples/ultimate-geography.md create mode 100644 documentation/docs/getting-started/cli-tour.md create mode 100644 documentation/docs/getting-started/install.md create mode 100644 documentation/docs/getting-started/quickstart.md create mode 100644 documentation/docs/intro.md create mode 100644 documentation/docs/reference/cli.md create mode 100644 documentation/docs/reference/decisions/0011-narrow-scope-to-local-first-deck-federation.md create mode 100644 documentation/docs/reference/decisions/0012-use-canonical-deck-as-federation-format.md create mode 100644 documentation/docs/reference/decisions/0013-use-stable-ids-as-primary-identity.md create mode 100644 documentation/docs/reference/decisions/0014-allow-overlays-to-target-any-deck-entity.md create mode 100644 documentation/docs/reference/decisions/0015-use-ordered-overlay-stack-with-explicit-conflicts.md create mode 100644 documentation/docs/reference/decisions/0016-represent-overlays-as-sparse-canonical-deck-fragments.md create mode 100644 documentation/docs/reference/decisions/0017-require-byte-stable-canonicalized-source-round-trips.md create mode 100644 documentation/docs/reference/decisions/0018-use-single-canonical-deck-file-as-source-of-truth.md create mode 100644 documentation/docs/reference/decisions/0019-use-strict-canonical-yaml-for-canonical-deck-files.md create mode 100644 documentation/docs/reference/decisions/0020-use-rust-for-core-and-cli.md create mode 100644 documentation/docs/reference/decisions/0021-structure-rust-workspace-around-reusable-core.md create mode 100644 documentation/docs/reference/decisions/0022-preserve-anki-compatible-deck-semantics.md create mode 100644 documentation/docs/reference/decisions/0023-exclude-review-state-from-canonical-deck.md create mode 100644 documentation/docs/reference/decisions/0024-store-media-as-external-assets-with-references.md create mode 100644 documentation/docs/reference/decisions/0025-use-strict-validation-by-default.md create mode 100644 documentation/docs/reference/decisions/0026-fail-on-unsupported-adapter-data.md create mode 100644 documentation/docs/reference/decisions/0027-require-expected-base-for-destructive-overlay-changes.md create mode 100644 documentation/docs/reference/decisions/0028-defer-recipe-system-until-cli-semantics-stabilize.md create mode 100644 documentation/docs/reference/decisions/0029-use-human-readable-stable-ids-with-separate-adapter-ids.md create mode 100644 documentation/docs/reference/decisions/0030-review-suggested-stable-ids-during-import.md create mode 100644 documentation/docs/reference/decisions/0031-key-canonical-deck-entities-by-stable-id.md create mode 100644 documentation/docs/reference/decisions/0032-represent-removals-as-tombstones.md create mode 100644 documentation/docs/reference/decisions/0033-keep-core-domain-pure-with-separate-format-codecs.md create mode 100644 documentation/docs/reference/decisions/0034-use-minimal-federated-deck-manifest.md create mode 100644 documentation/docs/reference/decisions/0035-use-language-neutral-stable-ids-for-translated-targets.md create mode 100644 documentation/docs/reference/decisions/0036-model-deck-variants-as-extension-overlays.md create mode 100644 documentation/docs/reference/decisions/0037-do-not-build-legacy-source-importers-for-initial-federation.md create mode 100644 documentation/docs/reference/decisions/0038-use-manifest-package-metadata-and-target-checks.md create mode 100644 documentation/docs/reference/decisions/0039-use-source-variables-and-translation-dictionaries.md create mode 100644 documentation/docs/reference/decisions/0040-use-locked-federated-package-inputs.md create mode 100644 documentation/docs/reference/decisions/0041-continue-as-rust-based-brain-brew.md create mode 100644 documentation/docs/reference/decisions/README.md create mode 100644 documentation/docs/reference/decisions/archive/0001-use-gren-as-primary-language.md create mode 100644 documentation/docs/reference/decisions/archive/0002-canonical-note-as-single-source-of-truth.md create mode 100644 documentation/docs/reference/decisions/archive/0003-recipe-system-based-on-nix-philosophy.md create mode 100644 documentation/docs/reference/decisions/archive/0004-bidirectional-sync-using-nucleus-inspired-architecture.md create mode 100644 documentation/docs/reference/decisions/archive/0005-web-first-deployment-with-multiple-distribution-options.md create mode 100644 documentation/docs/reference/decisions/archive/0006-adapter-interface-standardization.md create mode 100644 documentation/docs/reference/decisions/archive/0007-content-based-identification-for-sync.md create mode 100644 documentation/docs/reference/decisions/archive/0008-json-over-protobuf-for-data-exchange.md create mode 100644 documentation/docs/reference/decisions/archive/0009-federated-deck-architecture-through-canonicalnote-composition.md create mode 100644 documentation/docs/reference/decisions/archive/0010-note-level-sync-using-dropbox-nucleus-model.md create mode 100644 documentation/docs/reference/decisions/archive/README.md create mode 100644 documentation/docs/reference/decisions/archive/_category_.json create mode 100644 documentation/docs/reference/glossary.md create mode 100644 documentation/docs/reference/lockfile.md create mode 100644 documentation/docs/reference/project-scope.md create mode 100644 documentation/docs/reference/release-oracle.md create mode 100644 documentation/docs/reference/research/iced-and-anki-add-on.md create mode 100644 documentation/docs/reference/yaml.md create mode 100644 documentation/docusaurus.config.js create mode 100644 documentation/package-lock.json create mode 100644 documentation/package.json create mode 100644 documentation/sidebars.js create mode 100644 documentation/src/css/custom.css create mode 100644 skills/federated-deck-extensions/SKILL.md diff --git a/.gitignore b/.gitignore index 28dd34c..d3d8b63 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ # Local generated/downloaded caches /.cache/ + +# Local agent task queues +/.frontloop/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d224b00 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,46 @@ +# Agent Guidance + +## Project Aim + +Brain Brew is a Rust-based, local-first deck federation and round-trip engine for shared Anki-compatible decks. The first milestone is not a web app, SaaS, live sync tool, legacy Python recipe compatibility, or full Ultimate Geography clone. + +Read these before making design changes: + +- `CONTEXT.md` — domain glossary +- `documentation/docs/reference/project-scope.md` — current scope and architecture boundaries +- `documentation/docs/reference/decisions/README.md` — active ADR index + +Use the project skill `skills/federated-deck-extensions/SKILL.md` whenever creating, reviewing, or refactoring Federated Deck source, translation overlays, extension overlays, field fills, or UG-style variant targets. It captures the variable-first/shared-extension workflow and common mistakes to avoid. + +## Development Method + +Use TDD with Red-Green-Refactor: + +1. Add a failing test for the next behavior. +2. Implement the smallest change that passes. +3. Refactor with tests still green. + +Scaffolding can exist without tests, but domain behavior, format behavior, adapter behavior, and CLI behavior should enter through a failing test. + +## Crate Boundaries + +- `brain-brew-core`: pure domain only. No YAML, CrowdAnki, filesystem, terminal, or CLI dependencies. +- `brain-brew-formats`: reusable YAML/CrowdAnki codecs over core types. +- `brain-brew-cli`: thin command-line wrapper, filesystem access, prompts, and report rendering. + +## Commands + +Use Devbox: + +```bash +devbox run fmt +devbox run test +devbox run clippy +devbox run ci +``` + +Run `devbox run ci` before committing meaningful code changes. + +## Version Control + +This repo uses Jujutsu. Use `jj status`, `jj diff`, and `jj commit`; do not use direct `git` workflow commands. diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..7438441 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,239 @@ +# Brain Brew + +Brain Brew exists to help flashcard deck maintainers compose, evolve, and redistribute decks without losing the structure or history that makes those decks useful. + +## Language + +**Brain Brew**: +A local-first deck federation and round-trip system for flashcard decks. +_Avoid_: universal note sync service, SaaS sync platform + +**Deck**: +A shareable flashcard collection including notes, note types, card templates, styling, metadata, and media references. +_Avoid_: note list, CSV file, Anki export + +**Deck Maintainer**: +A person responsible for evolving and publishing a shared flashcard deck. +_Avoid_: learner, reviewer, end user + +**Learner**: +A person who studies with a shared deck and may have private changes to preserve. +_Avoid_: deck maintainer, publisher + +**Shared Deck**: +A flashcard deck intended to be installed, updated, or extended by people other than its maintainer. +_Avoid_: personal deck, private notes + +**Federated Deck**: +A composable source package for a shared deck contribution, containing a base deck, overlays, or both, intended to be composed with other Federated Decks. +_Avoid_: Anki subdeck, resolved deck, full deck copy + +**Canonical Deck**: +The format-independent representation of a deck's notes, note types, card templates, styling, metadata, and media references. +_Avoid_: canonical note list, CrowdAnki JSON + +**Canonical Deck File**: +The maintainer-owned source file containing a canonical deck. +_Avoid_: generated artifact, adapter export + +**Note**: +A deck entity containing field values and tags for one learnable fact or item. +_Avoid_: card, row + +**Note Type**: +A deck entity that defines the fields and card templates shared by a group of notes. +_Avoid_: template, card type + +**Card Template**: +A deck entity that defines how a note becomes a study card. +_Avoid_: note type, card + +**Card**: +A study item produced from a note through a card template. +_Avoid_: note, template + +**Review History**: +A learner's scheduling and study progress for cards in a spaced repetition system. +_Avoid_: deck content, note metadata + +**Media Asset**: +An external file used by deck content or presentation. +_Avoid_: embedded YAML data, field text + +**Media Reference**: +A deck entity that identifies and verifies a media asset. +_Avoid_: raw media file, HTML snippet + +**Deck Entity**: +An identifiable part of a deck, such as a note, note type, card template, media reference, or deck metadata item. +_Avoid_: file, row, JSON object + +**Stable ID**: +A human-readable identifier that says a deck entity is the same entity across source files, overlays, exports, and releases. +_Avoid_: content hash, row number, display name, adapter GUID + +**Adapter ID**: +An identifier used by an external deck format or tool for the same deck entity. +_Avoid_: stable ID, content hash + +**Suggested Stable ID**: +A proposed stable ID generated during import that must be accepted or corrected before becoming canonical. +_Avoid_: stable ID, adapter ID + +**Content Hash**: +A fingerprint of deck content used to detect changes, not to identify entities. +_Avoid_: note ID, canonical ID + +**Overlay**: +A bounded set of changes applied to a base deck without replacing the base deck. +_Avoid_: fork, duplicate deck + +**Translation Overlay**: +An overlay that changes deck language or localized text. +_Avoid_: separate translated deck + +**Extension Overlay**: +An overlay that adds new deck content or structure. +_Avoid_: patch, translation + +**Patch Overlay**: +An overlay that corrects or adjusts existing deck content or structure. +_Avoid_: extension, fork + +**Personal Overlay**: +An overlay containing learner-specific deck content or structure that should survive shared deck updates. +_Avoid_: upstream deck, maintainer patch, study state + +**Overlay Fragment**: +The sparse deck-shaped content of an overlay, containing only the deck entities and properties the overlay changes. +_Avoid_: full deck copy, command script + +**Source Variable**: +A named text value defined on a deck, note type, card template, or note and referenced from source text with `${variable.name}` before adapter export. +_Avoid_: recipe variable, runtime Anki field + +**Translation Dictionary**: +A translation overlay section mapping exact source text, source variables, and adapter IDs to their translated values, with the source key acting as an implicit expected base. +_Avoid_: CSV importer, global localization database + +**Field Fill**: +An overlay shorthand for filling existing blank note fields with new content while requiring the upstream field to still be blank. +_Avoid_: translation addition, field definition addition + +**Change Intent**: +The declared meaning of an overlay change, such as add, merge, replace, remove, or override. +_Avoid_: implicit overwrite, accidental merge + +**Tombstone**: +A record that a deck entity was deliberately removed. +_Avoid_: missing data, accidental deletion + +**Expected Base**: +The prior deck value or fingerprint an overlay declares before making a destructive or conflict-resolving change. +_Avoid_: current value guess, unchecked overwrite + +**Overlay Stack**: +An ordered set of overlays applied to a base deck. +_Avoid_: unordered overlay set, dependency graph + +**Overlay Catalog**: +A named collection of overlays available in a Federated Deck. +_Avoid_: raw file list, package solver + +**Overlay Dependency**: +A requirement that one overlay include and apply another overlay before itself. +_Avoid_: implicit conflict resolution, automatic content merge + +**Build Target**: +A named composition goal that resolves a base deck and selected overlays into a Resolved Deck. +_Avoid_: Anki export, source file, recipe step + +**Resolved Deck**: +The deck produced by applying an overlay stack to a base deck. +_Avoid_: build artifact, export format + +**Compose**: +To produce a resolved deck by applying an overlay stack to a base deck. +_Avoid_: build, export + +**Semantic Diff**: +A comparison of decks by stable IDs and deck entities rather than raw source lines. +_Avoid_: text diff, file diff + +**Federation Conflict**: +A situation where overlays make incompatible changes to the same deck entity. +_Avoid_: validation warning, last write wins + +**Deck Federation**: +The composition of a base deck with translations, extensions, patches, or personal overlays without copying the whole deck. +_Avoid_: fork, duplicate deck, one-off conversion + +**Canonicalized Source**: +A source representation after Brain Brew has applied its deterministic formatting rules. +_Avoid_: arbitrary original bytes, hand-formatted source + +**Round Trip**: +A workflow where deck data can move from source files to a distributable deck format and back without losing intentional information. +_Avoid_: import only, export only, one-way conversion + +## Relationships + +- **Brain Brew** primarily serves **Deck Maintainers**. +- A **Deck Maintainer** publishes one or more **Shared Decks**. +- A **Deck Maintainer** may publish a **Federated Deck** as a composable shared-deck source package. +- A **Federated Deck** may contribute a base **Deck**, **Overlays**, or both. +- **Federated Decks** compose through **Deck Federation** to produce **Resolved Decks**. +- A **Learner** studies a **Shared Deck** and may have a **Personal Overlay**. +- **Brain Brew** works on **Decks**. +- A **Canonical Deck** represents one **Deck** without binding it to a source or distribution format. +- A **Canonical Deck File** is the source of truth for a **Canonical Deck**. +- A **Deck** contains **Deck Entities**. +- A **Note** belongs to one **Note Type**. +- A **Note Type** has one or more **Card Templates**. +- A **Card** is produced from one **Note** and one **Card Template**. +- **Review History** is preserved by stable identity, not stored as **Deck** content. +- A **Media Reference** points to one **Media Asset**. +- A **Stable ID** identifies a **Deck Entity** across a **Round Trip**. +- An **Adapter ID** preserves identity in a specific external format or tool. +- A **Suggested Stable ID** becomes a **Stable ID** only after maintainer review. +- A **Content Hash** describes current content for change detection. +- **Deck Federation** combines one base **Deck** with zero or more **Overlays**. +- An **Overlay** contains an **Overlay Fragment**. +- An **Overlay** may use a **Translation Dictionary** to translate extracted source text without repeating per-field replacement boilerplate. +- An **Overlay** may use **Field Fills** to add content to existing blank note fields without misclassifying that content as a translation. +- A **Source Variable** lets shared card template structure refer to phrase values translated by a **Translation Dictionary**. +- An **Overlay** uses **Change Intents** to change **Deck Entities** by **Stable ID**. +- Replace, remove, and override **Change Intents** require an **Expected Base**. +- A remove **Change Intent** creates a **Tombstone**. +- An **Overlay Catalog** names the overlays available in a **Federated Deck**. +- An **Overlay Dependency** constrains the order of an **Overlay Stack**. +- An **Overlay Stack** applies overlays in declared or dependency-expanded order. +- A **Build Target** selects overlays from an **Overlay Catalog** for composition. +- **Compose** applies an **Overlay Stack** to a base **Deck** to produce a **Resolved Deck**. +- A **Semantic Diff** compares **Decks** through **Deck Entities** and **Stable IDs**. +- A **Federation Conflict** must be resolved explicitly. +- **Translation Overlays**, **Extension Overlays**, **Patch Overlays**, and **Personal Overlays** are kinds of **Overlay**. +- A **Round Trip** preserves a **Deck** across source and distributable forms. +- A **Round Trip** reproduces **Canonicalized Source**, not arbitrary original source bytes. + +## Example dialogue + +> **Dev:** "If a translator adds German text to Ultimate Geography, are they creating a new independent deck?" +> **Domain expert:** "No — they are participating in **Deck Federation** by applying a translation overlay to the base **Deck**." + +## Flagged ambiguities + +- "sync tool" previously meant live bidirectional note-system synchronization; resolved: **Brain Brew** is first a local-first **Deck Federation** and **Round Trip** system. +- "canonical note" previously meant the central federation object; resolved: the central object is the **Canonical Deck**, because a **Deck** includes more than notes. +- "content hash" previously meant identity; resolved: a **Content Hash** detects change, while a **Stable ID** defines sameness. +- "Anki GUID" could mean canonical identity; resolved: Anki/CrowdAnki GUIDs are **Adapter IDs**, while human-readable **Stable IDs** identify canonical deck entities. +- "personal overlay" was used to mean both learner workflow and derivative change; resolved: a **Personal Overlay** is a derivative change, while a full learner workflow is not implied. +- "preserve Anki history" could mean storing review data; resolved: **Review History** remains outside **Canonical Deck** content and is preserved through stable identity. +- "media in the deck file" could mean embedded bytes; resolved: **Canonical Deck** stores **Media References**, while **Media Assets** remain external files. +- "overlay order" could imply last-write-wins; resolved: an **Overlay Stack** is ordered, but conflicting changes fail unless explicitly resolved. +- "byte-for-byte round trip" could mean preserving arbitrary input formatting; resolved: byte stability applies to **Canonicalized Source**. +- "CSV source" previously implied the maintainer source of truth; resolved: the **Canonical Deck File** is the source of truth, while CSV is an adapter format. +- "subdeck" could mean Anki deck hierarchy; resolved: composable source packages in a deck federation are **Federated Decks**, not Anki subdecks. +- "translated deck identity" could mean a separate stable identity per language; resolved: translations use language-neutral **Stable IDs** for the same conceptual **Deck Entities** and language-specific external identities remain **Adapter IDs**. +- "Ultimate Geography support" could mean product-specific application behavior; resolved: Ultimate Geography is a demanding case study and parity fixture for general Brain Brew federation behavior, not a special-purpose application feature. +- "migration import" could mean Brain Brew should convert every legacy source layout; resolved: initial migration means refactoring into **Canonical Deck Files** and proving output parity, not building public legacy source importers. diff --git a/README.md b/README.md index 16b7f70..4388635 100644 --- a/README.md +++ b/README.md @@ -1,254 +1,91 @@ -# Brain-Brew +# Brain Brew - - +Brain Brew is a Rust-based, local-first deck federation and round-trip engine for shared Anki-compatible decks. -Brain Brew is an open-source flashcard manipulation tool designed to allow users to convert their Anki flashcards to/from many different formats to suit their own needs. -The goal is to facilitate collaboration and maximize user choice, with a powerful tool that minimizes effort. -[CrowdAnki](https://github.com/Stvad/CrowdAnki) Exports and Csv(s) are the only supported file types as of now, but there will be more to come. +It continues the established Brain Brew project name while replacing the legacy Python recipe pipeline with canonical deck source, overlays, manifests, and reproducible verification. +It aims to help deck maintainers compose a base deck with translations, extensions, patches, and personal overlays while preserving stable identity for Anki/CrowdAnki round trips. -[Anki Ultimate Geography](https://github.com/axelboc/anki-ultimate-geography/) is currently the best working example of a Flashcard repo using Brain Brew :tada: -See there for inspiration! +## Current Status +Brain Brew now has a working Rust core, reusable format codecs, and a thin CLI for Canonical Deck validation, overlay composition, CrowdAnki import/export, semantic diffing, media checks, authoring helpers, Federated Deck manifests, package-qualified target composition, and locked package inputs. -# Installation +The repository includes two tested fixtures: +- `fixtures/ug-style/` — a small Ultimate Geography-style fixture for fast end-to-end checks. +- `fixtures/ultimate-geography/` — a full Ultimate Geography canonical workspace used as a large parity case study, including Hardcore Geography as an extension overlay. -Install the latest version of [Brain Brew on PyPi.org](https://pypi.org/project/Brain-Brew/) -with `pip install brain-brew`. Virtual environment using `pipenv` is recommended! +Ultimate Geography is a fixture and case study for the general federation workflow; it is not a special product-specific CLI feature. -:exclamation: See the [Brain Brew Starter Project][BrainBrewStarter] for a working clone-able Git repo. -From this repo you can now create a functional Brain Brew setup automatically, -with your own flashcards! Simply by running +## Federated Deck workflow -```bash -brainbrew init [Your CrowdAnki Export Folder] -``` - -This will generate the entire working repo for you, including the recipe files, source files, and build folder. -For bi-directional sync: Anki <-> Source! - -See [the starter repo][BrainBrewStarter] for a step-by-step guide for all of this. +A Federated Deck workspace contains a base Canonical Deck, overlays, and a `brainbrew.yaml` manifest declaring reproducible build targets. -# Usage - -Brain Brew runs from the command line and takes a *Recipe.yaml* file to run. +Common commands: ```bash -brainbrew run source_to_anki.yaml +brainbrew targets --manifest brainbrew.yaml --json +brainbrew targets --package-root ../anki-geo-packages +brainbrew lock update --package anki-geo.ultimate-geography --path ../ultimate-geography +brainbrew lock verify +brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/ +brainbrew explain --manifest brainbrew.yaml --target de-extended --json +brainbrew compose --manifest brainbrew.yaml --target de-extended --out build/de-extended.yaml +brainbrew export crowdanki --manifest brainbrew.yaml --target de-extended --media-root media/ +brainbrew diff deck.yaml edited.yaml --as-overlay --id overlay.patch.capitals --kind patch ``` -Full usage help text: -```bash -Brain Brew vx.y.z -usage: brainbrew [-h] {run,init} ... +See the dedicated documentation site in [`documentation/`](documentation/) for manifest, source variable, translation dictionary, overlay, locking, and example workflows. Lock update/verify uses Rust-native fetching and NAR hashing; Nix is only an optional install/build path. -Manage Flashcards by transforming them to various types. +## Install the CLI with Nix -positional arguments: - {run,init} Commands that can be run - run Run a recipe file. This will convert some data to another format, based on the instructions in the recipe file. - init Initialise a Brain Brew repository, using a CrowdAnki export as the base data. +Run the CLI directly from this flake: -optional arguments: - -h, --help show this help message and exit +```bash +nix run . -- --help ``` +Build a local binary: -## Recipes - -These are the instructions for how Brain Brew will ~~build~~ *brew* your data into another format. - -What's YAML? See the current spec [here](http://www.yaml.org/spec/1.2/spec.html). - -Run a recipe with `--verify` or `-v` to confirm your recipe is valid, without actually running it. -A dry run of sorts. - -### Tasks - -A recipe is made of many individual tasks, which do specific functions. -Full detailed list coming soon™️, but see the [Yamale recipe schema](https://github.com/jeprecated/brain-brew/blob/master/brain_brew/schemas/recipe.yaml) -(local file: `brain_brew/schemas/recipe.yaml`) in the meantime :+1: - - - - -[//]: <> (Yamale) - -# The Why - -Brain Brew was made in an effort to solve some of the following issues with current collaboration of Anki Flashcards: - -#### Sharing Personal Information or Copyrighted Material - -Have some personal notes on your cards? Used some images randomly taken from the internet? -That usually means you cannot share your deck entirely, without having to go to the effort of removing the offending material and/or managing two separate copies. - -#### Having to Pick Between Source Control or Anki Editing - -Putting your cards into a source control system brings a lot of benefits. -You can see any changes that occur, go back in time should an mistake be discovered, and collaborate with others. - -However the current tools for managing Anki cards in source control -(such as [Anki-DM](https://github.com/OnkelTem/anki-dm), [GenAnki](https://github.com/kerrickstaley/genanki), -and [Remote Decks](https://github.com/c-okelly/anki-remote-decks)) are only one way. -You generate cards from a csv into a file that can *only be imported* into Anki. -There is no way to export them back, meaning a user must manually copy their changes over, or simple not edit their cards anywhere other than in source control. - -This robs the user of two important work flows: -1. Editing/fixing cards in Anki as you review them (on desktop or mobile) -1. The plethora of Anki add-ons that already exist that are amazingly useful. E.g: Image Occlusion, Morphman, AwesomeTTS. - -A user should not have to pick between these fantastic work flows and the usage of source control to structure, manage, and share their cards. - -#### Lack of Formatting Choice - -Csvs are great for editing data, but can only go so far by themselves. Having all the data inside one csv leaves a lot to be desired and can result in eventual problems. -When one gets as many columns as *this* (from [Ultimate Geography](https://github.com/axelboc/anki-ultimate-geography/)) then it becomes a nightmare to manage: - -|guid|Country|Country:de|Country:es|Country:fr|Country:nb|"Country info"|"Country info:de"|"Country info:es"|"Country info:fr"|"Country info:nb"|Capital|Capital:de|Capital:es|Capital:fr|Capital:nb|"Capital info"|"Capital info:de"|"Capital info:es"|"Capital info:fr"|"Capital info:nb"|"Capital hint"|"Capital hint:de"|"Capital hint:es"|"Capital hint:fr"|"Capital hint:nb"|Flag|"Flag similarity"|"Flag similarity:de"|"Flag similarity:es"|"Flag similarity:fr"|"Flag similarity:nb"|Map|tags| -| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -|crr.AfnVRi|England|England|Inglaterra|Angleterre|England|"Constituent country of the United Kingdom."|"Landesteil des Vereinigten Königreichs."|"Nación constitutiva del Reino Unido."|"Nation constitutive du Royaume-Uni."|"Land som utgjør en del av Storbritannia."|London|London|Londres|Londres|London| | | | | |"Not a sovereign country"|"Kein souveräner Staat"|"No es un país soberano"|"Pas une nation souveraine"|"Ikke selvstendig land"|""| | | | | |""|UG::Europe| -"h"|"Ireland (orange and green flipped, wider)"|"Irland (Orange und Grün vertauscht, breiter)"|"Irlanda (naranja y verde intercambiados, más ancha)"|"Irlande (orange et vert inversés, plus large)"|"Ireland (byttet plass på oransje og grønt, bredere)"|""|"UG::Africa UG::Sovereign_State UG::West_Africa" - -Then there's having too many rows in one csv for it to be properly managed. - - -# Features of Brain Brew -### Multi-directional Card Syncing -Make changes in your source file and sync those into your Anki collection. - -Make changes inside Anki and pull those back into the source. - -Any user of your shared deck can make a change inside Anki and at some later point export their deck (or just part of it) using CrowdAnki. -Then the source file can be updated with their changes and a new CrowdAnki Export for all users to import can be generated with one run of Brain Brew. - -### Modular Configuration Files -Yaml config files are what drive the conversion of Brain Brew, allowing users to easily change the functionality as they wish. - - - -```Yaml -- generate_guids_in_csv: - source: src/data/words.csv - columns: [ guid ] - -- build_parts: - - note_model_from_yaml_part: - part_id: LL Word - file: src/note_models/LL Word.yaml - - - headers_from_yaml_part: - part_id: default header - file: src/headers/default.yaml - override: # Optional - deck_description_html_file: src/headers/desc.html - - - media_group_from_folder: - part_id: all_media - source: src/media - recursive: true # Optional - - - notes_from_csvs: - part_id: english-to-danish - - note_model_mappings: - - note_models: - - LL Word - columns_to_fields: # Optional - guid: guid - tags: tags - - english: English - danish: Word - picture: Picture - danish audio: Pronunciation (Recording and/or IPA) - - file_mappings: - - file: src/data/words.csv - note_model: LL Word - sort_by_columns: [english] # Optional - reverse_sort: no # Optional +```bash +nix build .#brainbrew +./result/bin/brainbrew --help ``` -### Personal Fields -Deck managers can set specific fields to be "Personal", meaning they will not overwrite an existing value on import. - -Working version currently exists, but full PR coming soon to CrowdAnki! - -### Extensibility and Open Source -Free for all to use, modify, or sell this product. - -Further source types are relatively easy to add due to the flexible nature of the backend -Instead of creating a Csv <-> CrowdAnki converter Brain Brew first goes through a middle layer called "Deck Parts". -These consist of Notes, Headers, Note Models, and Media files. - -Each new source type to be added to Brain Brew (such as Markdown) need only be able to convert from Deck Parts <-> itself, and suddenly it can convert to and from all existing source types! +Install into your user profile: -### Smart Csvs - -Csvs only update the rows which have changed. -Meaning a user can import *a subset* of their cards which have changed and still update the source file without deleting the cards they did not include. - -##### Csv Splitting / Derivatives - -Split data into multiple csvs so that your data is neatly organised however you like. - -The two following csv files contain information about England, but split into different csv files: - -###### data-main.csv - -| guid | country | flag | map | tags | -| ---- | ---- | ---- | ---- | ---- | -| "e+/O]%*qfk | England | | | UG::Europe | - -###### data-capital.csv -| country | capital | capital de | capital es | capital fr | capital nb | -| ---- | ---- | ---- | ---- | ---- | ---- | -| England | London | London | Londres | Londres | London | - -Brain Brew can be told that `data-capital` is a derivative of `data-main` in the build config file as such: - -```yaml -- file: src/data/data-main.csv # <---- Main - note_model: Ultimate Geography - derivatives: - - file: src/data/data-country.csv - - file: src/data/data-country-info.csv - - file: src/data/data-capital.csv # <---- Capital - - file: src/data/data-capital-info.csv - - file: src/data/data-capital-hint.csv - # note_model: different_note_model - # derivatives: - # - file: derivative-of-a-derivative.csv - # derivatives: - # - file: infinite-nesting.csv - - file: src/data/data-flag-similarity.csv +```bash +nix profile install .#brainbrew +brainbrew --help ``` -When run Brain Brew will perform the following steps for each derivative: -1. Finds which columns in the derivative csv match the main (only `country` in this case) -1. Go through each row in the derivative and find the row with matching values in the main file -1. Add in the extra columns (`capital` in each language) to that matching row in the main file - -###### Resulting csv data -| guid | country | flag | map | tags | capital | capital de | capital es | capital fr | capital nb | -| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -| "e+/O]%*qfk | England | | | UG::Europe | London | London | Londres | Londres | London | - -##### Note: +See [`documentation/docs/getting-started/install.md`](documentation/docs/getting-started/install.md) for install options and an edit/export loop for trying changes against a Federated Deck workspace. -1. **Derivatives can also have derivatives**. +## Workspace -1. **Csv splitting works in both directions**, to and from csv. +```text +crates/ + brain-brew-core/ Pure domain model, validation, composition, semantic diffing + brain-brew-formats/ Reusable YAML and CrowdAnki codecs + brain-brew-cli/ Thin `brainbrew` command-line interface +``` -1. **Derivatives can be given a Note Model**, which overrides their parent's note model for all the matched rows. +## Development -See the [Brain Brew Starter Project][BrainBrewStarter] for an example of Csv Derivatives working. +This project uses Devbox: +```bash +devbox run fmt +devbox run test +devbox run clippy +devbox run ci +``` +Useful docs: -[BrainBrewStarter]: https://github.com/jeprecated/brain-brew-starter +- Agent guidance: [`AGENTS.md`](AGENTS.md) +- Documentation site source: [`documentation/`](documentation/) +- Start here: [`documentation/docs/intro.md`](documentation/docs/intro.md) +- Domain glossary: [`documentation/docs/reference/glossary.md`](documentation/docs/reference/glossary.md) +- Project scope: [`documentation/docs/reference/project-scope.md`](documentation/docs/reference/project-scope.md) +- Active ADRs: [`documentation/docs/reference/decisions/`](documentation/docs/reference/decisions/) diff --git a/documentation/README.md b/documentation/README.md new file mode 100644 index 0000000..7f499ea --- /dev/null +++ b/documentation/README.md @@ -0,0 +1,18 @@ +# Brain Brew documentation site + +This directory contains the Docusaurus documentation site for Brain Brew. + +```bash +npm install +npm run start +npm run build +``` + +From the repository root you can also use Devbox: + +```bash +devbox run docs:install +devbox run docs:build +``` + +The source pages live under `documentation/docs/`. The generated site is not committed. diff --git a/documentation/docs/authoring/diff-explain.md b/documentation/docs/authoring/diff-explain.md new file mode 100644 index 0000000..3b9453a --- /dev/null +++ b/documentation/docs/authoring/diff-explain.md @@ -0,0 +1,58 @@ +--- +title: Diff and explain +--- + +# Diff and explain + +Use `explain` to understand a target. Use `diff` to compare two decks or draft an overlay. + +## Explain a target + +```bash +brainbrew explain --manifest brainbrew.yaml --target de-extended +``` + +Human output shows the expanded overlay stack and semantic changes. + +For tools and UIs: + +```bash +brainbrew explain --manifest brainbrew.yaml --target de-extended --json +``` + +## Semantic diff + +```bash +brainbrew diff deck.yaml edited.yaml +``` + +Example output: + +```text +1 semantic change + +~ notes.note.finland.fields.field.capital + - Helsinki + + Helsingfors +``` + +The path uses stable IDs, not row numbers or raw YAML positions. + +## JSON diff + +```bash +brainbrew diff deck.yaml edited.yaml --json +``` + +Use JSON when another tool needs to render or inspect changes. + +## Draft an overlay + +```bash +brainbrew diff deck.yaml edited.yaml \ + --as-overlay \ + --id overlay.patch.capitals \ + --kind patch > overlays/patches/capitals.yaml +``` + +Review the generated overlay before committing. Destructive changes include `expected_base` values. diff --git a/documentation/docs/authoring/extensions.md b/documentation/docs/authoring/extensions.md new file mode 100644 index 0000000..ebe460c --- /dev/null +++ b/documentation/docs/authoring/extensions.md @@ -0,0 +1,81 @@ +--- +title: Extension overlays +--- + +# Extension overlays + +An extension overlay adds optional content or structure without copying the full deck. + +## Add fields and values + +Use `field_additions` when an extension adds fields to an existing note type and optionally fills them on existing notes. Existing notes that do not provide a value for a new field receive a blank value automatically. + +```yaml +id: overlay.extension.population +kind: extension +field_additions: + note-type.country: + fields: + field.population: Population + field.area: Area + values: + note.france: + field.population: 68 million + field.area: 643,801 km² + note.germany: + field.population: 84 million + field.area: 357,592 km² +``` + +## Add notes + +```yaml +id: overlay.extension.regions +kind: extension +notes: + note.brittany: + intent: add + note: + note_type_id: note-type.country + fields: + field.country: Brittany + field.capital: Rennes + tags: + - Europe + adapter_ids: {} +``` + +## Add card templates + +```yaml +id: overlay.variant.extended +kind: extension +note_types: + note-type.country: + intent: merge + card_templates: + template.capital-to-country: + intent: add + insert_after: template.country-to-capital + template: + name: Capital → Country + question_format: '{{Capital}}' + answer_format: '{{Country}}' + adapter_ids: {} +``` + +## Shared extension, small language residues + +For variants such as “Extended”, put shared structure in one overlay: + +```text +overlays/variants/extended.yaml +``` + +Put only real language-specific residue in per-language files: + +```text +overlays/variants/extended/de.yaml +``` + +Avoid copying full template HTML just to translate labels. Use source variables and translation dictionaries instead. diff --git a/documentation/docs/authoring/field-fills.md b/documentation/docs/authoring/field-fills.md new file mode 100644 index 0000000..10143cc --- /dev/null +++ b/documentation/docs/authoring/field-fills.md @@ -0,0 +1,63 @@ +--- +title: Field fills +--- + +# Field fills + +`field_fills` is an overlay shorthand for filling existing blank note fields. + +Use it when content belongs to an extension or patch, not to a translation dictionary. + +## Example + +```yaml +id: overlay.extension.hardcore.field-fills.en +kind: extension +field_fills: + note.anguilla: + field.capital: The Valley + field.flag: '' + note.canary-islands: + field.capital: Santa Cruz de Tenerife, Las Palmas + field.capital-info: The capital is shared between the two cities of Santa Cruz de Tenerife and Las Palmas. +``` + +This lowers to explicit checked changes: + +```yaml +notes: + note.anguilla: + intent: merge + fields: + field.capital: + intent: replace + value: The Valley + expected_base: + value: '' +``` + +If upstream later fills `field.capital`, composition fails instead of overwriting it. + +## When to use it + +Use `field_fills` for: + +- adding extension-owned content to blank fields on existing notes; +- preserving a non-destructive “only if blank” policy; +- language-specific extension content such as Hardcore Geography's filled capitals/flags. + +Do not use it for: + +- adding new field definitions — use [`field_additions`](extensions.md#add-fields-and-values); +- translating non-blank source text — use [`translations.changes`](translations.md#basic-dictionary); +- adding new notes — use `notes` with `intent: add`. + +## Why not `translations.additions`? + +A path-indexed value is not automatically a translation. + +`translations.additions` says “this blank localized text belongs to a translation overlay.” + +`field_fills` says “this extension or patch fills a blank field with new content.” + +Keeping them separate makes English extension content possible without inventing an English translation overlay. diff --git a/documentation/docs/authoring/manifests-targets.md b/documentation/docs/authoring/manifests-targets.md new file mode 100644 index 0000000..a7711b3 --- /dev/null +++ b/documentation/docs/authoring/manifests-targets.md @@ -0,0 +1,95 @@ +--- +title: Manifests and targets +--- + +# Manifests and targets + +`brainbrew.yaml` names the reproducible build targets in a workspace. + +## Minimal manifest + +```yaml +package: + id: example.capitals + version: 0.1.0 +base: deck.yaml +overlays: {} +targets: + en-standard: + overlays: [] +``` + +## Overlay catalog + +The catalog gives every overlay a stable reference: + +```yaml +overlays: + overlay.translation.de: + file: overlays/languages/de.yaml + kind: translation + overlay.variant.extended: + file: overlays/variants/extended.yaml + kind: extension +``` + +The manifest `kind` should match the overlay file's `kind`. + +## Overlay dependencies + +Dependencies are inclusion dependencies. Selecting the dependent overlay selects its dependencies first. + +```yaml +overlays: + overlay.variant.extended.de: + file: overlays/variants/extended/de.yaml + kind: extension + depends_on: + - overlay.translation.de + - overlay.variant.extended +``` + +The expanded stack is deterministic: + +```bash +brainbrew explain --manifest brainbrew.yaml --target de-extended +``` + +## Targets + +A target is a named composition goal. + +```yaml +targets: + de-extended: + overlays: + - overlay.variant.extended.de + exports: + crowdanki: + out: build/crowdanki/de-extended +``` + +Users and CI select targets instead of memorizing overlay paths. + +## Package-qualified targets + +A downstream package can extend an upstream target: + +```yaml +targets: + en-america: + extends: anki-geo.ultimate-geography:en-standard + overlays: + - overlay.extension.america +``` + +It may also mix package-qualified overlays: + +```yaml +targets: + en-mixed: + extends: anki-geo.ultimate-geography:en-standard + overlays: + - anki-geo.america:overlay.extension.america + - anki-geo.mountains:overlay.extension.rockies +``` diff --git a/documentation/docs/authoring/packages-locking.md b/documentation/docs/authoring/packages-locking.md new file mode 100644 index 0000000..3b1ff77 --- /dev/null +++ b/documentation/docs/authoring/packages-locking.md @@ -0,0 +1,84 @@ +--- +title: Packages and lock files +--- + +# Packages and lock files + +Federated packages let one repository compose with another without copying upstream source. + +## Downstream package + +```yaml +package: + id: anki-geo.america + version: 0.1.0 + depends_on: + - anki-geo.ultimate-geography@0.1.0 +base: deck.yaml +overlays: + overlay.extension.america: + file: overlays/america.yaml + kind: extension +targets: + en-america: + extends: anki-geo.ultimate-geography:en-standard + overlays: + - overlay.extension.america +``` + +## Local includes + +During local development, include another manifest explicitly: + +```bash +brainbrew compose \ + --manifest america/brainbrew.yaml \ + --include ultimate-geography/brainbrew.yaml \ + --target en-america +``` + +Or discover sibling packages: + +```bash +brainbrew targets --package-root ../anki-geo-packages +``` + +## Lock an upstream package + +```bash +brainbrew lock update \ + --package anki-geo.ultimate-geography \ + --git https://github.com/anki-geo/ultimate-geography.git \ + --ref main + +brainbrew lock verify +``` + +After locking, normal commands resolve packages from `brainbrew.lock` automatically: + +```bash +brainbrew compose --manifest america/brainbrew.yaml --target en-america +brainbrew verify --manifest america/brainbrew.yaml --all-targets +``` + +## Supported source inputs + +```bash +brainbrew lock update --package pkg.id --path ../pkg +brainbrew lock update --package pkg.id --tarball https://example.org/pkg.tar.gz +brainbrew lock update --package pkg.id --git https://github.com/owner/repo.git --ref main +``` + +The CLI computes `nar_hash` in Rust and does not require Nix at runtime. + +## Review after updates + +When upstream changes, rerun: + +```bash +brainbrew lock verify +brainbrew verify --manifest brainbrew.yaml --all-targets +brainbrew explain --manifest brainbrew.yaml --target en-america +``` + +Expected failures are the review surface: stale translation entries, expected-base mismatches, missing targets, media mismatches, or changed golden exports. diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md new file mode 100644 index 0000000..bdeed94 --- /dev/null +++ b/documentation/docs/authoring/translations.md @@ -0,0 +1,98 @@ +--- +title: Translation overlays +--- + +# Translation overlays + +A translation overlay changes deck language or localized text. It should not add unrelated extension content. + +## Basic dictionary + +```yaml +id: overlay.translation.de +kind: translation +translations: + changes: + Germany: Deutschland + Austria: Österreich +``` + +The source key is the expected base. If `Germany` no longer exists where the overlay expects it, composition fails with a stale translation entry. + +## Path-scoped translations + +Use a path when the same source text needs different translations in different places. + +```yaml +translations: + changes: + Overseas territory of the United Kingdom.: + notes.note.bermuda.fields.field.country-info: Britisches Überseegebiet. + notes.note.falkland-islands.fields.field.country-info: Britisches Überseegebiet des Vereinigten Königreichs. +``` + +## Blank localized text + +Use `translations.additions` only when blank localized text genuinely belongs to the translation overlay. + +```yaml +translations: + additions: + notes.note.united-kingdom.fields.field.country-info: Offiziell das Vereinigte Königreich Großbritannien und Nordirland. +``` + +If an extension fills blank fields with new content, use [`field_fills`](field-fills.md) instead. + +## Translate source variables + +Variables keep card templates shared across languages. + +Base source: + +```yaml +note_types: + note-type.country: + variables: + label.capital: Capital + label.location: Location + card_templates: + template.map: + question_format: '
${label.location}
{{Map}}' +``` + +Translation overlay: + +```yaml +translations: + variables: + label.capital: + Capital: Hauptstadt + label.location: + Location: Lage +``` + +Prefer variable translations over copying whole card templates per language. + +## Translate adapter IDs + +Legacy translated decks may already have different CrowdAnki GUIDs. + +```yaml +translations: + adapter_ids: + crowdanki:guid: + english-guid: german-guid +``` + +## Deterministic section order + +The formatter emits translation dictionary sections in this order: + +1. `require_complete` +2. `ignore_paths` +3. `changes` +4. `additions` +5. `variables` +6. `adapter_ids` + +A file with no `changes` starts at the next non-empty section. That is still deterministic. diff --git a/documentation/docs/authoring/verify-export.md b/documentation/docs/authoring/verify-export.md new file mode 100644 index 0000000..4563e2d --- /dev/null +++ b/documentation/docs/authoring/verify-export.md @@ -0,0 +1,87 @@ +--- +title: Verify and export +--- + +# Verify and export + +Verification is the CI gate for a Federated Deck workspace. + +## Verify one target + +```bash +brainbrew verify --manifest brainbrew.yaml --target de-standard +``` + +## Verify every target + +```bash +brainbrew verify --manifest brainbrew.yaml --all-targets +``` + +Verification checks: + +1. manifest parsing and formatting; +2. base deck parsing and formatting; +3. overlay parsing and formatting; +4. lock-file package resolution and hashes; +5. dependency expansion; +6. target composition; +7. Canonical Deck validation; +8. configured CrowdAnki golden checks. + +## Verify media + +```bash +brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/ +``` + +With `--media-root`, Brain Brew checks that referenced media files exist and match their declared SHA-256 hashes. + +## Export CrowdAnki + +```bash +brainbrew export crowdanki \ + --manifest brainbrew.yaml \ + --target de-standard \ + --out build/crowdanki/de-standard +``` + +With media copied into the CrowdAnki folder's `media/` subdirectory: + +```bash +brainbrew export crowdanki \ + --manifest brainbrew.yaml \ + --target de-standard \ + --media-root media/ \ + --out build/crowdanki/de-standard +``` + +## Default and configured export paths + +```yaml +targets: + de-standard: + overlays: + - overlay.translation.de + exports: + crowdanki: + out: build/crowdanki/de-standard + golden: goldens/de-standard/deck.json +``` + +When `--out` is omitted, Brain Brew uses `exports.crowdanki.out` when configured; otherwise it defaults to `build/crowdanki/`. For example: + +```bash +brainbrew export crowdanki --manifest brainbrew.yaml --target de-standard +``` + +## Golden checks + +When `golden` is configured, `verify` compares generated CrowdAnki JSON against the golden as parsed JSON. + +Use `golden_allowlist` only after reviewing concrete differences: + +```yaml +golden_allowlist: + - note_models[0].latex_pre +``` diff --git a/documentation/docs/authoring/workspace.md b/documentation/docs/authoring/workspace.md new file mode 100644 index 0000000..0fbb2b7 --- /dev/null +++ b/documentation/docs/authoring/workspace.md @@ -0,0 +1,65 @@ +--- +title: Workspace layout +--- + +# Workspace layout + +A Federated Deck workspace contains a manifest, a base deck, and overlays. + +```text +my-deck/ + brainbrew.yaml + deck.yaml + overlays/ + languages/de.yaml + variants/extended.yaml + variants/extended/de.yaml + extensions/rivers.yaml + patches/capitals.yaml + media/ + flags/fi.svg +``` + +## `deck.yaml` + +The base Canonical Deck. It owns shared structure and content. + +## `overlays/` + +Sparse changes to the base deck. Keep overlays small and purpose-shaped: + +- language overlays in `overlays/languages/`; +- shared variant overlays in `overlays/variants/`; +- optional content extensions in `overlays/extensions/`; +- corrections in `overlays/patches/`. + +## `brainbrew.yaml` + +The manifest declares package metadata, named overlays, dependencies, and build targets. + +```yaml +package: + id: example.capitals + version: 0.1.0 +base: deck.yaml +overlays: + overlay.translation.de: + file: overlays/languages/de.yaml + kind: translation +targets: + de-standard: + overlays: + - overlay.translation.de +``` + +## Formatting + +Use canonical formatting as a review gate: + +```bash +brainbrew fmt deck.yaml +brainbrew fmt brainbrew.yaml +find overlays -name '*.yaml' -print0 | xargs -0 -n1 brainbrew fmt +``` + +`brainbrew verify --all-targets` also checks formatting. diff --git a/documentation/docs/concepts/canonical-deck.md b/documentation/docs/concepts/canonical-deck.md new file mode 100644 index 0000000..0311487 --- /dev/null +++ b/documentation/docs/concepts/canonical-deck.md @@ -0,0 +1,72 @@ +--- +title: Canonical Deck +--- + +# Canonical Deck + +A Canonical Deck is Brain Brew' format-independent representation of an Anki-compatible deck. + +It includes deck metadata, note types, card templates, notes, tags, media references, tombstones, stable IDs, and adapter IDs. + +It excludes review history and scheduling state. + +## Shape + +```yaml +deck: + id: deck.capitals + name: Capital Cities + description: A small geography deck. + adapter_ids: {} +note_types: + note-type.capital: + name: Capital Card + field_order: + - field.country + - field.capital + fields: + field.country: + name: Country + field.capital: + name: Capital + card_template_order: + - template.country-to-capital + card_templates: + template.country-to-capital: + name: Country → Capital + question_format: '{{Country}}' + answer_format: '{{Capital}}' + adapter_ids: {} + styling: '' + adapter_ids: {} +notes: + note.france: + note_type_id: note-type.capital + fields: + field.country: France + field.capital: Paris + tags: [] + adapter_ids: {} +media: {} +tombstones: [] +``` + +## Strict source + +Canonical YAML is deliberately strict: + +- unknown fields fail; +- stable IDs key entities; +- note type field/template order is explicit; +- formatting is deterministic; +- comments are not part of the durable model. + +Run the formatter before review: + +```bash +brainbrew fmt deck.yaml +``` + +## Round trips + +CrowdAnki import/export is an adapter around this model. The adapter preserves Anki-compatible deck semantics and external IDs, but the Canonical Deck stays the source of truth. diff --git a/documentation/docs/concepts/identity.md b/documentation/docs/concepts/identity.md new file mode 100644 index 0000000..4589fde --- /dev/null +++ b/documentation/docs/concepts/identity.md @@ -0,0 +1,62 @@ +--- +title: Stable IDs and adapter IDs +--- + +# Stable IDs and adapter IDs + +Brain Brew separates deck identity from external-tool identity. + +## Stable IDs + +Stable IDs are maintainer-owned names for deck entities: + +```yaml +notes: + note.finland: + note_type_id: note-type.ultimate-geography +``` + +They are used by overlays, diffs, manifests, and tests. They should be readable and stable across releases. + +Good stable IDs: + +- `note.finland` +- `field.capital` +- `template.country-map` +- `media.flag.finland` + +## Adapter IDs + +Adapter IDs preserve identity in external tools such as Anki/CrowdAnki: + +```yaml +notes: + note.finland: + adapter_ids: + crowdanki:guid: abc123 +``` + +A translation overlay may map external IDs when a legacy translated target already has different Anki GUIDs: + +```yaml +id: overlay.translation.de +kind: translation +translations: + adapter_ids: + crowdanki:guid: + en-guid: de-guid +``` + +## Why keep both? + +Stable IDs make source review pleasant. Adapter IDs keep round trips compatible with existing exported decks. + +A semantic diff therefore reports stable paths: + +```text +~ notes.note.finland.fields.field.capital + - Helsinki + + Helsingfors +``` + +The path remains meaningful even when external GUIDs differ by language. diff --git a/documentation/docs/concepts/media.md b/documentation/docs/concepts/media.md new file mode 100644 index 0000000..e7623ee --- /dev/null +++ b/documentation/docs/concepts/media.md @@ -0,0 +1,59 @@ +--- +title: Media references +--- + +# Media references + +Media assets are external files. Canonical Deck YAML stores references to those files and their hashes. + +## Declare media + +```yaml +media: + media.flag.finland: + path: flags/fi.svg + sha256: 7b2b... +``` + +Field text, template text, and note-type styling can then use normal Anki-compatible references (``, ` -
{{Country}}
-
- -
${label.location}
- -
- - - -
- - - - - - - + value: !include source/templates/experimental/country-map-question.html answer_format: intent: merge - value: | - -
{{Country}}
- {{#Country info}}
{{Country info}}
{{/Country info}} - -
- -
${label.location}
- -
{{Map}}
- - - - - + value: !include source/templates/experimental/country-map-answer.html media: media.interactive-map-config-js: intent: add diff --git a/fixtures/ultimate-geography/overlays/variants/extended.yaml b/fixtures/ultimate-geography/overlays/variants/extended.yaml index 62f75d4..3919ad3 100644 --- a/fixtures/ultimate-geography/overlays/variants/extended.yaml +++ b/fixtures/ultimate-geography/overlays/variants/extended.yaml @@ -15,66 +15,14 @@ note_types: insert_after: template.capital-country template: name: Country - Flag - question_format: |- - {{#Flag}} -
{{Country}}
- {{#Country info}}
{{Country info}}
{{/Country info}} - -
- -
${label.flag}
-
- - - -
- {{/Flag}} - answer_format: | -
{{Country}}
- {{#Country info}}
{{Country info}}
{{/Country info}} - -
- -
${label.flag}
-
{{Flag}}
- {{#Flag similarity}}
${sentence.flag-similar}
{{/Flag similarity}} + question_format: !include source/templates/extended/country-flag-question.html + answer_format: !include source/templates/extended/country-flag-answer.html adapter_ids: {} template.country-map: intent: add insert_after: template.flag-country template: name: Country - Map - question_format: |- - {{#Map}} -
{{Country}}
- -
- -
${label.location}
-
- - - -
- {{/Map}} - answer_format: | -
{{Country}}
- {{#Country info}}
{{Country info}}
{{/Country info}} - -
- -
${label.location}
-
{{Map}}
+ question_format: !include source/templates/extended/country-map-question.html + answer_format: !include source/templates/extended/country-map-answer.html adapter_ids: {} diff --git a/fixtures/ultimate-geography/overlays/variants/extended/sv.yaml b/fixtures/ultimate-geography/overlays/variants/extended/sv.yaml index 3cbd312..3657ec7 100644 --- a/fixtures/ultimate-geography/overlays/variants/extended/sv.yaml +++ b/fixtures/ultimate-geography/overlays/variants/extended/sv.yaml @@ -8,41 +8,9 @@ deck: value: 'Ultimate Geography [SV]' description: intent: override - value: | - FULL DESCRIPTION | RELEASE NOTES | CONTRIBUTING - - Ultimate Geography v5.3 features: - - - the world's 205 sovereign states (820 cards) - - 59 territories, world regions, and other entities (103 cards) - - 48 oceans and seas (48 cards, maps only) - - 7 continents (7 cards, maps only) - - for a total of 319 unique notes, 978 cards, 221 flags and 319 maps. - - The deck is available in English, German, Spanish, French, Norwegian, Czech, Russian, Dutch, Swedish, Portuguese, Chinese (simplified and traditional), Polish, Italian and Danish. An extended version is also available in each language. To help with memorisation and provide context while learning, some notes include extra information such as similar flags, governance information, alternative country names, etc. - - You can use Anki's filtered deck feature to focus your study on a subset of the deck, such as sovereign states, a single note template (e.g. map to country), or a specific continent (e.g. Europe). - - This deck is maintained on GitHub. If you spot a mistake, have a suggestion or want to help, please don't hesitate to open an issue. Want to stay informed of new releases? Watch the GitHub repository or subscribe to the releases feed! + value: !include source/descriptions/en.html expected_base: - value: | - FULLSTÄNDIG BESKRIVNING | UTGIVNINGSANMÄRKNINGAR | BIDRA - - Ultimate Geography v5.3 innehåller: - - - världens 205 självständiga stater (820 kort) - - 59 territorum, världsregioner och andra enheter (103 kort) - - 48 sjöar och hav (48 kort, enbart kartor) - - 7 kontinenter (7 kort, enbart kartor) - - sammanlagt 319 unika noter, 978 kort, 221 flaggor och 319 kartor. - - Kortleken finns tillgänglig på fjorton olika språk: engelska, tyska, spanska, franska, norska, tjeckiska, ryska, nederländska, svenska, portugisiska, kinesiska, polska, italienska och danska. För vardera språk finns dessutom en utökad version. - - För att underlätta inlärningen så har vissa kort en extra informationsrad, med exempelvis information om att flaggan på kortet liknar en annan flagga, politisk styrelse eller ett alternativt namn på ett land. - - Du kan skapa en filtrerad kortlek om du vill fokusera på specifika delar av kortleken. Till exempel kan du på så sätt välja att enbart studera suveräna stater, endast en specifik korttyp (såsom "karta → land") eller en specifik världsdel (till exempel Europa). - - Den här kortleken administreras på GitHub. Om du upptäcker ett fel, har förslag eller vill hjälpa till, tveka inte att öppna ett ärende. Vill du bli informerad om uppdateringar av kortleken? Bevaka källlkodskatalogen på GitHub eller prenumerera på feeden för utgåvor! + value: !include source/descriptions/sv.html adapter_ids: crowdanki:uuid: intent: override diff --git a/fixtures/ultimate-geography/source/descriptions/en.html b/fixtures/ultimate-geography/source/descriptions/en.html new file mode 100644 index 0000000..f3d5d4f --- /dev/null +++ b/fixtures/ultimate-geography/source/descriptions/en.html @@ -0,0 +1,15 @@ +FULL DESCRIPTION | RELEASE NOTES | CONTRIBUTING + +Ultimate Geography v5.3 features: + +- the world's 205 sovereign states (820 cards) +- 59 territories, world regions, and other entities (103 cards) +- 48 oceans and seas (48 cards, maps only) +- 7 continents (7 cards, maps only) +- for a total of 319 unique notes, 978 cards, 221 flags and 319 maps. + +The deck is available in English, German, Spanish, French, Norwegian, Czech, Russian, Dutch, Swedish, Portuguese, Chinese (simplified and traditional), Polish, Italian and Danish. An extended version is also available in each language. To help with memorisation and provide context while learning, some notes include extra information such as similar flags, governance information, alternative country names, etc. + +You can use Anki's filtered deck feature to focus your study on a subset of the deck, such as sovereign states, a single note template (e.g. map to country), or a specific continent (e.g. Europe). + +This deck is maintained on GitHub. If you spot a mistake, have a suggestion or want to help, please don't hesitate to open an issue. Want to stay informed of new releases? Watch the GitHub repository or subscribe to the releases feed! diff --git a/fixtures/ultimate-geography/source/descriptions/es.html b/fixtures/ultimate-geography/source/descriptions/es.html new file mode 100644 index 0000000..cb01ff5 --- /dev/null +++ b/fixtures/ultimate-geography/source/descriptions/es.html @@ -0,0 +1,15 @@ +DESCRIPCIÓN COMPLETA | NOTAS DE LANZAMIENTO | CÓMO CONTRIBUIR + +Características de Ultimate Geography v5.3: + +- Los 205 estados soberanos del mundo (820 cartas). +- 59 territorios, regiones del mundo, y otras entidades (103 cartas). +- 48 océanos y mares (48 cartas, solo mapas). +- 7 continentes (7 cartas, solo mapas). +- Un total de 319 notas diferentes, 978 cartas, 221 banderas y 319 mapas. + +Este mazo está disponible en inglés, alemán, español, francés, noruego, checo, ruso, neerlandés, sueco, portugués, chino (simplificado y tradicional), polaco, italiano y danesa. También está disponible una versión extendida en cada idioma. Para ayudar a la memorización y contextualizar mientras se aprende, algunas notas incluyen información extra como banderas similares, información sobre la forma de gobierno, nombres alternativos del país, etc. + +Puedes usar la función de Anki de mazos filtrados para centrarte en alguna parte del mazo en particular, como estados soberanos, un tipo de plantilla de nota (por ejemplo, mapa-país) o un continente en concreto (por ejemplo, Europa). + +Este mazo se desarrolla en Github. Si encuentras algún fallo, tienes alguna sugerencia o quieres ayudar, por favor, no dudes en abrir una incidencia. ¿Quieres mantenerte informado de las nuevas versiones? ¡Mira el repositorio de Github o sigue los lanzamientos! diff --git a/fixtures/ultimate-geography/source/descriptions/sv.html b/fixtures/ultimate-geography/source/descriptions/sv.html new file mode 100644 index 0000000..fa39dce --- /dev/null +++ b/fixtures/ultimate-geography/source/descriptions/sv.html @@ -0,0 +1,17 @@ +FULLSTÄNDIG BESKRIVNING | UTGIVNINGSANMÄRKNINGAR | BIDRA + +Ultimate Geography v5.3 innehåller: + +- världens 205 självständiga stater (820 kort) +- 59 territorum, världsregioner och andra enheter (103 kort) +- 48 sjöar och hav (48 kort, enbart kartor) +- 7 kontinenter (7 kort, enbart kartor) +- sammanlagt 319 unika noter, 978 kort, 221 flaggor och 319 kartor. + +Kortleken finns tillgänglig på fjorton olika språk: engelska, tyska, spanska, franska, norska, tjeckiska, ryska, nederländska, svenska, portugisiska, kinesiska, polska, italienska och danska. För vardera språk finns dessutom en utökad version. + +För att underlätta inlärningen så har vissa kort en extra informationsrad, med exempelvis information om att flaggan på kortet liknar en annan flagga, politisk styrelse eller ett alternativt namn på ett land. + +Du kan skapa en filtrerad kortlek om du vill fokusera på specifika delar av kortleken. Till exempel kan du på så sätt välja att enbart studera suveräna stater, endast en specifik korttyp (såsom "karta → land") eller en specifik världsdel (till exempel Europa). + +Den här kortleken administreras på GitHub. Om du upptäcker ett fel, har förslag eller vill hjälpa till, tveka inte att öppna ett ärende. Vill du bli informerad om uppdateringar av kortleken? Bevaka källlkodskatalogen på GitHub eller prenumerera på feeden för utgåvor! diff --git a/fixtures/ultimate-geography/source/descriptions/zh-tw.html b/fixtures/ultimate-geography/source/descriptions/zh-tw.html new file mode 100644 index 0000000..4d776e0 --- /dev/null +++ b/fixtures/ultimate-geography/source/descriptions/zh-tw.html @@ -0,0 +1,15 @@ +完整說明 | 發布版本 | 貢獻指南 + +Ultimate Geography v5.3 包含: + +- 全世界 205 個主權國家 (820 張卡片) +- 59 個領土、世界區域和其他實體 (103 張卡片) +- 48 個海洋 (48 張卡片,僅有地圖) +- 7 個大陸 (7 張卡片,僅有地圖) +- 總共 319 個不同的筆記, 978 張卡片, 221 個國旗 以及 319 個地圖. + +本牌組 包含 英文(English), 德文(German), 西班牙文(Spanish), 法文(French), 挪威語(Norwegian), 捷克語(Czech), 俄語(Russian), 荷蘭語(Dutch), 瑞典語(Swedish), 葡萄牙文(Portuguese), 簡體中文(Simplified Chinese/ZH), 繁體中文(Traditional Chinese/ZH-TW), 波蘭語(Polish), 意大利文(Italian)丹麥語(Danish)。 同時 每種語言皆有延伸版本。 為了幫助記憶和提供上下文,一些筆記帶有額外的資訊,例如類似的國旗、治理相關資訊、國家別名等。 + +你還能使用Anki的過濾牌組功能 , 將您的學習重點放在某個種類的牌上, 例如主權國家州、一種筆記模板(例如地圖對國家)或特定大陸(例如歐洲)。 + +這個牌組在 GitHub上維護。如果您發現錯誤、有建議或想要幫助我們,請不要猶豫打開一個問題(Issue)。 想要隨時了解新版本? 查看(Watch)我們的 GitHub Repository或者訂閱(Subscribe)發布通知(releases feed)! diff --git a/fixtures/ultimate-geography/source/descriptions/zh.html b/fixtures/ultimate-geography/source/descriptions/zh.html new file mode 100644 index 0000000..edf01bb --- /dev/null +++ b/fixtures/ultimate-geography/source/descriptions/zh.html @@ -0,0 +1,15 @@ +完整说明 | 发行说明 | 贡献指南 + +Ultimate Geography v5.3 特性: + +- 全世界 205 个主权国家 (820 张卡片) +- 59 个领土、世界区域和其他实体 (103 张卡片) +- 48 个海洋 (48 张卡片,仅地图) +- 7 个大陆 (7 张卡片,仅地图) +- 总计 319 个独立笔记, 978 张卡片, 221 个旗帜 以及 319 个地图. + +本卡组 支持 英语(English), 德语(German), 西班牙语(Spanish), 法语(French), 挪威语(Norwegian), 捷克语(Czech), 俄语(Russian), 荷兰语(Dutch), 瑞典语(Swedish), 葡萄牙语(Portuguese), 简化字(Simplified Chinese/ZH), 正體字(Traditional Chinese/ZH-TW), 波兰语(Polish), 意大利语(Italian)丹麦语(Danish)。 同时 每种语言均支持拓展版本。 为了帮助记忆并在学习时提供上下文,一些笔记带有额外的信息,例如类似的旗帜、治理信息、替代国家名称等。 + +你还能使用Anki的过滤牌组功能 , 将您的研究重点放在套牌的子集上, 例如主权州、单个笔记模板(例如地图→国家/地区)或特定大陆(例如欧洲)。 + +这个卡组在 GitHub上维护。如果您发现错误、有建议或想要帮助,请不要犹豫打开一个问题(Issue)。 想要随时了解新版本? 观看(Watch)我们的 GitHub 仓库(Repository)或者订阅(Subscribe)发布提要(releases feed)! diff --git a/fixtures/ultimate-geography/source/styles/standard.css b/fixtures/ultimate-geography/source/styles/standard.css new file mode 100644 index 0000000..252e2d0 --- /dev/null +++ b/fixtures/ultimate-geography/source/styles/standard.css @@ -0,0 +1,104 @@ +.card { + padding: 1em 0; + background-color: white; + color: black; + font-family: Verdana; + font-size: 16px; + text-align: center; +} + +.type { + margin-bottom: 0.25em; + color: #333; + font-size: 70%; + font-weight: bold; + text-transform: uppercase; +} + +.info { + max-width: 30em; + margin: 0.75em auto; + color: #333; + font-size: 90%; + font-style: italic; +} + +.value { + font-size: 150%; +} + +.value--top { + margin-top: 1em; +} + +.value--image { + margin-top: 0.75em; +} + +.value--map { + width: 90%; + height: 65vh; + margin: 2em auto 0 auto; +} + +.value > img, +.value > .placeholder { + max-width: 100%; + height: auto; +} + +/** + * Apply shadow to images, notably to bring out white areas on flags. + * Ignore images with non-rectangular outlines (e.g. flag of Nepal). + */ +.value > img:not([src*="-nobox"]) { + box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.2); +} + +/** + * Some flags (e.g. Guam's) contain identifying words that can give away the answer. + * If a blurred version is available, show it on the front but not on the back. + */ +.value--front > img[src*="-blur"] + img { + display: none; +} + +.value--back > img[src*="-blur"] { + display: none; +} + +/** + * Placeholder SVG to hint at the type of answer that is expected. + * Used on "Country - Flag" and "Country - Map" templates. + */ +.placeholder { + color: #333; +} + +.placeholder > path { + fill: none; + stroke: currentColor; + stroke-width: 1; +} + +.night_mode .info, +.night_mode .type, +.night_mode .placeholder, +.nightMode .info, +.nightMode .type, +.nightMode .placeholder { + color: #ccc; +} + +/** + * Apply shadow to images, to bring out black areas on flags, in night + mode. + */ +.nightMode .value > img:not([src*="-nobox"]), +.night_mode .value > img:not([src*="-nobox"]) { + box-shadow: 0 0 4px 1px rgba(54, 54, 54, 0.9); +} + +hr { + margin: 1.5em 0; +} diff --git a/fixtures/ultimate-geography/source/templates/experimental/country-map-answer.html b/fixtures/ultimate-geography/source/templates/experimental/country-map-answer.html new file mode 100644 index 0000000..19744ba --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/experimental/country-map-answer.html @@ -0,0 +1,15 @@ + +
{{Country}}
+{{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.location}
+ +
{{Map}}
+ + + + + diff --git a/fixtures/ultimate-geography/source/templates/experimental/country-map-question.html b/fixtures/ultimate-geography/source/templates/experimental/country-map-question.html new file mode 100644 index 0000000..0be14a0 --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/experimental/country-map-question.html @@ -0,0 +1,46 @@ + + +
{{Country}}
+
+ +
${label.location}
+ +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/fixtures/ultimate-geography/source/templates/extended/country-flag-answer.html b/fixtures/ultimate-geography/source/templates/extended/country-flag-answer.html new file mode 100644 index 0000000..375da7f --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/extended/country-flag-answer.html @@ -0,0 +1,8 @@ +
{{Country}}
+{{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.flag}
+
{{Flag}}
+{{#Flag similarity}}
${sentence.flag-similar}
{{/Flag similarity}} diff --git a/fixtures/ultimate-geography/source/templates/extended/country-flag-question.html b/fixtures/ultimate-geography/source/templates/extended/country-flag-question.html new file mode 100644 index 0000000..5ffac41 --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/extended/country-flag-question.html @@ -0,0 +1,19 @@ +{{#Flag}} +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.flag}
+
+ + + +
+{{/Flag}} \ No newline at end of file diff --git a/fixtures/ultimate-geography/source/templates/extended/country-map-answer.html b/fixtures/ultimate-geography/source/templates/extended/country-map-answer.html new file mode 100644 index 0000000..9a5c2bf --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/extended/country-map-answer.html @@ -0,0 +1,7 @@ +
{{Country}}
+{{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.location}
+
{{Map}}
diff --git a/fixtures/ultimate-geography/source/templates/extended/country-map-question.html b/fixtures/ultimate-geography/source/templates/extended/country-map-question.html new file mode 100644 index 0000000..4ca86a5 --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/extended/country-map-question.html @@ -0,0 +1,18 @@ +{{#Map}} +
{{Country}}
+ +
+ +
${label.location}
+
+ + + +
+{{/Map}} \ No newline at end of file diff --git a/fixtures/ultimate-geography/source/templates/standard/capital-country-answer.html b/fixtures/ultimate-geography/source/templates/standard/capital-country-answer.html new file mode 100644 index 0000000..714b109 --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/standard/capital-country-answer.html @@ -0,0 +1,9 @@ +
{{Country}}
+{{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.capital}
+
{{Capital}}
+{{#Capital hint}}
${label.capital-hint.answer}
{{/Capital hint}} +{{#Capital info}}
{{Capital info}}
{{/Capital info}} diff --git a/fixtures/ultimate-geography/source/templates/standard/capital-country-question.html b/fixtures/ultimate-geography/source/templates/standard/capital-country-question.html new file mode 100644 index 0000000..797ddec --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/standard/capital-country-question.html @@ -0,0 +1,9 @@ +{{#Capital}} +
?
+ +
+ +
${label.capital}
+
{{Capital}}
+ {{#Capital hint}}
${label.capital-hint.question}
{{/Capital hint}} +{{/Capital}} \ No newline at end of file diff --git a/fixtures/ultimate-geography/source/templates/standard/country-capital-answer.html b/fixtures/ultimate-geography/source/templates/standard/country-capital-answer.html new file mode 100644 index 0000000..0d2b2db --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/standard/country-capital-answer.html @@ -0,0 +1,8 @@ +
{{Country}}
+{{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.capital}
+
{{Capital}}
+{{#Capital info}}
{{Capital info}}
{{/Capital info}} diff --git a/fixtures/ultimate-geography/source/templates/standard/country-capital-question.html b/fixtures/ultimate-geography/source/templates/standard/country-capital-question.html new file mode 100644 index 0000000..368ccf8 --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/standard/country-capital-question.html @@ -0,0 +1,9 @@ +{{#Capital}} +
{{Country}}
+ {{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.capital}
+
?
+{{/Capital}} \ No newline at end of file diff --git a/fixtures/ultimate-geography/source/templates/standard/flag-country-answer.html b/fixtures/ultimate-geography/source/templates/standard/flag-country-answer.html new file mode 100644 index 0000000..da1a43e --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/standard/flag-country-answer.html @@ -0,0 +1,8 @@ +
{{Country}}
+{{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.flag}
+
{{Flag}}
+{{#Flag similarity}}
${sentence.flag-similar}
{{/Flag similarity}} diff --git a/fixtures/ultimate-geography/source/templates/standard/flag-country-question.html b/fixtures/ultimate-geography/source/templates/standard/flag-country-question.html new file mode 100644 index 0000000..88e076a --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/standard/flag-country-question.html @@ -0,0 +1,8 @@ +{{#Flag}} +
?
+ +
+ +
${label.flag}
+
{{Flag}}
+{{/Flag}} \ No newline at end of file diff --git a/fixtures/ultimate-geography/source/templates/standard/map-country-answer.html b/fixtures/ultimate-geography/source/templates/standard/map-country-answer.html new file mode 100644 index 0000000..3bbdcc2 --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/standard/map-country-answer.html @@ -0,0 +1,7 @@ +
{{Country}}
+{{#Country info}}
{{Country info}}
{{/Country info}} + +
+ +
${label.location}
+
{{Map}}
diff --git a/fixtures/ultimate-geography/source/templates/standard/map-country-question.html b/fixtures/ultimate-geography/source/templates/standard/map-country-question.html new file mode 100644 index 0000000..801453a --- /dev/null +++ b/fixtures/ultimate-geography/source/templates/standard/map-country-question.html @@ -0,0 +1,8 @@ +{{#Map}} +
?
+ +
+ +
${label.location}
+
{{Map}}
+{{/Map}} \ No newline at end of file diff --git a/skills/federated-deck-extensions/SKILL.md b/skills/federated-deck-extensions/SKILL.md index 43cc6f4..17baa5a 100644 --- a/skills/federated-deck-extensions/SKILL.md +++ b/skills/federated-deck-extensions/SKILL.md @@ -79,11 +79,11 @@ translations: 'Flag similar to {{Flag similarity}}.': 'Flaget ligner {{Flag similarity}}.' ``` -Use `translations.changes` for content text and path-scoped exceptions. Do **not** path-scope metadata like `note_types.note-type.ultimate-geography.name` if a variable can express it. +Use `translations.direct` for reusable content text and `translations.contextual` for path-scoped/content-specific exceptions. Do **not** path-scope metadata like `note_types.note-type.ultimate-geography.name` if a variable can express it. ### 3. Use field fills for non-translation blank content -If an extension fills fields that already exist but are blank on base notes, use `field_fills` in an extension or patch overlay. Do not put extension content under `translations.additions` just because it is path-indexed. +If an extension fills fields that already exist but are blank on base notes, use `field_fills` in an extension or patch overlay. Do not put extension content under `translations.target_additions` just because it is path-indexed. Prefer: @@ -96,7 +96,7 @@ field_fills: field.flag: '' ``` -Reserve `translations.additions` for blank localized text that is genuinely part of a translation overlay. +Reserve `translations.target_additions` for blank localized text that is genuinely part of a translation overlay. ### 4. Shared extension overlay first, language-specific residue later @@ -166,7 +166,7 @@ Stop if you see any of these: - `overlays/variants/extended/.yaml` contains full `card_templates:` blocks for every language. - Template HTML differs only by labels such as `Flag`, `Location`, or `Flag similar...`. - Translation overlays include `notes:` blocks for ordinary field translations. -- Translation overlays use `translations.additions` for extension-owned field content instead of `field_fills`. +- Translation overlays use `translations.target_additions` for extension-owned field content instead of `field_fills`. - Translation overlays path-scope note-type names instead of translating a variable. - `expected_base` refers to the rendered value of a variable-backed source property. - The same extension template exists in more than one language file. From 974a5dfa76dbcf51cf87b0d2802d90608a6b848a Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 13:18:06 +0200 Subject: [PATCH 013/142] refine contextual translation scopes --- crates/brain-brew-core/src/lib.rs | 153 +++++++++++++++++- .../brain-brew-core/tests/overlay_compose.rs | 19 ++- .../extensions/hardcore/translations/de.yaml | 60 +++---- .../extensions/hardcore/translations/es.yaml | 63 +++----- .../extensions/hardcore/translations/fr.yaml | 64 +++----- .../extensions/hardcore/translations/nb.yaml | 60 +++---- .../overlays/languages/cs.yaml | 50 +++--- .../overlays/languages/da.yaml | 28 ++-- .../overlays/languages/de.yaml | 22 +-- .../overlays/languages/fr.yaml | 14 +- .../overlays/languages/it.yaml | 8 +- .../overlays/languages/nb.yaml | 50 +++--- .../overlays/languages/pt.yaml | 18 +-- .../overlays/languages/ru.yaml | 32 ++-- .../overlays/languages/sv.yaml | 44 ++--- .../overlays/languages/zh-tw.yaml | 6 +- .../overlays/languages/zh.yaml | 2 +- 17 files changed, 396 insertions(+), 297 deletions(-) diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs index d7daf7e..c2c294e 100644 --- a/crates/brain-brew-core/src/lib.rs +++ b/crates/brain-brew-core/src/lib.rs @@ -380,6 +380,7 @@ fn apply_translation_dictionary( let mut seen_target_additions = BTreeSet::new(); let mut seen_variables = BTreeSet::new(); let mut seen_adapter_ids = BTreeSet::new(); + let mut source_paths = BTreeMap::>::new(); { let mut context = TranslationApplyContext { @@ -390,6 +391,7 @@ fn apply_translation_dictionary( seen_target_additions: &mut seen_target_additions, seen_variables: &mut seen_variables, seen_adapter_ids: &mut seen_adapter_ids, + source_paths: &mut source_paths, changed_paths, errors, }; @@ -474,7 +476,7 @@ fn apply_translation_dictionary( } } for (context_path, replacements) in &translations.contextual { - for source in replacements.keys() { + for (source, translated) in replacements { if !seen_contextual.contains(&(context_path.clone(), source.clone())) { errors.push(ComposeError::new( ComposeErrorKind::StaleTranslationEntry, @@ -483,6 +485,22 @@ fn apply_translation_dictionary( "invalid contextual translation: source {source:?} did not match any extracted text under {context_path}; the source key may be stale, the context may be invalid, or a blank source field should use translations.target_additions" ), )); + continue; + } + if let Some(shorter_context) = shortest_safe_context( + translations, + &source_paths, + context_path, + source, + translated, + ) { + errors.push(ComposeError::new( + ComposeErrorKind::ValidationFailed, + format!("translations.contextual.{context_path}.{source}"), + format!( + "contextual translation for source {source:?} is more specific than necessary; use context {shorter_context:?} instead of {context_path:?}" + ), + )); } } } @@ -533,6 +551,7 @@ struct TranslationApplyContext<'a, 'b> { seen_target_additions: &'b mut BTreeSet, seen_variables: &'b mut BTreeSet<(String, String)>, seen_adapter_ids: &'b mut BTreeSet<(String, String)>, + source_paths: &'b mut BTreeMap>, changed_paths: &'b mut BTreeMap, errors: &'b mut Vec, } @@ -597,6 +616,10 @@ impl TranslationApplyContext<'_, '_> { } let source = value.clone(); + self.source_paths + .entry(source.clone()) + .or_default() + .insert(path.clone()); let direct_translation = self.translations.direct.get(source.as_str()); if direct_translation.is_some() { self.seen_direct.insert(source.clone()); @@ -689,6 +712,134 @@ impl TranslationApplyContext<'_, '_> { } } +fn shortest_safe_context( + translations: &TranslationDictionary, + source_paths: &BTreeMap>, + context_path: &str, + source: &str, + translated: &str, +) -> Option { + context_parent_candidates(context_path) + .into_iter() + .filter(|candidate| { + contextual_replacement_is_safe( + translations, + source_paths, + candidate, + source, + translated, + ) + }) + .min_by_key(|candidate| candidate.len()) +} + +fn contextual_replacement_is_safe( + translations: &TranslationDictionary, + source_paths: &BTreeMap>, + candidate_context: &str, + source: &str, + translated: &str, +) -> bool { + if translations + .contextual + .get(candidate_context) + .and_then(|replacements| replacements.get(source)) + .is_some_and(|existing| existing != translated) + { + return false; + } + + let Some(paths) = source_paths.get(source) else { + return false; + }; + paths + .iter() + .filter(|path| context_matches_path(candidate_context, path)) + .all(|path| { + let current = direct_or_contextual_translation_for_path(translations, source, path); + let candidate = direct_or_contextual_translation_for_path_with_candidate( + translations, + candidate_context, + source, + translated, + path, + ); + current == candidate + }) +} + +fn direct_or_contextual_translation_for_path<'a>( + translations: &'a TranslationDictionary, + source: &str, + path: &str, +) -> Option<&'a str> { + translations + .contextual + .iter() + .filter(|(context_path, replacements)| { + context_matches_path(context_path, path) && replacements.contains_key(source) + }) + .max_by_key(|(context_path, _)| context_path.len()) + .and_then(|(_, replacements)| replacements.get(source).map(String::as_str)) + .or_else(|| translations.direct.get(source).map(String::as_str)) +} + +fn direct_or_contextual_translation_for_path_with_candidate<'a>( + translations: &'a TranslationDictionary, + candidate_context: &'a str, + source: &str, + translated: &'a str, + path: &str, +) -> Option<&'a str> { + let best_contextual = translations + .contextual + .iter() + .filter(|(context_path, replacements)| { + context_matches_path(context_path, path) && replacements.contains_key(source) + }) + .max_by_key(|(context_path, _)| context_path.len()); + + if context_matches_path(candidate_context, path) + && best_contextual + .is_none_or(|(context_path, _)| candidate_context.len() > context_path.len()) + { + return Some(translated); + } + + best_contextual + .and_then(|(_, replacements)| replacements.get(source).map(String::as_str)) + .or_else(|| translations.direct.get(source).map(String::as_str)) +} + +fn context_parent_candidates(context_path: &str) -> Vec { + if let Some((note_context, _)) = context_path.split_once(".fields.") + && note_context.starts_with("notes.") + { + return vec![note_context.to_owned()]; + } + if let Some((note_context, _)) = context_path.split_once(".tags.") + && note_context.starts_with("notes.") + { + return vec![note_context.to_owned()]; + } + if let Some((note_context, _)) = context_path.split_once(".variables.") + && note_context.starts_with("notes.") + { + return vec![note_context.to_owned()]; + } + if let Some((note_type_context, _)) = context_path.split_once(".fields.") + && note_type_context.starts_with("note_types.") + { + return vec![note_type_context.to_owned()]; + } + if let Some((note_type_context, _)) = context_path.split_once(".card_templates.") + && note_type_context.starts_with("note_types.") + { + return vec![note_type_context.to_owned()]; + } + Vec::new() +} + fn context_matches_path(context_path: &str, path: &str) -> bool { path == context_path || path diff --git a/crates/brain-brew-core/tests/overlay_compose.rs b/crates/brain-brew-core/tests/overlay_compose.rs index e9f54e7..74fd851 100644 --- a/crates/brain-brew-core/tests/overlay_compose.rs +++ b/crates/brain-brew-core/tests/overlay_compose.rs @@ -254,6 +254,9 @@ fn translation_dictionary_distinguishes_direct_contextual_and_target_additions() .fields .insert(sid("field.country"), "Shared source".to_owned()); sweden.fields.insert(sid("field.capital"), String::new()); + sweden + .fields + .insert(sid("field.flag"), "Shared source".to_owned()); base.notes.insert(sid("note.sweden"), sweden); let overlay = Overlay { id: sid("overlay.translation.da"), @@ -306,6 +309,10 @@ fn translation_dictionary_distinguishes_direct_contextual_and_target_additions() resolved.notes[&sid("note.sweden")].fields[&sid("field.capital")], "Stockholm" ); + assert_eq!( + resolved.notes[&sid("note.sweden")].fields[&sid("field.flag")], + "Direkte standard" + ); } #[test] @@ -376,7 +383,7 @@ fn translation_dictionary_reports_missing_direct_translation_when_complete() { } #[test] -fn translation_dictionary_contextual_reports_stale_source_or_context() { +fn translation_dictionary_contextual_reports_unnecessary_specificity() { let base = ug_style_deck(); let overlay = Overlay { id: sid("overlay.translation.da"), @@ -384,7 +391,7 @@ fn translation_dictionary_contextual_reports_stale_source_or_context() { translations: Some(TranslationDictionary { direct: BTreeMap::new(), contextual: BTreeMap::from([( - "notes.note.finland.fields.field.country".to_owned(), + "notes.note.finland.fields.field.capital".to_owned(), BTreeMap::from([("Helsinki".to_owned(), "Helsingfors".to_owned())]), )]), target_additions: BTreeMap::new(), @@ -401,12 +408,12 @@ fn translation_dictionary_contextual_reports_stale_source_or_context() { let report = base .compose(&[overlay]) - .expect_err("contextual translation must match source under context"); + .expect_err("contextual translation should use the shortest safe context"); - assert!(report.has_kind(ComposeErrorKind::StaleTranslationEntry)); + assert!(report.has_kind(ComposeErrorKind::ValidationFailed)); assert!(report.errors.iter().any(|error| { - error.path == "translations.contextual.notes.note.finland.fields.field.country.Helsinki" - && error.message.contains("invalid contextual translation") + error.path == "translations.contextual.notes.note.finland.fields.field.capital.Helsinki" + && error.message.contains("use context \"notes.note.finland\"") })); } diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml index eb7e051..ee130c5 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml @@ -3,67 +3,53 @@ kind: translation translations: contextual: notes.note: - bonaire.fields.field.country-info: + bonaire: Special municipality of the Netherlands.: Besondere Gemeinde der Niederlande. - british-indian-ocean-territory.fields.field.country: + british-indian-ocean-territory: British Indian Ocean Territory: Britisches Territorium im Indischen Ozean - british-indian-ocean-territory.fields.field.country-info: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' - ceuta.fields.field.country-info: + ceuta: Autonomous city of Spain.: Autonome spanische Stadt. - christmas-island.fields.field.country: + christmas-island: Christmas Island: Weihnachtsinsel - christmas-island.fields.field.country-info: External territory of Australia.: 'Australisches Außengebiet.' - cocos-keeling-islands.fields.field.country: + cocos-keeling-islands: 'Cocos (Keeling) Islands': Kokosinseln - cocos-keeling-islands.fields.field.country-info: External territory of Australia.: 'Australisches Außengebiet.' - easter-island.fields.field.country: + easter-island: Easter Island: Osterinsel - easter-island.fields.field.country-info: Polynesian island and special territory of Chile.: Polynesische Insel unter Chilenischer Regierung. - gal-pagos-islands.fields.field.country: + gal-pagos-islands: 'Galápagos Islands': Galapagosinseln - gal-pagos-islands.fields.field.country-info: Province of Ecuador.: Ecuadorianische Provinz. - gal-pagos-islands.fields.field.flag-similarity: 'Sierra Leone (slightly lighter blue)': 'Sierra Leone (etwas helleres Blau)' - melilla.fields.field.country-info: + melilla: Autonomous city of Spain: Autonome spanische Stadt. - montserrat.fields.field.capital-info: + montserrat: Eruptions in 1997 destroyed the capital city of Plymouth, making Brades the de facto capital.: 'Nach einer Reihe von Vulkanausbrüchen wurde die eigentliche Hauptstadt Plymouth 1997 aufgegeben und Brades zur de-facto-Hauptstadt.' - montserrat.fields.field.country-info: Overseas territory of the United Kingdom.: 'Überseegebiet des Vereinigten Königreichs.' - mount-athos.fields.field.country: - Mount Athos: Athos - mount-athos.fields.field.country-info: + mount-athos: Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain.: 'Autonomer orthodoxer Klosterstaat im Nordosten Griechenlands, auch bekannt als „Heilige Berg“.' - norfolk-island.fields.field.capital-hint: - Not a sovereign country: 'Kein souveräner Staat' - norfolk-island.fields.field.country: - Norfolk Island: Norfolkinsel - norfolk-island.fields.field.country-info: + Mount Athos: Athos + norfolk-island: External territory of Australia.: 'Außengebiet von Australien.' - pitcairn-islands.fields.field.country: - Pitcairn Islands: Pitcairninseln - pitcairn-islands.fields.field.country-info: + Norfolk Island: Norfolkinsel + Not a sovereign country: 'Kein souveräner Staat' + pitcairn-islands: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' - saba.fields.field.country-info: + Pitcairn Islands: Pitcairninseln + saba: Special municipality of the Netherlands.: Besondere Gemeinde der Niederlande. - saint-barth-lemy.fields.field.country: - 'Saint Barthélemy': 'Saint-Barthélemy' - saint-barth-lemy.fields.field.country-info: + saint-barth-lemy: Overseas territory of France.: 'Französisches Überseegebiet.' - saint-helena.fields.field.country: - Saint Helena: St. Helena - saint-helena.fields.field.country-info: + 'Saint Barthélemy': 'Saint-Barthélemy' + saint-helena: Part of Saint Helena, Ascension and Tristan da Cunha, an overseas territory of the United Kingdom.: 'Teil von St. Helena, Ascension und Tristan da Cunha, einem Überseegebiet des Vereinigten Königreichs.' - sint-eustatius.fields.field.capital-hint: + Saint Helena: St. Helena + sint-eustatius: Special municipality: Besondere Gemeinde - sint-eustatius.fields.field.country-info: Special municipality of the Netherlands.: Besondere Gemeinde der Niederlande. - tokelau.fields.field.country-info: + tokelau: 'Dependent territory of New Zealand. Tokelau has no official capital; the meeting place of the Tokelau Council rotates annually.': 'Von Neuseeland abhängiges Gebiet. Tokelau hat keine offizielle Hauptstadt; der Treffpunkt des Tokelauer Rates wechselt jährlich.' adapter_ids: crowdanki:guid: diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml index 233ca8e..d5aca1a 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml @@ -3,69 +3,54 @@ kind: translation translations: contextual: notes.note: - bonaire.fields.field.country-info: + bonaire: Special municipality of the Netherlands.: 'Municipio especial de los Países Bajos.' - british-indian-ocean-territory.fields.field.country: + british-indian-ocean-territory: British Indian Ocean Territory: 'Territorio Británico del Océano Índico' - british-indian-ocean-territory.fields.field.country-info: Overseas territory of the United Kingdom.: Territorio de ultramar del Reino Unido. - ceuta.fields.field.country-info: + ceuta: Autonomous city of Spain.: 'Ciudad autónoma de España.' - christmas-island.fields.field.country: + christmas-island: Christmas Island: Isla de Navidad - christmas-island.fields.field.country-info: External territory of Australia.: Territorio externo de Australia. - cocos-keeling-islands.fields.field.country: + cocos-keeling-islands: 'Cocos (Keeling) Islands': Islas Cocos - cocos-keeling-islands.fields.field.country-info: External territory of Australia.: Territorio externo de Australia. - easter-island.fields.field.country: + easter-island: Easter Island: Isla de Pascua - easter-island.fields.field.country-info: Polynesian island and special territory of Chile.: Isla de la Polinesia y territorio especial de Chile. - gal-pagos-islands.fields.field.country: + gal-pagos-islands: 'Galápagos Islands': 'Islas Galápagos' - gal-pagos-islands.fields.field.country-info: Province of Ecuador.: Provincia de Ecuador. - gal-pagos-islands.fields.field.flag-similarity: 'Sierra Leone (slightly lighter blue)': 'Sierra Leona (azul ligeramente más claro)' - melilla.fields.field.country-info: + melilla: Autonomous city of Spain: 'Ciudad autónoma de España.' - montserrat.fields.field.capital-info: + montserrat: Eruptions in 1997 destroyed the capital city of Plymouth, making Brades the de facto capital.: Las erupciones de 1997 destruyeron la capital de Plymouth, haciendo Brades la capital de facto. - montserrat.fields.field.country-info: Overseas territory of the United Kingdom.: Territorio de ultramar del Reino Unido. - mount-athos.fields.field.country: - Mount Athos: Monte Athos - mount-athos.fields.field.country-info: + mount-athos: Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain.: 'Estado monástico ortodoxo autónomo en la parte noreste de Grecia, también conocido como la Montaña Sagrada.' - norfolk-island.fields.field.capital-hint: - Not a sovereign country: 'No es un país soberano' - norfolk-island.fields.field.country: - Norfolk Island: Isla Norfolk - norfolk-island.fields.field.country-info: + Mount Athos: Monte Athos + norfolk-island: External territory of Australia.: Territorio externo de Australia. - pitcairn-islands.fields.field.country: - Pitcairn Islands: Islas Pitcairn - pitcairn-islands.fields.field.country-info: + Norfolk Island: Isla Norfolk + Not a sovereign country: 'No es un país soberano' + pitcairn-islands: Overseas territory of the United Kingdom.: Territorio de ultramar del Reino Unido. - saba.fields.field.country-info: + Pitcairn Islands: Islas Pitcairn + saba: Special municipality of the Netherlands.: 'Municipio especial de los Países Bajos.' - saint-barth-lemy.fields.field.country: - 'Saint Barthélemy': 'San Bartolomé' - saint-barth-lemy.fields.field.country-info: + saint-barth-lemy: Overseas territory of France.: Territorio de ultramar de Francia. - saint-helena.fields.field.country: - Saint Helena: Santa Helena - saint-helena.fields.field.country-info: + 'Saint Barthélemy': 'San Bartolomé' + saint-helena: Part of Saint Helena, Ascension and Tristan da Cunha, an overseas territory of the United Kingdom.: 'Parte de Santa Elena, Ascensión y Tristán de Acuña, un territorio de ultramar del Reino Unido.' - sint-eustatius.fields.field.capital-hint: - Special municipality: Es un municipio especial - sint-eustatius.fields.field.country: + Saint Helena: Santa Helena + sint-eustatius: Sint Eustatius: San Eustaquio - sint-eustatius.fields.field.country-info: + Special municipality: Es un municipio especial Special municipality of the Netherlands.: 'Municipio especial de los Países Bajos.' - tokelau.fields.field.country-info: + tokelau: 'Dependent territory of New Zealand. Tokelau has no official capital; the meeting place of the Tokelau Council rotates annually.': 'Territorio dependiente de Nueva Zelanda. Tokelau no tiene capital oficial; el sitio de encuentro del Consejo de Tokelau rota anualmente.' adapter_ids: crowdanki:guid: diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml index ffd4925..7f607a1 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml @@ -3,71 +3,55 @@ kind: translation translations: contextual: notes.note: - bonaire.fields.field.country-info: + bonaire: Special municipality of the Netherlands.: 'Commune des Pays-Bas à statut particulier.' - british-indian-ocean-territory.fields.field.country: + british-indian-ocean-territory: British Indian Ocean Territory: 'Territoire britannique de l''océan Indien' - british-indian-ocean-territory.fields.field.country-info: Overseas territory of the United Kingdom.: 'Territoire britannique d''outre-mer.' - ceuta.fields.field.country-info: + ceuta: Autonomous city of Spain.: 'Ville autonome d’Espagne.' - christmas-island.fields.field.country: + christmas-island: Christmas Island: 'Île Christmas' - christmas-island.fields.field.country-info: External territory of Australia.: 'Territoire extérieur australien.' - cocos-keeling-islands.fields.field.country: + cocos-keeling-islands: 'Cocos (Keeling) Islands': 'Îles Cocos' - cocos-keeling-islands.fields.field.country-info: External territory of Australia.: 'Territoire extérieur australien.' - easter-island.fields.field.country: + easter-island: Easter Island: 'Île de Pâques' - easter-island.fields.field.country-info: Polynesian island and special territory of Chile.: 'Île polynésienne et territoire spécial du Chili.' - gal-pagos-islands.fields.field.country: + gal-pagos-islands: 'Galápagos Islands': 'Îles Galápagos' - gal-pagos-islands.fields.field.country-info: Province of Ecuador.: 'Province de l''Équateur.' - gal-pagos-islands.fields.field.flag-similarity: 'Sierra Leone (slightly lighter blue)': 'Sierra Leone (bleu légèrement plus clair)' - melilla.fields.field.country-info: + melilla: Autonomous city of Spain: 'Ville autonome d’Espagne.' - montserrat.fields.field.capital-info: + montserrat: Eruptions in 1997 destroyed the capital city of Plymouth, making Brades the de facto capital.: 'Des éruptions en 1997 ont détruit la capitale Plymouth, faisant de Brades la capitale de fait.' - montserrat.fields.field.country-info: Overseas territory of the United Kingdom.: 'Territoire britannique d''outre-mer.' - mount-athos.fields.field.capital: + mount-athos: + Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain.: 'République monastique autonome au nord-est de la Grêce, aussi connue comme la Sainte-Montagne.' Karyes: 'Karyès' - mount-athos.fields.field.country: Mount Athos: Mont Athos - mount-athos.fields.field.country-info: - Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain.: 'République monastique autonome au nord-est de la Grêce, aussi connue comme la Sainte-Montagne.' - norfolk-island.fields.field.capital-hint: - Not a sovereign country: Pas une nation souveraine - norfolk-island.fields.field.country: - Norfolk Island: 'Île Norfolk' - norfolk-island.fields.field.country-info: + norfolk-island: External territory of Australia.: 'Territoire extérieur australien.' - pitcairn-islands.fields.field.country: - Pitcairn Islands: 'Îles Pitcairn' - pitcairn-islands.fields.field.country-info: + Norfolk Island: 'Île Norfolk' + Not a sovereign country: Pas une nation souveraine + pitcairn-islands: Overseas territory of the United Kingdom.: 'Territoire britannique d''outre-mer.' - saba.fields.field.country-info: + Pitcairn Islands: 'Îles Pitcairn' + saba: Special municipality of the Netherlands.: 'Commune des Pays-Bas à statut particulier.' - saint-barth-lemy.fields.field.country: - 'Saint Barthélemy': 'Saint-Barthélemy' - saint-barth-lemy.fields.field.country-info: + saint-barth-lemy: Overseas territory of France.: 'Collectivité française d''outre-mer.' - saint-helena.fields.field.country: - Saint Helena: 'Sainte-Hélène' - saint-helena.fields.field.country-info: + 'Saint Barthélemy': 'Saint-Barthélemy' + saint-helena: Part of Saint Helena, Ascension and Tristan da Cunha, an overseas territory of the United Kingdom.: 'Fait partie de Sainte-Hélène, Ascension et Tristan da Cunha, un territoire britannique d''outre-mer.' - sint-eustatius.fields.field.capital-hint: - Special municipality: 'Commune à statut particulier' - sint-eustatius.fields.field.country: + Saint Helena: 'Sainte-Hélène' + sint-eustatius: Sint Eustatius: Saint-Eustache - sint-eustatius.fields.field.country-info: + Special municipality: 'Commune à statut particulier' Special municipality of the Netherlands.: 'Commune des Pays-Bas à statut particulier.' - tokelau.fields.field.country-info: + tokelau: 'Dependent territory of New Zealand. Tokelau has no official capital; the meeting place of the Tokelau Council rotates annually.': 'Territoire dépendant de la Nouvelle-Zélande. La capitale, de même que la présidence du Conseil permanent du Gouvernement, tournent entre les trois atolls.' adapter_ids: crowdanki:guid: diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml index 9ba0cf5..a8dc730 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml @@ -3,67 +3,53 @@ kind: translation translations: contextual: notes.note: - bonaire.fields.field.country-info: + bonaire: Special municipality of the Netherlands.: Kommune med spesiell status i Nederland. - british-indian-ocean-territory.fields.field.country: + british-indian-ocean-territory: British Indian Ocean Territory: Det britiske territoriet i Indiahavet - british-indian-ocean-territory.fields.field.country-info: Overseas territory of the United Kingdom.: Britisk besittelse. - ceuta.fields.field.country-info: + ceuta: Autonomous city of Spain.: Selvstyrt by i Spania. - christmas-island.fields.field.country: + christmas-island: Christmas Island: 'Christmasøya' - christmas-island.fields.field.country-info: External territory of Australia.: 'Australsk territorium. ' - cocos-keeling-islands.fields.field.country: + cocos-keeling-islands: 'Cocos (Keeling) Islands': 'Kokosøyene' - cocos-keeling-islands.fields.field.country-info: External territory of Australia.: Australsk territorium. - easter-island.fields.field.country: + easter-island: Easter Island: 'Påskeøya' - easter-island.fields.field.country-info: Polynesian island and special territory of Chile.: Territorium i Chile. - gal-pagos-islands.fields.field.country: + gal-pagos-islands: 'Galápagos Islands': 'Galápagosøyene' - gal-pagos-islands.fields.field.country-info: Province of Ecuador.: Region i Ecuador. - gal-pagos-islands.fields.field.flag-similarity: 'Sierra Leone (slightly lighter blue)': 'Sierra Leone (noe lysere blå)' - melilla.fields.field.country-info: + melilla: Autonomous city of Spain: 'Spansk by og enklave på Marokkos middelhavskyst.' - montserrat.fields.field.capital-info: + montserrat: Eruptions in 1997 destroyed the capital city of Plymouth, making Brades the de facto capital.: 'Vulkanutbrudd i 1997 ødela hovedstaden Plymouth, og Brades ble de facto hovedstad.' - montserrat.fields.field.country-info: Overseas territory of the United Kingdom.: 'Britisk oversjøisk territorium i Karibia.' - mount-athos.fields.field.country: - Mount Athos: Athos - mount-athos.fields.field.country-info: + mount-athos: Autonomous orthodox monastic state in northeastern Greece, also known as the Holy Mountain.: 'Autonom munkerepublikk i Nord-Hellas. Også kalt Det hellige berget.' - norfolk-island.fields.field.capital-hint: - Not a sovereign country: Ikke selvstendig land - norfolk-island.fields.field.country: - Norfolk Island: 'Norfolkøya' - norfolk-island.fields.field.country-info: + Mount Athos: Athos + norfolk-island: External territory of Australia.: Selvstyrt australsk territorium. - pitcairn-islands.fields.field.country: - Pitcairn Islands: 'Pitcairnøyene' - pitcairn-islands.fields.field.country-info: + Norfolk Island: 'Norfolkøya' + Not a sovereign country: Ikke selvstendig land + pitcairn-islands: Overseas territory of the United Kingdom.: 'Oversjøisk britisk territorium.' - saba.fields.field.country-info: + Pitcairn Islands: 'Pitcairnøyene' + saba: Special municipality of the Netherlands.: 'Kommune med særstatus i Nederland. Øy i Karibia.' - saint-barth-lemy.fields.field.country: - 'Saint Barthélemy': 'Saint-Barthélemy' - saint-barth-lemy.fields.field.country-info: + saint-barth-lemy: Overseas territory of France.: 'Fransk oversjøisk land.' - saint-helena.fields.field.country: - Saint Helena: Sankt Helena - saint-helena.fields.field.country-info: + 'Saint Barthélemy': 'Saint-Barthélemy' + saint-helena: Part of Saint Helena, Ascension and Tristan da Cunha, an overseas territory of the United Kingdom.: 'Del av det britiske oversjøiske territoriet Sankt Helena, Ascencion og Tristan da Cunha.' - sint-eustatius.fields.field.capital-hint: + Saint Helena: Sankt Helena + sint-eustatius: Special municipality: 'Kommune med særstatus' - sint-eustatius.fields.field.country-info: Special municipality of the Netherlands.: 'Kommune med særstatus i Nederland.' - tokelau.fields.field.country-info: + tokelau: 'Dependent territory of New Zealand. Tokelau has no official capital; the meeting place of the Tokelau Council rotates annually.': Newzealandsk territorium. adapter_ids: crowdanki:guid: diff --git a/fixtures/ultimate-geography/overlays/languages/cs.yaml b/fixtures/ultimate-geography/overlays/languages/cs.yaml index 401579c..5321e31 100644 --- a/fixtures/ultimate-geography/overlays/languages/cs.yaml +++ b/fixtures/ultimate-geography/overlays/languages/cs.yaml @@ -439,55 +439,55 @@ translations: 'Åland Islands': Alandy contextual: notes.note: - abkhazia.fields.field.country-info: + abkhazia: Independent state claimed by Georgia.: 'Nezávislý stát nárokováný Gruzií.' - akrotiri-and-dhekelia.fields.field.country-info: + akrotiri-and-dhekelia: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' - anguilla.fields.field.country-info: + anguilla: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie s autonomní správou.' - azores.fields.field.country-info: + azores: Autonomous region of Portugal.: 'Autonomní území Portugalska.' - bali.fields.field.country-info: + bali: Island of Indonesia.: 'Indonéský ostrov.' - bermuda.fields.field.country-info: + bermuda: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' - british-virgin-islands.fields.field.country-info: + british-virgin-islands: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' - cayman-islands.fields.field.country-info: + cayman-islands: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' + djibouti: + Djibouti: 'Džibutsko' djibouti.fields.field.capital: Djibouti: 'Džibuti' - djibouti.fields.field.country: - Djibouti: 'Džibutsko' - falkland-islands.fields.field.country-info: + falkland-islands: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' - faroe-islands.fields.field.country-info: + faroe-islands: Constituent country in the Kingdom of Denmark.: 'Konstituční země Dánského království.' - french-polynesia.fields.field.country-info: + french-polynesia: Overseas territory of France.: 'Zámořské území Francie.' - gibraltar.fields.field.country-info: + gibraltar: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' - greenland.fields.field.country-info: + greenland: Constituent country in the Kingdom of Denmark.: 'Autonomní součást Dánského království.' - java.fields.field.country-info: + java: Island of Indonesia.: 'Indonéský ostrov.' - madeira.fields.field.country-info: + madeira: Autonomous region of Portugal.: 'Autonomní region Portugalska.' + monaco: + Monaco: Monako monaco.fields.field.capital: Monaco: Monaco-Ville - monaco.fields.field.country: - Monaco: Monako - new-caledonia.fields.field.country-info: + new-caledonia: Overseas territory of France.: 'Zámořské území Francie.' - saint-martin.fields.field.country-info: + saint-martin: Overseas territory of France.: 'Konstituční země Francie.' - south-ossetia.fields.field.country-info: + south-ossetia: Independent state claimed by Georgia.: 'Nezávislý stát nárokovaný Gruzií.' - sumatra.fields.field.country-info: + sumatra: Island of Indonesia.: 'Ostrov Indonésie.' - turks-and-caicos-islands.fields.field.country-info: + turks-and-caicos-islands: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' - wallis-and-futuna.fields.field.country-info: + wallis-and-futuna: Overseas territory of France.: 'Zámořské území Francie.' target_additions: notes.note.pacific-ocean.fields.field.country-info: 'Známý též jako Pacifik.' diff --git a/fixtures/ultimate-geography/overlays/languages/da.yaml b/fixtures/ultimate-geography/overlays/languages/da.yaml index 331b743..e93127f 100644 --- a/fixtures/ultimate-geography/overlays/languages/da.yaml +++ b/fixtures/ultimate-geography/overlays/languages/da.yaml @@ -344,33 +344,33 @@ translations: 'Åland Islands': 'Åland' contextual: notes.note: - akrotiri-and-dhekelia.fields.field.country-info: + akrotiri-and-dhekelia: Overseas territory of the United Kingdom.: 'Britisk oversøisk territorie.' - anguilla.fields.field.country-info: + anguilla: Overseas territory of the United Kingdom.: 'Oversøisk territorie i Storbritannien.' - azores.fields.field.country-info: + azores: Autonomous region of Portugal.: 'Selvstændig region Autonomous region i Portugal.' - bermuda.fields.field.country-info: + bermuda: Overseas territory of the United Kingdom.: Delvis selvstyrende nation i Storbritannien. - british-virgin-islands.fields.field.country-info: + british-virgin-islands: Overseas territory of the United Kingdom.: 'Oversøisk territorie i Storbritannien.' - cayman-islands.fields.field.country-info: + cayman-islands: Overseas territory of the United Kingdom.: 'Oversøisk territorie i Storbritannien.' - falkland-islands.fields.field.country-info: + falkland-islands: Overseas territory of the United Kingdom.: 'Britisk oversøisk territorie.' - french-polynesia.fields.field.country-info: + french-polynesia: Overseas territory of France.: 'Fransk oversøisk territorie.' - gibraltar.fields.field.country-info: + gibraltar: Overseas territory of the United Kingdom.: 'Oversøisk territorie i Storbritannien.' - madeira.fields.field.country-info: + madeira: Autonomous region of Portugal.: 'Selvstændig region i Portugal.' - new-caledonia.fields.field.country-info: + new-caledonia: Overseas territory of France.: 'Fransk oversøisk departement.' - saint-martin.fields.field.country-info: + saint-martin: Overseas territory of France.: 'Fransk oversøisk departement.' - turks-and-caicos-islands.fields.field.country-info: + turks-and-caicos-islands: Overseas territory of the United Kingdom.: 'Oversøisk territorie i Storbritannien.' - wallis-and-futuna.fields.field.country-info: + wallis-and-futuna: Overseas territory of France.: 'Fransk oversøisk departement.' target_additions: notes.note.arctic-ocean.fields.field.country-info: 'Også kendt som Det Arktiske Ocean.' diff --git a/fixtures/ultimate-geography/overlays/languages/de.yaml b/fixtures/ultimate-geography/overlays/languages/de.yaml index 23795c8..6815027 100644 --- a/fixtures/ultimate-geography/overlays/languages/de.yaml +++ b/fixtures/ultimate-geography/overlays/languages/de.yaml @@ -380,27 +380,27 @@ translations: 'Åland Islands': 'Åland' contextual: notes.note: - akrotiri-and-dhekelia.fields.field.country-info: + akrotiri-and-dhekelia: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' - anguilla.fields.field.country-info: + anguilla: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' - bali.fields.field.country-info: + bali: Island of Indonesia.: Insel von Indonesien. - bermuda.fields.field.country-info: + bermuda: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' - british-virgin-islands.fields.field.country-info: + british-virgin-islands: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' - cayman-islands.fields.field.country-info: + cayman-islands: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' - falkland-islands.fields.field.country-info: + falkland-islands: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' - gibraltar.fields.field.country-info: + gibraltar: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' - java.fields.field.country-info: + java: Island of Indonesia.: Insel von Indonesien. - sumatra.fields.field.country-info: + sumatra: Island of Indonesia.: Indonesische Insel. - turks-and-caicos-islands.fields.field.country-info: + turks-and-caicos-islands: Overseas territory of the United Kingdom.: 'Auch Turks & Caicos. Britisches Überseegebiet.' target_additions: notes.note.aegean-sea.fields.field.country-info: 'Auch Ägäis.' diff --git a/fixtures/ultimate-geography/overlays/languages/fr.yaml b/fixtures/ultimate-geography/overlays/languages/fr.yaml index 536fb9d..43e348b 100644 --- a/fixtures/ultimate-geography/overlays/languages/fr.yaml +++ b/fixtures/ultimate-geography/overlays/languages/fr.yaml @@ -421,19 +421,19 @@ translations: 'Åland Islands': 'Åland' contextual: notes.note: - bali.fields.field.country-info: + bali: Island of Indonesia.: 'Île d''Indonésie.' - french-polynesia.fields.field.country-info: + french-polynesia: Overseas territory of France.: 'Collectivité française d''outre-mer.' - java.fields.field.country-info: + java: Island of Indonesia.: 'Île d''Indonésie.' - new-caledonia.fields.field.country-info: + new-caledonia: Overseas territory of France.: 'Territoire français d''outre-mer.' - saint-martin.fields.field.country-info: + saint-martin: Overseas territory of France.: 'Collectivité française d''outre-mer.' - sumatra.fields.field.country-info: + sumatra: Island of Indonesia.: 'Île indonésienne.' - wallis-and-futuna.fields.field.country-info: + wallis-and-futuna: Overseas territory of France.: 'Collectivité française d''outre-mer.' target_additions: notes.note.celebes-sea.fields.field.country-info: Aussi connu comme la mer de Sulawesi. diff --git a/fixtures/ultimate-geography/overlays/languages/it.yaml b/fixtures/ultimate-geography/overlays/languages/it.yaml index 0562aec..2702c2b 100644 --- a/fixtures/ultimate-geography/overlays/languages/it.yaml +++ b/fixtures/ultimate-geography/overlays/languages/it.yaml @@ -367,14 +367,14 @@ translations: 'Åland Islands': 'Isole Åland' contextual: notes.note: - faroe-islands.fields.field.country-info: + faroe-islands: Constituent country in the Kingdom of Denmark.: 'Nazione costitutiva del Regno di Danimarca; conosciute anche come Isole Faroe.' - greenland.fields.field.country-info: + greenland: Constituent country in the Kingdom of Denmark.: Nazione costitutiva del Regno di Danimarca. + monaco: + Monaco: Principato di Monaco monaco.fields.field.capital: Monaco: Comune di Monaco - monaco.fields.field.country: - Monaco: Principato di Monaco target_additions: notes.note.arctic-ocean.fields.field.country-info: Conosciuto a livello internazionale come Oceano Artico. variables: diff --git a/fixtures/ultimate-geography/overlays/languages/nb.yaml b/fixtures/ultimate-geography/overlays/languages/nb.yaml index c3c8eb5..ccc8908 100644 --- a/fixtures/ultimate-geography/overlays/languages/nb.yaml +++ b/fixtures/ultimate-geography/overlays/languages/nb.yaml @@ -321,55 +321,55 @@ translations: 'Åland Islands': 'Åland' contextual: notes.note: - akrotiri-and-dhekelia.fields.field.country-info: + akrotiri-and-dhekelia: Overseas territory of the United Kingdom.: 'Britisk oversjøisk territorium.' - anguilla.fields.field.country-info: + anguilla: Overseas territory of the United Kingdom.: 'Selvstyrt britisk oversjøisk territorium i Karibia.' - aruba.fields.field.country-info: + aruba: Constituent country of the Kingdom of the Netherlands.: '«Land» som sammen med Nederland, Curaçao og Sint Maarten utgjør føderasjonen Kongeriket Nederlandene.' - bermuda.fields.field.country-info: + bermuda: Overseas territory of the United Kingdom.: 'Selvstyrt britisk oversjøisk territorium.' - british-virgin-islands.fields.field.country-info: + british-virgin-islands: Overseas territory of the United Kingdom.: 'Selvstyrt britisk oversjøisk territorium i Karibia.' - cayman-islands.fields.field.country-info: + cayman-islands: Overseas territory of the United Kingdom.: 'Selvstyrt britisk oversjøisk territorium i Karibia.' - cura-ao.fields.field.country-info: + cura-ao: Constituent country of the Kingdom of the Netherlands.: '«Land» som sammen med Nederland, Aruba og Sint Maarten utgjør føderasjonen Kongeriket Nederlandene.' - falkland-islands.fields.field.country-info: + falkland-islands: Overseas territory of the United Kingdom.: 'Britisk øygruppe.' - french-guiana.fields.field.country-info: + french-guiana: Overseas department of France.: 'Fransk oversjøisk departement og del av EU.' - french-polynesia.fields.field.country-info: + french-polynesia: Overseas territory of France.: 'Fransk oversjøisk land.' - gibraltar.fields.field.country-info: + gibraltar: Overseas territory of the United Kingdom.: Britisk kronkoloni. - guadeloupe.fields.field.country-info: + guadeloupe: Overseas department of France.: 'Fransk oversjøisk departement.' - guernsey.fields.field.country-info: + guernsey: Crown dependency of the United Kingdom.: 'Kanaløy underlagt britisk kronbesittelse.' - hong-kong.fields.field.country-info: + hong-kong: Special Administrative Region of China.: 'Også kjent som Hong Kong. Spesiell administrativ region i Kina.' - isle-of-man.fields.field.country-info: + isle-of-man: Crown dependency of the United Kingdom.: 'Også kalt Isle of Man. Kanaløy underlagt britisk kronbesittelse. Hverken del av Storbritannia eller EU.' - jersey.fields.field.country-info: + jersey: Crown dependency of the United Kingdom.: 'Kanaløy underlagt britisk kronbesittelse.' - macau.fields.field.country-info: + macau: Special Administrative Region of China.: 'Også kalt Macau. Spesiell administrativ region i Kina.' - martinique.fields.field.country-info: + martinique: Overseas department of France.: 'Fransk oversjøisk departement.' - mayotte.fields.field.country-info: + mayotte: Overseas department of France.: 'Fransk oversjøisk territorium.' - new-caledonia.fields.field.country-info: + new-caledonia: Overseas territory of France.: 'Fransk oversjøisk territorium i det sørvestlige Stillehavet.' - r-union.fields.field.country-info: + r-union: Overseas department of France.: 'Fransk oversjøisk departement.' - saint-martin.fields.field.country-info: + saint-martin: Overseas territory of France.: 'Fransk oversjøisk land.' - sint-maarten.fields.field.country-info: + sint-maarten: Constituent country of the Kingdom of the Netherlands.: '«Land» som sammen med Nederland, Curaçao og Aruba utgjør føderasjonen Kongeriket Nederlandene.' - turks-and-caicos-islands.fields.field.country-info: + turks-and-caicos-islands: Overseas territory of the United Kingdom.: 'Selvstyrt britisk oversjøisk territorium i Karibia.' - wallis-and-futuna.fields.field.country-info: + wallis-and-futuna: Overseas territory of France.: 'Fransk oversjøisk land.' target_additions: notes.note.arabian-sea.fields.field.country-info: 'Også kalt Arabiahavet.' diff --git a/fixtures/ultimate-geography/overlays/languages/pt.yaml b/fixtures/ultimate-geography/overlays/languages/pt.yaml index cebcb28..0709fe5 100644 --- a/fixtures/ultimate-geography/overlays/languages/pt.yaml +++ b/fixtures/ultimate-geography/overlays/languages/pt.yaml @@ -522,23 +522,23 @@ translations: 'Åland Islands': Alanda contextual: notes.note: - azores.fields.field.country-info: + azores: Autonomous region of Portugal.: 'Região autônoma de Portugal.' - french-polynesia.fields.field.country-info: + french-polynesia: Overseas territory of France.: 'Território ultramarino da França.' - guernsey.fields.field.country-info: + guernsey: Crown dependency of the United Kingdom.: 'Dependência da coroa do Reino Unido.' - isle-of-man.fields.field.country-info: + isle-of-man: Crown dependency of the United Kingdom.: Ilha de Man - jersey.fields.field.country-info: + jersey: Crown dependency of the United Kingdom.: 'Dependência da coroa do Reino Unido.' - madeira.fields.field.country-info: + madeira: Autonomous region of Portugal.: 'Região Autônoma de Portugal.' - new-caledonia.fields.field.country-info: + new-caledonia: Overseas territory of France.: 'Território ultramarino da França.' - saint-martin.fields.field.country-info: + saint-martin: Overseas territory of France.: 'País constituinte do Reino dos Países Baixos.' - wallis-and-futuna.fields.field.country-info: + wallis-and-futuna: Overseas territory of France.: 'Território ultramarino da França.' target_additions: notes.note.antigua-and-barbuda.fields.field.capital-hint: Estado soberano diff --git a/fixtures/ultimate-geography/overlays/languages/ru.yaml b/fixtures/ultimate-geography/overlays/languages/ru.yaml index 1c7a007..a7f4191 100644 --- a/fixtures/ultimate-geography/overlays/languages/ru.yaml +++ b/fixtures/ultimate-geography/overlays/languages/ru.yaml @@ -644,37 +644,37 @@ translations: 'Åland Islands': 'Аландские острова' contextual: notes.note: - akrotiri-and-dhekelia.fields.field.country-info: + akrotiri-and-dhekelia: Overseas territory of the United Kingdom.: 'Заморская территория Великобритании.' - anguilla.fields.field.country-info: + anguilla: Overseas territory of the United Kingdom.: 'Заморская территория Соединенного Королевства.' - aruba.fields.field.country-info: + aruba: Constituent country of the Kingdom of the Netherlands.: 'Страна-учредитель Королевства Нидерландов.' - bermuda.fields.field.country-info: + bermuda: Overseas territory of the United Kingdom.: 'Заморская территория Соединенного Королевства.' - british-virgin-islands.fields.field.country-info: + british-virgin-islands: Overseas territory of the United Kingdom.: 'Заморская территория Великобритании.' - cayman-islands.fields.field.country-info: + cayman-islands: Overseas territory of the United Kingdom.: 'Заморская территория Соединенного Королевства.' - cura-ao.fields.field.country-info: + cura-ao: Constituent country of the Kingdom of the Netherlands.: 'Самоуправляемое государственное образование, составная часть Королевства Нидерландов.' - falkland-islands.fields.field.country-info: + falkland-islands: Overseas territory of the United Kingdom.: 'Зарубежная территория Великобритании.' - gibraltar.fields.field.country-info: + gibraltar: Overseas territory of the United Kingdom.: 'Заморская территория Великобритании.' - guernsey.fields.field.country-info: + guernsey: Crown dependency of the United Kingdom.: 'Зависимость короны от Соединенного Королевства.' - isle-of-man.fields.field.country-info: + isle-of-man: Crown dependency of the United Kingdom.: 'Зависимость Соединенного Королевства от короны.' - jersey.fields.field.country-info: + jersey: Crown dependency of the United Kingdom.: 'Зависимость Соединенного Королевства от короны.' - sardinia.fields.field.country-info: + sardinia: Autonomous region of Italy.: 'Автономная область Италии.' - sicily.fields.field.country-info: + sicily: Autonomous region of Italy.: 'Автономный регион Италии.' - sint-maarten.fields.field.country-info: + sint-maarten: Constituent country of the Kingdom of the Netherlands.: 'Страна, входящая в состав Королевства Нидерландов.' - turks-and-caicos-islands.fields.field.country-info: + turks-and-caicos-islands: Overseas territory of the United Kingdom.: 'Заморская территория Соединенного Королевства.' target_additions: notes.note.english-channel.fields.field.country-info: 'Также известен как Английский канал' diff --git a/fixtures/ultimate-geography/overlays/languages/sv.yaml b/fixtures/ultimate-geography/overlays/languages/sv.yaml index b3db9bf..8ddbc6a 100644 --- a/fixtures/ultimate-geography/overlays/languages/sv.yaml +++ b/fixtures/ultimate-geography/overlays/languages/sv.yaml @@ -347,51 +347,51 @@ translations: 'Åland Islands': 'Åland' contextual: notes.note: - akrotiri-and-dhekelia.fields.field.country-info: + akrotiri-and-dhekelia: Overseas territory of the United Kingdom.: 'Brittiska “suveräna basområden”.' - anguilla.fields.field.country-info: + anguilla: Overseas territory of the United Kingdom.: Brittiskt utomeuropeiskt territorium. - azores.fields.field.country-info: + azores: Autonomous region of Portugal.: 'Portugisisk autonom ögrupp.' - bermuda.fields.field.country-info: + bermuda: Overseas territory of the United Kingdom.: Brittiskt utomeuropeiskt territorium. - british-virgin-islands.fields.field.country-info: + british-virgin-islands: Overseas territory of the United Kingdom.: Brittiskt utomeuropeiskt territorium. - cayman-islands.fields.field.country-info: + cayman-islands: Overseas territory of the United Kingdom.: Brittiskt utomeuropeiskt territorium. - falkland-islands.fields.field.country-info: + falkland-islands: Overseas territory of the United Kingdom.: Brittiskt utomeuropeiskt territorium. - french-guiana.fields.field.country-info: + french-guiana: Overseas department of France.: Franskt utomeuropeiskt departement. - french-polynesia.fields.field.country-info: + french-polynesia: Overseas territory of France.: 'Franskt utomeuropeiskt förvaltningsområde.' - gibraltar.fields.field.country-info: + gibraltar: Overseas territory of the United Kingdom.: Brittiskt territorium. - guadeloupe.fields.field.country-info: + guadeloupe: Overseas department of France.: Utomeuropeiskt franskt departement. - hong-kong.fields.field.country-info: + hong-kong: Special Administrative Region of China.: 'Särskild administrativ region i Kina.' - macau.fields.field.country-info: + macau: Special Administrative Region of China.: 'Särskild administrativ region i Kina, stavas alternativt Macau.' - madeira.fields.field.country-info: + madeira: Autonomous region of Portugal.: Portugisisk autonom region. - martinique.fields.field.country-info: + martinique: Overseas department of France.: Franskt utomeuropeiskt departement. - mayotte.fields.field.country-info: + mayotte: Overseas department of France.: Franskt utomeuropeiskt departement. - new-caledonia.fields.field.country-info: + new-caledonia: Overseas territory of France.: Franskt utomeuropeiskt territorium. - r-union.fields.field.country-info: + r-union: Overseas department of France.: Franskt utomeuropeiskt departement. - saint-martin.fields.field.country-info: + saint-martin: Overseas territory of France.: 'Franskt utomeuropeiskt område.' - turks-and-caicos-islands.fields.field.country-info: + turks-and-caicos-islands: Overseas territory of the United Kingdom.: Brittiskt utomeuropeiskt territorium. vatican-city.fields.field.capital: Vatican City: Vatikanstaden - vatican-city.fields.field.country: + vatican-city: Vatican City: Vatikanstaten - wallis-and-futuna.fields.field.country-info: + wallis-and-futuna: Overseas territory of France.: 'Franskt utomeuropeiskt område.' target_additions: notes.note.arabian-sea.fields.field.country-info: 'Även känt som Arabiska sjön.' diff --git a/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml b/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml index fd33477..f7cdf0e 100644 --- a/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml +++ b/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml @@ -646,13 +646,13 @@ translations: 'Åland Islands': '奧蘭群島 (Åland Islands)' contextual: notes.note: - abkhazia.fields.field.country-info: + abkhazia: Independent state claimed by Georgia.: '喬治亞宣稱擁有該地區主權,但目前是獨立狀態。' djibouti.fields.field.capital: Djibouti: '吉布地 (Djibouti)' - djibouti.fields.field.country: + djibouti: Djibouti: '吉布地共和國 (Djibouti)' - south-ossetia.fields.field.country-info: + south-ossetia: Independent state claimed by Georgia.: '從喬治亞獨立。' target_additions: notes.note.kiribati.fields.field.capital-info: '拉巴斯正在建造來取代馬拉波成為首都。' diff --git a/fixtures/ultimate-geography/overlays/languages/zh.yaml b/fixtures/ultimate-geography/overlays/languages/zh.yaml index e7eee65..7644fee 100644 --- a/fixtures/ultimate-geography/overlays/languages/zh.yaml +++ b/fixtures/ultimate-geography/overlays/languages/zh.yaml @@ -649,7 +649,7 @@ translations: notes.note: vatican-city.fields.field.capital: Vatican City: '梵蒂冈城(Vatican City)' - vatican-city.fields.field.country: + vatican-city: Vatican City: '梵蒂冈(Vatican City)' target_additions: notes.note.myanmar.fields.field.capital-info: '缅甸于2005年11月6日首都从仰光(Yangon)迁往内比都(Naypyidaw)。' From 1b3525315d9ef29878ce7a27cac4b124b75adaae Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 13:55:42 +0200 Subject: [PATCH 014/142] add translation coverage workflow --- crates/brain-brew-cli/src/args.rs | 21 + crates/brain-brew-cli/src/commands/mod.rs | 1 + .../src/commands/translations.rs | 608 ++++++++++++++++++ crates/brain-brew-cli/src/commands/verify.rs | 59 +- crates/brain-brew-cli/src/help.rs | 7 +- crates/brain-brew-cli/src/main.rs | 1 + crates/brain-brew-cli/tests/cli.rs | 164 +++++ crates/brain-brew-core/src/lib.rs | 413 ++++++++++++ .../brain-brew-core/tests/overlay_compose.rs | 151 ++++- crates/brain-brew-formats/src/manifest.rs | 68 +- .../brain-brew-formats/tests/manifest_yaml.rs | 39 ++ .../docs/authoring/manifests-targets.md | 18 + documentation/docs/authoring/translations.md | 31 +- documentation/docs/reference/yaml.md | 1 + 14 files changed, 1567 insertions(+), 15 deletions(-) create mode 100644 crates/brain-brew-cli/src/commands/translations.rs diff --git a/crates/brain-brew-cli/src/args.rs b/crates/brain-brew-cli/src/args.rs index 2fb20f2..1bcd1cb 100644 --- a/crates/brain-brew-cli/src/args.rs +++ b/crates/brain-brew-cli/src/args.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use brain_brew_core::{OverlayKind, StableId}; +use brain_brew_formats::manifest::TranslationCoveragePolicy; pub(crate) struct ManifestTargetArgs { pub(crate) manifest_path: PathBuf, @@ -18,6 +19,7 @@ pub(crate) struct VerifyArgs { pub(crate) media_root: Option, pub(crate) include_paths: Vec, pub(crate) package_roots: Vec, + pub(crate) translation_coverage: Option, } pub(crate) struct ExportArgs { @@ -158,6 +160,7 @@ pub(crate) fn parse_verify_args(args: &[String]) -> Result { let mut media_root = None; let mut include_paths = Vec::new(); let mut package_roots = Vec::new(); + let mut translation_coverage = None; let mut index = 0; while index < args.len() { match args[index].as_str() { @@ -200,6 +203,13 @@ pub(crate) fn parse_verify_args(args: &[String]) -> Result { package_roots.push(PathBuf::from(path)); index += 2; } + "--translation-coverage" => { + let Some(policy) = args.get(index + 1) else { + return Err("--translation-coverage requires lenient or strict".to_owned()); + }; + translation_coverage = Some(parse_translation_coverage_policy(policy)?); + index += 2; + } other => return Err(format!("unexpected verify argument {other:?}")), } } @@ -213,9 +223,20 @@ pub(crate) fn parse_verify_args(args: &[String]) -> Result { media_root, include_paths, package_roots, + translation_coverage, }) } +fn parse_translation_coverage_policy(value: &str) -> Result { + match value { + "lenient" => Ok(TranslationCoveragePolicy::Lenient), + "strict" => Ok(TranslationCoveragePolicy::Strict), + other => Err(format!( + "invalid translation coverage policy {other:?}; expected lenient or strict" + )), + } +} + pub(crate) fn parse_overlay_and_optional_out( args: &[String], ) -> Result<(Vec, Option), String> { diff --git a/crates/brain-brew-cli/src/commands/mod.rs b/crates/brain-brew-cli/src/commands/mod.rs index d9fc279..41cfb62 100644 --- a/crates/brain-brew-cli/src/commands/mod.rs +++ b/crates/brain-brew-cli/src/commands/mod.rs @@ -6,5 +6,6 @@ pub(crate) mod fmt; pub(crate) mod import; pub(crate) mod lock; pub(crate) mod targets; +pub(crate) mod translations; pub(crate) mod validate; pub(crate) mod verify; diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs new file mode 100644 index 0000000..28e99a6 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -0,0 +1,608 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +use brain_brew_core::{ + CanonicalDeck, Overlay, TranslationCoverageCategory, TranslationCoverageEntry, + TranslationCoverageReport, +}; +use serde_json::json; + +use crate::help; +use crate::io::{PlannedOverlay, plan_manifest_target_with_packages, read_manifest}; +use crate::output; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if args.len() == 1 && (args[0] == "--help" || args[0] == "-h") { + print!( + "{}", + help::command("translations").expect("translations help exists") + ); + return Ok(()); + } + let args = parse_translation_args(args)?; + let manifest = read_manifest(&args.manifest_path)?; + let target_names = if args.all_targets || args.target.is_none() { + manifest.targets.keys().cloned().collect::>() + } else { + vec![args.target.clone().expect("target exists")] + }; + + let mut reports = Vec::new(); + let mut stubs_by_file = BTreeMap::>::new(); + for target in &target_names { + let plan = plan_manifest_target_with_packages( + &args.manifest_path, + target, + &args.include_paths, + &args.package_roots, + )?; + let mut current = plan.base.clone(); + for (planned, overlay) in &plan.overlays { + if overlay.translations.is_some() { + if overlay_matches_scope(target, planned, overlay, &args) + && let Some(report) = current.translation_coverage(overlay) + { + let entries = report + .entries + .into_iter() + .filter(|entry| entry_matches_scope(entry, &args)) + .collect::>(); + if args.apply { + let stubs = entries + .iter() + .filter(|entry| { + entry.category == TranslationCoverageCategory::UntranslatedFallback + }) + .map(|entry| entry.source.clone()) + .collect::>(); + if !stubs.is_empty() { + stubs_by_file + .entry(planned.file.clone()) + .or_default() + .extend(stubs); + } + } + reports.push(ScopedTranslationReport { + target: target.clone(), + overlay_id: planned.id.clone(), + overlay_file: planned.display_file.clone(), + report: TranslationCoverageReport { + overlay_id: report.overlay_id, + entries, + }, + }); + } + current = compose_lenient_translation_overlay(¤t, overlay)?; + } else { + current = current + .compose(std::slice::from_ref(overlay)) + .map_err(|error| { + format!( + "failed to compose overlay {} for target {target}: {error}", + planned.id + ) + })?; + } + } + } + + let mut applied = BTreeMap::new(); + if args.apply { + for (path, stubs) in &stubs_by_file { + let count = apply_direct_stubs(path, stubs)?; + if count > 0 { + applied.insert(path.display().to_string(), count); + } + } + } + + if args.json_output { + print_json_reports(&reports, &applied); + } else { + print_human_reports(&reports); + if args.apply { + let total = applied.values().sum::(); + let details = applied + .iter() + .map(|(file, count)| (file.as_str(), count.to_string())) + .collect::>(); + output::print_success(format!("applied {total} translation stub(s)"), &details); + } + } + + Ok(()) +} + +struct TranslationArgs { + manifest_path: PathBuf, + target: Option, + all_targets: bool, + include_paths: Vec, + package_roots: Vec, + language: Option, + overlay: Option, + note: Option, + field: Option, + path_prefixes: Vec, + apply: bool, + json_output: bool, +} + +fn parse_translation_args(args: &[String]) -> Result { + let mut parsed = TranslationArgs { + manifest_path: PathBuf::from("brainbrew.yaml"), + target: None, + all_targets: false, + include_paths: Vec::new(), + package_roots: Vec::new(), + language: None, + overlay: None, + note: None, + field: None, + path_prefixes: Vec::new(), + apply: false, + json_output: false, + }; + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--manifest" => { + let Some(path) = args.get(index + 1) else { + return Err("--manifest requires a path".to_owned()); + }; + parsed.manifest_path = PathBuf::from(path); + index += 2; + } + "--target" => { + let Some(name) = args.get(index + 1) else { + return Err("--target requires a name".to_owned()); + }; + parsed.target = Some(name.clone()); + index += 2; + } + "--all-targets" => { + parsed.all_targets = true; + index += 1; + } + "--include" => { + let Some(path) = args.get(index + 1) else { + return Err("--include requires a path".to_owned()); + }; + parsed.include_paths.push(PathBuf::from(path)); + index += 2; + } + "--package-root" => { + let Some(path) = args.get(index + 1) else { + return Err("--package-root requires a path".to_owned()); + }; + parsed.package_roots.push(PathBuf::from(path)); + index += 2; + } + "--language" => { + let Some(language) = args.get(index + 1) else { + return Err("--language requires a code".to_owned()); + }; + parsed.language = Some(language.clone()); + index += 2; + } + "--overlay" => { + let Some(overlay) = args.get(index + 1) else { + return Err("--overlay requires an id or file fragment".to_owned()); + }; + parsed.overlay = Some(overlay.clone()); + index += 2; + } + "--note" => { + let Some(note) = args.get(index + 1) else { + return Err("--note requires a note id".to_owned()); + }; + parsed.note = Some(note.clone()); + index += 2; + } + "--field" => { + let Some(field) = args.get(index + 1) else { + return Err("--field requires a field id".to_owned()); + }; + parsed.field = Some(field.clone()); + index += 2; + } + "--path-prefix" => { + let Some(prefix) = args.get(index + 1) else { + return Err("--path-prefix requires a deck path prefix".to_owned()); + }; + parsed.path_prefixes.push(prefix.clone()); + index += 2; + } + "--apply" => { + parsed.apply = true; + index += 1; + } + "--json" => { + parsed.json_output = true; + index += 1; + } + other => return Err(format!("unexpected translations argument {other:?}")), + } + } + if parsed.all_targets && parsed.target.is_some() { + return Err("choose --all-targets or --target, not both".to_owned()); + } + Ok(parsed) +} + +struct ScopedTranslationReport { + target: String, + overlay_id: String, + overlay_file: String, + report: TranslationCoverageReport, +} + +fn overlay_matches_scope( + target: &str, + planned: &PlannedOverlay, + overlay: &Overlay, + args: &TranslationArgs, +) -> bool { + if let Some(overlay_filter) = &args.overlay { + let overlay_id = overlay.id.as_str(); + if !planned.id.contains(overlay_filter) + && !planned.display_file.contains(overlay_filter) + && !overlay_id.contains(overlay_filter) + { + return false; + } + } + if let Some(language) = &args.language + && !language_matches(language, target, planned, overlay) + { + return false; + } + true +} + +fn language_matches( + language: &str, + target: &str, + planned: &PlannedOverlay, + overlay: &Overlay, +) -> bool { + let language = language.trim(); + if language.is_empty() { + return true; + } + let target_has_language = target == language + || target.starts_with(&format!("{language}-")) + || target.ends_with(&format!("-{language}")); + let overlay_id = overlay.id.as_str(); + let file_stem = Path::new(&planned.display_file) + .file_stem() + .and_then(|stem| stem.to_str()) + .unwrap_or_default(); + target_has_language + || planned.id.contains(&format!(".{language}")) + || planned.id.contains(&format!("-{language}")) + || overlay_id.contains(&format!(".{language}")) + || overlay_id.contains(&format!("-{language}")) + || file_stem == language + || planned + .display_file + .split(['/', '\\']) + .any(|part| part == language || part == format!("{language}.yaml")) +} + +fn entry_matches_scope(entry: &TranslationCoverageEntry, args: &TranslationArgs) -> bool { + if let Some(note) = &args.note { + let full = if note.starts_with("note.") { + note.clone() + } else { + format!("note.{note}") + }; + if !entry.path.starts_with(&format!("notes.{note}.")) + && !entry.path.starts_with(&format!("notes.{full}.")) + && !entry.path.contains(&format!(".notes.{note}.")) + && !entry.path.contains(&format!(".notes.{full}.")) + { + return false; + } + } + if let Some(field) = &args.field { + let full = if field.starts_with("field.") { + field.clone() + } else { + format!("field.{field}") + }; + if !entry.path.contains(&format!(".fields.{field}")) + && !entry.path.contains(&format!(".fields.{full}")) + { + return false; + } + } + if !args.path_prefixes.is_empty() + && !args + .path_prefixes + .iter() + .any(|prefix| entry.path == *prefix || entry.path.starts_with(&format!("{prefix}."))) + { + return false; + } + true +} + +fn compose_lenient_translation_overlay( + current: &CanonicalDeck, + overlay: &Overlay, +) -> Result { + let mut sanitized = overlay.clone(); + if let Some(translations) = &mut sanitized.translations { + translations.require_complete = false; + if let Some(report) = current.translation_coverage(overlay) { + for entry in report.entries { + match entry.category { + TranslationCoverageCategory::StaleDirectKey => { + translations.direct.remove(&entry.source); + } + TranslationCoverageCategory::StaleContextualKey => { + if let Some(context) = &entry.context + && let Some(replacements) = translations.contextual.get_mut(context) + { + replacements.remove(&entry.source); + if replacements.is_empty() { + translations.contextual.remove(context); + } + } + } + TranslationCoverageCategory::StaleTargetAddition + | TranslationCoverageCategory::InvalidTargetAddition => { + let path = entry.context.as_deref().unwrap_or(&entry.path); + translations.target_additions.remove(path); + } + TranslationCoverageCategory::StaleVariableKey => { + if let Some(variable_key) = &entry.context + && let Some(replacements) = translations.variables.get_mut(variable_key) + { + replacements.remove(&entry.source); + if replacements.is_empty() { + translations.variables.remove(variable_key); + } + } + } + TranslationCoverageCategory::StaleAdapterIdKey => { + if let Some(adapter_key) = &entry.context + && let Some(replacements) = + translations.adapter_ids.get_mut(adapter_key) + { + replacements.remove(&entry.source); + if replacements.is_empty() { + translations.adapter_ids.remove(adapter_key); + } + } + } + _ => {} + } + } + } + } + current.compose(&[sanitized]).map_err(|error| { + format!( + "failed to compose translation overlay {}: {error}", + overlay.id + ) + }) +} + +fn print_human_reports(reports: &[ScopedTranslationReport]) { + for report in reports { + let counts = category_counts(&report.report.entries); + println!( + "Translation coverage for target {} overlay {} ({})", + report.target, report.overlay_id, report.overlay_file + ); + println!( + " direct translations: {}", + counts.get("direct_translation").copied().unwrap_or(0) + ); + println!( + " contextual overrides: {}", + counts.get("contextual_override").copied().unwrap_or(0) + ); + println!( + " target-language additions: {}", + counts.get("target_language_addition").copied().unwrap_or(0) + ); + println!( + " stale/invalid keys: {}", + report + .report + .entries + .iter() + .filter(|entry| { + matches!( + entry.category, + TranslationCoverageCategory::StaleDirectKey + | TranslationCoverageCategory::StaleContextualKey + | TranslationCoverageCategory::StaleTargetAddition + | TranslationCoverageCategory::StaleVariableKey + | TranslationCoverageCategory::StaleAdapterIdKey + | TranslationCoverageCategory::InvalidTargetAddition + ) + }) + .count() + ); + println!( + " missing/untranslated fallbacks: {}", + counts.get("untranslated_fallback").copied().unwrap_or(0) + ); + for entry in report.report.problem_entries() { + println!( + " - {} {} source={} translated={}", + entry.category.as_str(), + entry.path, + yaml_scalar(&entry.source), + entry + .translated + .as_deref() + .map(yaml_scalar) + .unwrap_or_else(|| "''".to_owned()) + ); + } + } +} + +fn print_json_reports(reports: &[ScopedTranslationReport], applied: &BTreeMap) { + let reports_json = reports + .iter() + .map(|report| { + json!({ + "target": report.target, + "overlay": report.overlay_id, + "file": report.overlay_file, + "summary": category_counts(&report.report.entries), + "entries": report.report.entries.iter().map(entry_json).collect::>(), + }) + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&json!({ + "reports": reports_json, + "applied": applied, + })) + .expect("translation report JSON serializes") + ); +} + +fn entry_json(entry: &TranslationCoverageEntry) -> serde_json::Value { + json!({ + "category": entry.category.as_str(), + "path": entry.path, + "source": entry.source, + "translated": entry.translated, + "context": entry.context, + }) +} + +fn category_counts(entries: &[TranslationCoverageEntry]) -> BTreeMap<&'static str, usize> { + let mut counts = BTreeMap::new(); + for entry in entries { + *counts.entry(entry.category.as_str()).or_insert(0) += 1; + } + counts +} + +fn apply_direct_stubs(path: &Path, stubs: &BTreeSet) -> Result { + if stubs.is_empty() { + return Ok(0); + } + let input = fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; + let stub_lines = stubs + .iter() + .map(|source| format!(" {}: {}\n", yaml_scalar(source), yaml_scalar(source))) + .collect::(); + let output = insert_direct_stub_lines(&input, &stub_lines); + if output != input { + fs::write(path, output).map_err(|error| format!("{}: {error}", path.display()))?; + } + Ok(stubs.len()) +} + +fn insert_direct_stub_lines(input: &str, stub_lines: &str) -> String { + let spans = line_spans(input); + let translations_line = spans.iter().position(|(start, end)| { + let line = &input[*start..*end]; + line.trim_end() == "translations:" + }); + + if let Some(translations_index) = translations_line { + let direct_index = spans + .iter() + .enumerate() + .skip(translations_index + 1) + .take_while(|(_, (start, end))| { + let line = &input[*start..*end]; + line.trim().is_empty() + || line.trim_start().starts_with('#') + || line.starts_with(' ') + }) + .find(|(_, (start, end))| input[*start..*end].trim_end() == " direct:") + .map(|(index, _)| index); + if let Some(direct_index) = direct_index { + let insert_at = spans + .iter() + .enumerate() + .skip(direct_index + 1) + .find(|(_, (start, end))| { + let line = &input[*start..*end]; + !line.trim().is_empty() + && !line.trim_start().starts_with('#') + && (line.starts_with(" ") && !line.starts_with(" ") + || !line.starts_with(' ')) + }) + .map(|(_, (start, _))| *start) + .unwrap_or(input.len()); + return insert_at_byte(input, insert_at, stub_lines); + } + + let insert_at = spans[translations_index].1; + return insert_at_byte(input, insert_at, &format!(" direct:\n{stub_lines}")); + } + + let insert_after = spans + .iter() + .find(|(start, end)| input[*start..*end].trim_start().starts_with("kind:")) + .map(|(_, end)| *end) + .or_else(|| spans.first().map(|(_, end)| *end)) + .unwrap_or(0); + insert_at_byte( + input, + insert_after, + &format!("translations:\n direct:\n{stub_lines}"), + ) +} + +fn insert_at_byte(input: &str, index: usize, insertion: &str) -> String { + let mut output = String::with_capacity(input.len() + insertion.len() + 1); + output.push_str(&input[..index]); + if index > 0 && !input[..index].ends_with('\n') { + output.push('\n'); + } + output.push_str(insertion); + output.push_str(&input[index..]); + output +} + +fn line_spans(input: &str) -> Vec<(usize, usize)> { + let mut spans = Vec::new(); + let mut start = 0; + for (index, ch) in input.char_indices() { + if ch == '\n' { + spans.push((start, index + 1)); + start = index + 1; + } + } + if start < input.len() || input.is_empty() { + spans.push((start, input.len())); + } + spans +} + +fn yaml_scalar(value: &str) -> String { + if !value.is_empty() + && !value.starts_with([ + ' ', '-', '?', ':', '@', '`', '&', '*', '!', '|', '>', '#', '{', '[', ',', + ]) + && !value.ends_with(' ') + && value.chars().all(|ch| { + ch.is_ascii_alphanumeric() || matches!(ch, ' ' | '.' | ',' | '_' | '-' | '/' | ':') + }) + && !value.chars().all(|ch| ch.is_ascii_digit()) + && !matches!( + value, + "true" | "false" | "True" | "False" | "TRUE" | "FALSE" | "null" | "Null" | "NULL" + ) + { + value.to_owned() + } else { + format!("'{}'", value.replace('\'', "''")) + } +} diff --git a/crates/brain-brew-cli/src/commands/verify.rs b/crates/brain-brew-cli/src/commands/verify.rs index 6188c09..d6950a9 100644 --- a/crates/brain-brew-cli/src/commands/verify.rs +++ b/crates/brain-brew-cli/src/commands/verify.rs @@ -1,14 +1,15 @@ use std::fs; use std::path::Path; -use brain_brew_core::CanonicalDeck; +use brain_brew_core::{CanonicalDeck, TranslationCoverageCategory}; use brain_brew_formats::{crowdanki, manifest}; use crate::args::parse_verify_args; use crate::help; use crate::io::{ - manifest_root, plan_manifest_target_with_packages, read_manifest, root_relative_path, - verify_canonical_deck_format, verify_manifest_format, verify_overlay_format, + ManifestTargetPlan, manifest_root, plan_manifest_target_with_packages, read_manifest, + root_relative_path, verify_canonical_deck_format, verify_manifest_format, + verify_overlay_format, }; use crate::media_assets::validate_media_assets; use crate::output; @@ -48,6 +49,14 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { for (overlay, _) in &plan.overlays { verify_overlay_format(&overlay.file)?; } + let policy = verify_args.translation_coverage.unwrap_or_else(|| { + manifest + .targets + .get(target) + .map(|target| target.translation_coverage) + .unwrap_or_default() + }); + verify_translation_coverage_policy(&plan, policy)?; let deck = plan.compose()?; deck.validate().map_err(|error| error.to_string())?; if let Some(media_root) = &media_root { @@ -68,6 +77,50 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { Ok(()) } +fn verify_translation_coverage_policy( + plan: &ManifestTargetPlan, + policy: manifest::TranslationCoveragePolicy, +) -> Result<(), String> { + if policy == manifest::TranslationCoveragePolicy::Lenient { + return Ok(()); + } + + let mut current = plan.base.clone(); + for (planned, overlay) in &plan.overlays { + if let Some(report) = current.translation_coverage(overlay) { + let missing = report + .entries + .iter() + .filter(|entry| entry.category == TranslationCoverageCategory::UntranslatedFallback) + .take(10) + .map(|entry| format!("{} source {:?}", entry.path, entry.source)) + .collect::>(); + if !missing.is_empty() { + return Err(format!( + "translation coverage strict policy failed for target {} overlay {} ({}): {} untranslated fallback(s): {}", + plan.target, + planned.id, + planned.display_file, + report + .entries + .iter() + .filter(|entry| entry.category + == TranslationCoverageCategory::UntranslatedFallback) + .count(), + missing.join(", ") + )); + } + } + current = current.compose(std::slice::from_ref(overlay)).map_err(|error| { + format!( + "failed to compose overlay {} for target {} while checking translation coverage: {error}", + planned.id, plan.target + ) + })?; + } + Ok(()) +} + fn verify_configured_exports( root: &Path, manifest: &manifest::FederatedDeckManifest, diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index b5f6598..95e7073 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -13,6 +13,7 @@ pub(crate) fn general() -> String { " import Import CrowdAnki into CanonicalDeck YAML\n", " lock Update or verify locked federated package inputs\n", " targets List manifest targets\n", + " translations Report or apply translation coverage stubs\n", " verify Run manifest formatting, composition, validation, media, and golden checks\n", " explain Explain a manifest target and its overlay stack\n", " diff Compare decks semantically, or emit an overlay draft\n\n", @@ -23,6 +24,7 @@ pub(crate) fn general() -> String { " brainbrew compose --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n", " brainbrew lock update --package anki-geo.ultimate-geography --path ../ultimate-geography\n", " brainbrew export crowdanki --manifest brainbrew.yaml --target de-extended --media-root media/\n", + " brainbrew translations --manifest brainbrew.yaml --target de-standard\n", " brainbrew verify --manifest brainbrew.yaml --all-targets\n\n", "Run `brainbrew --help` for command-specific examples.\n", ), @@ -53,8 +55,11 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "targets" => Some( "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", ), + "translations" => Some( + "Usage:\n brainbrew translations [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--json]\n\nExamples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n\nReport mode is the default and never edits files. --apply inserts source->source stubs for missing untranslated fallbacks into translations.direct. Use --path-prefix with paths from a diff to scope a changed-base review.\n", + ), "verify" => Some( - "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n", + "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", ), "explain" => Some( "Usage:\n brainbrew explain [--manifest brainbrew.yaml] --target [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew explain --manifest brainbrew.yaml --target da-standard\n brainbrew explain --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew explain --manifest brainbrew.yaml --target de-extended --json\n", diff --git a/crates/brain-brew-cli/src/main.rs b/crates/brain-brew-cli/src/main.rs index d50489f..5945242 100644 --- a/crates/brain-brew-cli/src/main.rs +++ b/crates/brain-brew-cli/src/main.rs @@ -43,6 +43,7 @@ fn run() -> Result<(), String> { "import" => commands::import::run(&args[1..]), "lock" => commands::lock::run(&args[1..]), "targets" => commands::targets::run(&args[1..]), + "translations" => commands::translations::run(&args[1..]), "verify" => commands::verify::run(&args[1..]), "explain" => commands::explain::run(&args[1..]), "diff" => commands::diff::run(&args[1..]), diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 3dbc143..16bec8d 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -902,6 +902,108 @@ fn verify_checks_all_manifest_targets() { assert!(out.contains("verified 1 target")); } +#[test] +fn translations_reports_missing_stale_contextual_and_additions_without_modifying() { + let dir = temp_dir("translations-report"); + write_translation_workspace(&dir); + let overlay_path = dir.join("da.yaml"); + let before = fs::read_to_string(&overlay_path).unwrap(); + + let output = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("Translation coverage for target da-standard")); + assert!(out.contains("contextual overrides: 1")); + assert!(out.contains("target-language additions: 1")); + assert!(out.contains("stale_direct_key translations.direct.Removed source")); + assert!(out.contains("untranslated_fallback notes.note.sweden.fields.field.country")); + assert_eq!(fs::read_to_string(overlay_path).unwrap(), before); +} + +#[test] +fn translations_apply_inserts_sorted_direct_stubs_and_preserves_comments() { + let dir = temp_dir("translations-apply"); + write_translation_workspace(&dir); + let overlay_path = dir.join("da.yaml"); + + let output = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--path-prefix", + "notes.note.sweden.fields.field.country", + "--apply", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let updated = fs::read_to_string(overlay_path).unwrap(); + assert!(updated.contains("# translator note kept")); + assert!(updated.contains(" Sweden: Sweden\n")); +} + +#[test] +fn verify_translation_coverage_policy_can_be_lenient_or_strict() { + let dir = temp_dir("translations-verify-policy"); + write_translation_workspace(&dir); + let overlay_path = dir.join("da.yaml"); + fs::write( + &overlay_path, + r#"id: overlay.translation.da +kind: translation +translations: + ignore_paths: + - 'deck.*' + - 'note_types.*' + - 'notes.*.fields.field.flag' + - 'notes.*.tags.*' + direct: + Finland: Finland + contextual: + notes.note: + finland: + Helsinki: Helsingfors + target_additions: + notes.note.finland.fields.field.flag: '' +"#, + ) + .unwrap(); + + let lenient_output = run([ + "verify", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--translation-coverage", + "lenient", + ]); + assert!( + lenient_output.status.success(), + "stderr: {}", + stderr(&lenient_output) + ); + + let strict_output = run([ + "verify", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-release", + ]); + assert!(!strict_output.status.success()); + assert!(stderr(&strict_output).contains("translation coverage strict policy failed")); + assert!(stderr(&strict_output).contains("notes.note.sweden.fields.field.country")); +} + #[test] fn export_crowdanki_copies_media_from_media_root_and_checks_hashes() { let dir = temp_dir("export-media"); @@ -1549,6 +1651,68 @@ fn write_manifest_workspace(dir: &Path) { fs::write(dir.join("brainbrew.yaml"), MANIFEST_YAML).unwrap(); } +fn write_translation_workspace(dir: &Path) { + let deck = SAMPLE_CANONICAL_YAML + .replace("field.flag: ''", "field.flag: ''") + .replace( + "media:\n", + r#" note.sweden: + note_type_id: note-type.country + fields: + field.capital: Stockholm + field.country: Sweden + field.flag: '' + tags: + - Europe + - Nordic + adapter_ids: + crowdanki:guid: ug-sweden-guid +media: +"#, + ); + fs::write(dir.join("deck.yaml"), deck).unwrap(); + fs::write( + dir.join("da.yaml"), + r#"id: overlay.translation.da +kind: translation +# translator note kept +translations: + ignore_paths: + - deck.* + - note_types.* + - notes.*.tags.* + - notes.*.fields.field.flag + direct: + Finland: Finland + Removed source: Fjernet + contextual: + notes.note.finland: + Helsinki: Helsingfors + target_additions: + notes.note.finland.fields.field.flag: '' +"#, + ) + .unwrap(); + fs::write( + dir.join("brainbrew.yaml"), + r#"base: deck.yaml +overlays: + overlay.translation.da: + file: da.yaml + kind: translation +targets: + da-release: + overlays: + - overlay.translation.da + translation_coverage: strict + da-standard: + overlays: + - overlay.translation.da +"#, + ) + .unwrap(); +} + fn write_include_workspace(dir: &Path) { fs::create_dir_all(dir.join("content")).unwrap(); fs::create_dir_all(dir.join("templates")).unwrap(); diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs index c2c294e..3f7371b 100644 --- a/crates/brain-brew-core/src/lib.rs +++ b/crates/brain-brew-core/src/lib.rs @@ -188,6 +188,14 @@ impl CanonicalDeck { render_deck_variables(self) } + /// Report translation coverage for one translation overlay without modifying this deck. + pub fn translation_coverage(&self, overlay: &Overlay) -> Option { + overlay + .translations + .as_ref() + .map(|translations| translation_coverage_report(self, overlay, translations)) + } + /// Validate strict core invariants that do not require filesystem or format access. pub fn validate(&self) -> Result<(), ValidationReport> { let mut errors = Vec::new(); @@ -368,6 +376,312 @@ fn fill_added_field_blanks( } } +fn translation_coverage_report( + deck: &CanonicalDeck, + overlay: &Overlay, + translations: &TranslationDictionary, +) -> TranslationCoverageReport { + let mut builder = TranslationCoverageBuilder { + translations, + entries: Vec::new(), + seen_direct: BTreeSet::new(), + seen_contextual: BTreeSet::new(), + seen_target_additions: BTreeSet::new(), + seen_variables: BTreeSet::new(), + seen_adapter_ids: BTreeSet::new(), + }; + + builder.record_string(&deck.name, "deck.name".to_owned(), None); + builder.record_string(&deck.description, "deck.description".to_owned(), None); + builder.record_variables(&deck.variables, "deck.variables"); + builder.record_adapter_ids(&deck.adapter_ids, "deck.adapter_ids"); + + for (note_type_id, note_type) in &deck.note_types { + builder.record_string( + ¬e_type.name, + format!("note_types.{note_type_id}.name"), + None, + ); + builder.record_variables( + ¬e_type.variables, + &format!("note_types.{note_type_id}.variables"), + ); + for field in ¬e_type.fields { + builder.record_string( + &field.name, + format!("note_types.{note_type_id}.fields.{}.name", field.id), + None, + ); + } + for template in ¬e_type.card_templates { + builder.record_string( + &template.name, + format!( + "note_types.{note_type_id}.card_templates.{}.name", + template.id + ), + None, + ); + builder.record_variables( + &template.variables, + &format!( + "note_types.{note_type_id}.card_templates.{}.variables", + template.id + ), + ); + builder.record_adapter_ids( + &template.adapter_ids, + &format!( + "note_types.{note_type_id}.card_templates.{}.adapter_ids", + template.id + ), + ); + } + builder.record_adapter_ids( + ¬e_type.adapter_ids, + &format!("note_types.{note_type_id}.adapter_ids"), + ); + } + + for (note_id, note) in &deck.notes { + builder.record_variables(¬e.variables, &format!("notes.{note_id}.variables")); + for (field_id, value) in ¬e.fields { + builder.record_string(value, format!("notes.{note_id}.fields.{field_id}"), None); + } + builder.record_tags(¬e.tags, &format!("notes.{note_id}.tags")); + builder.record_adapter_ids(¬e.adapter_ids, &format!("notes.{note_id}.adapter_ids")); + } + + builder.finish(overlay.id.clone()) +} + +struct TranslationCoverageBuilder<'a> { + translations: &'a TranslationDictionary, + entries: Vec, + seen_direct: BTreeSet, + seen_contextual: BTreeSet<(String, String)>, + seen_target_additions: BTreeSet, + seen_variables: BTreeSet<(String, String)>, + seen_adapter_ids: BTreeSet<(String, String)>, +} + +impl TranslationCoverageBuilder<'_> { + fn record_variables(&mut self, variables: &BTreeMap, path_prefix: &str) { + for (key, value) in variables { + self.record_string(value, format!("{path_prefix}.{key}"), Some(key)); + } + } + + fn record_string(&mut self, value: &str, path: String, variable_key: Option<&str>) { + if let Some(addition) = self.translations.target_additions.get(&path) { + self.seen_target_additions.insert(path.clone()); + let category = if value.is_empty() { + TranslationCoverageCategory::TargetLanguageAddition + } else { + TranslationCoverageCategory::InvalidTargetAddition + }; + self.entries.push(TranslationCoverageEntry { + category, + path, + source: value.to_owned(), + translated: Some(addition.clone()), + context: None, + }); + return; + } + + if value.is_empty() { + return; + } + + if is_ignored_translation_path(self.translations, &path) { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::IgnoredSource, + path, + source: value.to_owned(), + translated: None, + context: None, + }); + return; + } + + if let Some(variable_key) = variable_key + && let Some(replacements) = self.translations.variables.get(variable_key) + && let Some(translated) = replacements.get(value) + { + self.seen_variables + .insert((variable_key.to_owned(), value.to_owned())); + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::VariableTranslation, + path, + source: value.to_owned(), + translated: Some(translated.clone()), + context: Some(variable_key.to_owned()), + }); + return; + } + + let source = value.to_owned(); + let direct_translation = self.translations.direct.get(value); + if direct_translation.is_some() { + self.seen_direct.insert(source.clone()); + } + + let mut contextual_translation: Option<(&String, &String)> = None; + for (context_path, replacements) in &self.translations.contextual { + if context_matches_path(context_path, &path) + && let Some(translated) = replacements.get(value) + { + self.seen_contextual + .insert((context_path.clone(), source.clone())); + if contextual_translation + .as_ref() + .is_none_or(|(current_context, _)| context_path.len() > current_context.len()) + { + contextual_translation = Some((context_path, translated)); + } + } + } + + if let Some((context_path, translated)) = contextual_translation { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::ContextualOverride, + path, + source, + translated: Some(translated.clone()), + context: Some(context_path.clone()), + }); + } else if let Some(translated) = direct_translation { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::DirectTranslation, + path, + source, + translated: Some(translated.clone()), + context: None, + }); + } else { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::UntranslatedFallback, + path, + source: source.clone(), + translated: Some(source), + context: None, + }); + } + } + + fn record_tags(&mut self, tags: &BTreeSet, path_prefix: &str) { + for tag in tags { + self.record_string(tag, format!("{path_prefix}.{tag}"), None); + } + } + + fn record_adapter_ids(&mut self, adapter_ids: &AdapterIds, path_prefix: &str) { + for (key, value) in adapter_ids.iter() { + let Some(replacements) = self.translations.adapter_ids.get(key) else { + continue; + }; + let Some(translated) = replacements.get(value) else { + continue; + }; + self.seen_adapter_ids + .insert((key.to_owned(), value.to_owned())); + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::AdapterIdTranslation, + path: format!("{path_prefix}.{key}"), + source: value.to_owned(), + translated: Some(translated.clone()), + context: Some(key.to_owned()), + }); + } + } + + fn finish(mut self, overlay_id: StableId) -> TranslationCoverageReport { + for (source, translated) in &self.translations.direct { + if !self.seen_direct.contains(source) { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::StaleDirectKey, + path: format!("translations.direct.{source}"), + source: source.clone(), + translated: Some(translated.clone()), + context: None, + }); + } + } + for (context_path, replacements) in &self.translations.contextual { + for (source, translated) in replacements { + if !self + .seen_contextual + .contains(&(context_path.clone(), source.clone())) + { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::StaleContextualKey, + path: format!("translations.contextual.{context_path}.{source}"), + source: source.clone(), + translated: Some(translated.clone()), + context: Some(context_path.clone()), + }); + } + } + } + for (path, translated) in &self.translations.target_additions { + if !self.seen_target_additions.contains(path) { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::StaleTargetAddition, + path: format!("translations.target_additions.{path}"), + source: String::new(), + translated: Some(translated.clone()), + context: Some(path.clone()), + }); + } + } + for (variable_key, replacements) in &self.translations.variables { + for (source, translated) in replacements { + if !self + .seen_variables + .contains(&(variable_key.clone(), source.clone())) + { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::StaleVariableKey, + path: format!("translations.variables.{variable_key}.{source}"), + source: source.clone(), + translated: Some(translated.clone()), + context: Some(variable_key.clone()), + }); + } + } + } + for (adapter_key, replacements) in &self.translations.adapter_ids { + for (source, translated) in replacements { + if !self + .seen_adapter_ids + .contains(&(adapter_key.clone(), source.clone())) + { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::StaleAdapterIdKey, + path: format!("translations.adapter_ids.{adapter_key}.{source}"), + source: source.clone(), + translated: Some(translated.clone()), + context: Some(adapter_key.clone()), + }); + } + } + } + + self.entries.sort_by(|left, right| { + left.path + .cmp(&right.path) + .then_with(|| left.category.as_str().cmp(right.category.as_str())) + .then_with(|| left.source.cmp(&right.source)) + }); + + TranslationCoverageReport { + overlay_id, + entries: self.entries, + } + } +} + fn apply_translation_dictionary( resolved: &mut CanonicalDeck, overlay: &Overlay, @@ -2489,6 +2803,105 @@ pub struct TranslationDictionary { pub ignore_paths: BTreeSet, } +/// Non-mutating coverage report for one translation overlay applied to a source deck. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TranslationCoverageReport { + pub overlay_id: StableId, + pub entries: Vec, +} + +impl TranslationCoverageReport { + /// Entries that indicate untranslated fallback output or stale/invalid dictionary keys. + pub fn problem_entries(&self) -> impl Iterator { + self.entries + .iter() + .filter(|entry| entry.category.is_problem()) + } + + /// Returns true when at least one extracted source string would fall back untranslated. + pub fn has_untranslated_fallbacks(&self) -> bool { + self.entries + .iter() + .any(|entry| entry.category == TranslationCoverageCategory::UntranslatedFallback) + } + + /// Returns true when the dictionary contains stale or invalid entries. + pub fn has_stale_or_invalid_entries(&self) -> bool { + self.entries.iter().any(|entry| { + matches!( + entry.category, + TranslationCoverageCategory::StaleDirectKey + | TranslationCoverageCategory::StaleContextualKey + | TranslationCoverageCategory::StaleTargetAddition + | TranslationCoverageCategory::StaleVariableKey + | TranslationCoverageCategory::StaleAdapterIdKey + | TranslationCoverageCategory::InvalidTargetAddition + ) + }) + } +} + +/// One source string, dictionary entry, or fallback in a translation coverage report. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TranslationCoverageEntry { + pub category: TranslationCoverageCategory, + pub path: String, + pub source: String, + pub translated: Option, + pub context: Option, +} + +/// Maintainer-facing classification for translation coverage entries. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TranslationCoverageCategory { + DirectTranslation, + ContextualOverride, + TargetLanguageAddition, + VariableTranslation, + AdapterIdTranslation, + IgnoredSource, + UntranslatedFallback, + StaleDirectKey, + StaleContextualKey, + StaleTargetAddition, + StaleVariableKey, + StaleAdapterIdKey, + InvalidTargetAddition, +} + +impl TranslationCoverageCategory { + pub fn as_str(self) -> &'static str { + match self { + Self::DirectTranslation => "direct_translation", + Self::ContextualOverride => "contextual_override", + Self::TargetLanguageAddition => "target_language_addition", + Self::VariableTranslation => "variable_translation", + Self::AdapterIdTranslation => "adapter_id_translation", + Self::IgnoredSource => "ignored_source", + Self::UntranslatedFallback => "untranslated_fallback", + Self::StaleDirectKey => "stale_direct_key", + Self::StaleContextualKey => "stale_contextual_key", + Self::StaleTargetAddition => "stale_target_addition", + Self::StaleVariableKey => "stale_variable_key", + Self::StaleAdapterIdKey => "stale_adapter_id_key", + Self::InvalidTargetAddition => "invalid_target_addition", + } + } + + pub fn is_problem(self) -> bool { + matches!( + self, + Self::UntranslatedFallback + | Self::StaleDirectKey + | Self::StaleContextualKey + | Self::StaleTargetAddition + | Self::StaleVariableKey + | Self::StaleAdapterIdKey + | Self::InvalidTargetAddition + ) + } +} + /// Maintainer-facing category for an overlay. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum OverlayKind { diff --git a/crates/brain-brew-core/tests/overlay_compose.rs b/crates/brain-brew-core/tests/overlay_compose.rs index 74fd851..ae1c35b 100644 --- a/crates/brain-brew-core/tests/overlay_compose.rs +++ b/crates/brain-brew-core/tests/overlay_compose.rs @@ -4,7 +4,8 @@ use brain_brew_core::{ AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplate, CardTemplateChange, ChangeIntent, ComposeErrorKind, DeckChange, ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, - Overlay, OverlayKind, PropertyChange, StableId, TagChange, TranslationDictionary, + Overlay, OverlayKind, PropertyChange, StableId, TagChange, TranslationCoverageCategory, + TranslationDictionary, }; #[test] @@ -454,6 +455,154 @@ fn translation_dictionary_target_addition_fails_when_base_is_not_blank() { })); } +#[test] +fn translation_coverage_reports_new_note_and_new_field_fallbacks() { + let mut base = ug_style_deck(); + let mut sweden = sweden_note(); + sweden + .fields + .insert(sid("field.population"), "10 million".to_owned()); + base.notes.insert(sid("note.sweden"), sweden); + base.note_types + .get_mut(&sid("note-type.country")) + .unwrap() + .fields + .push(FieldDefinition { + id: sid("field.population"), + name: "Population".to_owned(), + }); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::from([ + ("Finland".to_owned(), "Finland".to_owned()), + ("Helsinki".to_owned(), "Helsingfors".to_owned()), + ]), + contextual: BTreeMap::new(), + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::from([ + "deck.*".to_owned(), + "note_types.*".to_owned(), + "notes.*.fields.field.flag".to_owned(), + "notes.*.tags.*".to_owned(), + ]), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = base + .translation_coverage(&overlay) + .expect("translation overlay has coverage"); + + assert!(report.has_untranslated_fallbacks()); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::UntranslatedFallback + && entry.path == "notes.note.sweden.fields.field.country" + && entry.source == "Sweden" + })); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::UntranslatedFallback + && entry.path == "notes.note.sweden.fields.field.population" + && entry.source == "10 million" + })); +} + +#[test] +fn translation_coverage_reports_stale_changed_or_removed_source_keys() { + let mut base = ug_style_deck(); + base.notes + .get_mut(&sid("note.finland")) + .unwrap() + .fields + .insert(sid("field.capital"), "Helsinki City".to_owned()); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::from([("Helsinki".to_owned(), "Helsingfors".to_owned())]), + contextual: BTreeMap::new(), + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::new(), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = base + .translation_coverage(&overlay) + .expect("translation overlay has coverage"); + + assert!(report.has_stale_or_invalid_entries()); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::StaleDirectKey + && entry.path == "translations.direct.Helsinki" + })); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::UntranslatedFallback + && entry.path == "notes.note.finland.fields.field.capital" + && entry.source == "Helsinki City" + })); +} + +#[test] +fn translation_coverage_reports_path_specific_overrides_and_additions() { + let mut base = ug_style_deck(); + base.notes + .get_mut(&sid("note.finland")) + .unwrap() + .fields + .insert(sid("field.flag"), String::new()); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::from([("Finland".to_owned(), "Finland".to_owned())]), + contextual: BTreeMap::from([( + "notes.note.finland".to_owned(), + BTreeMap::from([("Helsinki".to_owned(), "Helsingfors".to_owned())]), + )]), + target_additions: BTreeMap::from([( + "notes.note.finland.fields.field.flag".to_owned(), + "".to_owned(), + )]), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::new(), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = base + .translation_coverage(&overlay) + .expect("translation overlay has coverage"); + + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::ContextualOverride + && entry.context.as_deref() == Some("notes.note.finland") + && entry.path == "notes.note.finland.fields.field.capital" + })); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::TargetLanguageAddition + && entry.path == "notes.note.finland.fields.field.flag" + })); +} + #[test] fn extension_overlay_can_add_a_note_type_field_and_backfill_note_values() { let base = ug_style_deck(); diff --git a/crates/brain-brew-formats/src/manifest.rs b/crates/brain-brew-formats/src/manifest.rs index bb160ba..af9ed7e 100644 --- a/crates/brain-brew-formats/src/manifest.rs +++ b/crates/brain-brew-formats/src/manifest.rs @@ -88,11 +88,22 @@ pub struct OverlayManifestEntry { pub depends_on: Vec, } +/// Translation coverage policy enforced during verification for one target. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum TranslationCoveragePolicy { + /// Missing translations are reported by `brainbrew translations` but do not fail verify. + #[default] + Lenient, + /// Missing untranslated fallbacks fail verify for release-ready targets. + Strict, +} + /// One named composition goal in a Federated Deck manifest. #[derive(Clone, Debug, Eq, PartialEq)] pub struct BuildTarget { pub extends: Option, pub overlays: Vec, + pub translation_coverage: TranslationCoveragePolicy, pub exports: TargetExports, } @@ -135,7 +146,7 @@ pub struct ExpandedOverlay { /// Parse a Federated Deck manifest from strict YAML. pub fn from_str(input: &str) -> Result { let yaml: ManifestYaml = serde_yaml::from_str(input).map_err(ManifestError::Parse)?; - Ok(yaml.into_manifest()) + yaml.into_manifest() } /// Parse and re-emit a Federated Deck manifest using deterministic formatting. @@ -208,6 +219,12 @@ pub fn to_string(manifest: &FederatedDeckManifest) -> String { out.push_str(&format!(" - {}\n", yaml_scalar(overlay))); } } + if target.translation_coverage != TranslationCoveragePolicy::Lenient { + out.push_str(&format!( + " translation_coverage: {}\n", + translation_coverage_policy_name(target.translation_coverage) + )); + } if !target.exports.is_empty() { out.push_str(" exports:\n"); if let Some(export) = &target.exports.crowdanki { @@ -231,6 +248,25 @@ pub fn to_string(manifest: &FederatedDeckManifest) -> String { out } +fn translation_coverage_policy_name(policy: TranslationCoveragePolicy) -> &'static str { + match policy { + TranslationCoveragePolicy::Lenient => "lenient", + TranslationCoveragePolicy::Strict => "strict", + } +} + +fn parse_translation_coverage_policy( + value: &str, +) -> Result { + match value { + "lenient" => Ok(TranslationCoveragePolicy::Lenient), + "strict" => Ok(TranslationCoveragePolicy::Strict), + other => Err(ManifestError::InvalidTranslationCoveragePolicy( + other.to_owned(), + )), + } +} + fn yaml_scalar(value: &str) -> String { if !value.is_empty() && !value.starts_with([ @@ -258,6 +294,7 @@ pub enum ManifestError { MissingTarget(String), MissingOverlay(String), DependencyCycle(Vec), + InvalidTranslationCoveragePolicy(String), } impl fmt::Display for ManifestError { @@ -275,6 +312,10 @@ impl fmt::Display for ManifestError { cycle.join(" -> ") ) } + Self::InvalidTranslationCoveragePolicy(policy) => write!( + f, + "invalid translation coverage policy {policy:?}; expected lenient or strict" + ), } } } @@ -296,8 +337,8 @@ struct ManifestYaml { } impl ManifestYaml { - fn into_manifest(self) -> FederatedDeckManifest { - FederatedDeckManifest { + fn into_manifest(self) -> Result { + Ok(FederatedDeckManifest { package: self.package.map(PackageMetadataYaml::into_metadata), base: self.base, include_roots: self.include_roots, @@ -309,9 +350,9 @@ impl ManifestYaml { targets: self .targets .into_iter() - .map(|(id, target)| (id, target.into_target())) - .collect(), - } + .map(|(id, target)| Ok((id, target.into_target()?))) + .collect::>()?, + }) } } @@ -365,16 +406,25 @@ struct BuildTargetYaml { #[serde(default)] overlays: Vec, #[serde(default)] + translation_coverage: Option, + #[serde(default)] exports: TargetExportsYaml, } impl BuildTargetYaml { - fn into_target(self) -> BuildTarget { - BuildTarget { + fn into_target(self) -> Result { + let translation_coverage = self + .translation_coverage + .as_deref() + .map(parse_translation_coverage_policy) + .transpose()? + .unwrap_or_default(); + Ok(BuildTarget { extends: self.extends, overlays: self.overlays, + translation_coverage, exports: self.exports.into_exports(), - } + }) } } diff --git a/crates/brain-brew-formats/tests/manifest_yaml.rs b/crates/brain-brew-formats/tests/manifest_yaml.rs index 95aced8..30cb35f 100644 --- a/crates/brain-brew-formats/tests/manifest_yaml.rs +++ b/crates/brain-brew-formats/tests/manifest_yaml.rs @@ -166,6 +166,45 @@ targets: ); } +#[test] +fn parses_and_formats_translation_coverage_policy() { + let formatted = manifest::format_str( + r#" +base: deck.yaml +targets: + de-release: + translation_coverage: strict + overlays: [] + de-dev: + translation_coverage: lenient + overlays: [] +"#, + ) + .expect("manifest formats"); + + assert_eq!( + formatted, + r#"base: deck.yaml +overlays: {} +targets: + de-dev: + overlays: [] + de-release: + overlays: [] + translation_coverage: strict +"# + ); + let manifest = manifest::from_str(&formatted).expect("manifest parses"); + assert_eq!( + manifest.targets["de-release"].translation_coverage, + manifest::TranslationCoveragePolicy::Strict + ); + assert_eq!( + manifest.targets["de-dev"].translation_coverage, + manifest::TranslationCoveragePolicy::Lenient + ); +} + #[test] fn parses_and_formats_target_extends_for_package_federation() { let formatted = manifest::format_str( diff --git a/documentation/docs/authoring/manifests-targets.md b/documentation/docs/authoring/manifests-targets.md index 7fbfd72..97d4b43 100644 --- a/documentation/docs/authoring/manifests-targets.md +++ b/documentation/docs/authoring/manifests-targets.md @@ -87,6 +87,24 @@ targets: Users and CI select targets instead of memorizing overlay paths. +## Translation coverage policy + +Translated targets can choose how strictly `brainbrew verify` treats untranslated fallbacks: + +```yaml +targets: + de-dev: + overlays: + - overlay.translation.de + translation_coverage: lenient + de-release: + overlays: + - overlay.translation.de + translation_coverage: strict +``` + +`lenient` is the default and allows missing translations while translators are in progress. `strict` fails verification when a translation overlay leaves a non-empty source string untranslated. Use `brainbrew translations --target ` to inspect the missing/stale entries, and `--apply` to seed `source: source` stubs before handing the file to translators. + ## Package-qualified targets A downstream package can extend an upstream target: diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index 8c044c0..ae86c0b 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -124,7 +124,36 @@ translations: ## Complete coverage, translator context, and sync/apply -When `require_complete: true`, every extracted non-empty translatable string must be translated by `direct`, translated by a matching `contextual` entry, or matched by `ignore_paths`. +Use `brainbrew translations` to inspect coverage before editing a translated overlay: + +```bash +brainbrew translations --manifest brainbrew.yaml --target de-standard +brainbrew translations --manifest brainbrew.yaml --all-targets --language de +brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.berlin +brainbrew translations --manifest brainbrew.yaml --target de-standard --path-prefix notes.note.berlin.fields.field.country +``` + +Report mode is the default and never modifies files. It groups extracted text into direct translations, contextual overrides, target-language additions, stale/invalid keys, and missing/untranslated fallbacks. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. + +To seed translator work after adding English notes or fields, run apply explicitly: + +```bash +brainbrew translations --manifest brainbrew.yaml --target de-standard --apply +``` + +`--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing fallbacks in scope. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. + +When `require_complete: true`, composition fails if any extracted non-empty translatable string is not translated by `direct`, translated by a matching `contextual` entry, or matched by `ignore_paths`. For release workflows, prefer target-level verification policy in `brainbrew.yaml`: + +```yaml +targets: + de-standard: + overlays: + - overlay.translation.de + translation_coverage: strict +``` + +`translation_coverage: lenient` is the default and allows untranslated fallbacks during development. `translation_coverage: strict` makes `brainbrew verify` fail when a translation overlay leaves missing fallbacks. The CLI flag `brainbrew verify --translation-coverage strict` can override target configuration for one run. Translator context views should present extracted strings in the same categories: diff --git a/documentation/docs/reference/yaml.md b/documentation/docs/reference/yaml.md index 4e78dbc..baf6c73 100644 --- a/documentation/docs/reference/yaml.md +++ b/documentation/docs/reference/yaml.md @@ -152,6 +152,7 @@ targets: de-standard: overlays: - overlay.translation.de + translation_coverage: strict # optional; lenient by default ``` ## Lock file From 6438e33ab90dc32cf3883fbc680192efcdc8adda Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 14:20:31 +0200 Subject: [PATCH 015/142] use local brainbrew in devenv --- devenv.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/devenv.nix b/devenv.nix index a58dd5e..8ab99b0 100644 --- a/devenv.nix +++ b/devenv.nix @@ -19,6 +19,9 @@ in echo 'Brain Brew dev environment ready' > /dev/null ''; + scripts.brainbrew.exec = '' + cargo run -q -p brainbrew -- "$@" + ''; scripts.fmt.exec = "cargo fmt --all"; scripts."fmt:check".exec = "cargo fmt --all -- --check"; scripts.check.exec = "cargo check --workspace --all-targets"; From 99bf9126357665910e60f3ea6d89a721122633ea Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 14:43:21 +0200 Subject: [PATCH 016/142] improve translation command UX --- .../src/commands/translations.rs | 1166 +++++++++++++++-- crates/brain-brew-cli/src/help.rs | 8 +- crates/brain-brew-cli/src/main.rs | 35 +- crates/brain-brew-cli/tests/cli.rs | 206 ++- documentation/docs/authoring/translations.md | 16 +- 5 files changed, 1280 insertions(+), 151 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 28e99a6..0885ff7 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -1,11 +1,14 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::env; use std::fs; +use std::io::{self, BufRead, IsTerminal, Write}; use std::path::{Path, PathBuf}; use brain_brew_core::{ CanonicalDeck, Overlay, TranslationCoverageCategory, TranslationCoverageEntry, TranslationCoverageReport, }; +use brain_brew_formats::manifest::FederatedDeckManifest; use serde_json::json; use crate::help; @@ -20,80 +23,41 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { ); return Ok(()); } - let args = parse_translation_args(args)?; - let manifest = read_manifest(&args.manifest_path)?; - let target_names = if args.all_targets || args.target.is_none() { - manifest.targets.keys().cloned().collect::>() - } else { - vec![args.target.clone().expect("target exists")] - }; - let mut reports = Vec::new(); - let mut stubs_by_file = BTreeMap::>::new(); - for target in &target_names { - let plan = plan_manifest_target_with_packages( - &args.manifest_path, - target, - &args.include_paths, - &args.package_roots, - )?; - let mut current = plan.base.clone(); - for (planned, overlay) in &plan.overlays { - if overlay.translations.is_some() { - if overlay_matches_scope(target, planned, overlay, &args) - && let Some(report) = current.translation_coverage(overlay) - { - let entries = report - .entries - .into_iter() - .filter(|entry| entry_matches_scope(entry, &args)) - .collect::>(); - if args.apply { - let stubs = entries - .iter() - .filter(|entry| { - entry.category == TranslationCoverageCategory::UntranslatedFallback - }) - .map(|entry| entry.source.clone()) - .collect::>(); - if !stubs.is_empty() { - stubs_by_file - .entry(planned.file.clone()) - .or_default() - .extend(stubs); - } - } - reports.push(ScopedTranslationReport { - target: target.clone(), - overlay_id: planned.id.clone(), - overlay_file: planned.display_file.clone(), - report: TranslationCoverageReport { - overlay_id: report.overlay_id, - entries, - }, - }); - } - current = compose_lenient_translation_overlay(¤t, overlay)?; - } else { - current = current - .compose(std::slice::from_ref(overlay)) - .map_err(|error| { - format!( - "failed to compose overlay {} for target {target}: {error}", - planned.id - ) - })?; - } + let mut args = parse_translation_args(args)?; + let interactive = should_use_interactive(&args); + if !args.manifest_path.exists() && !interactive { + return Err(missing_manifest_error(&args.manifest_path)); + } + + if interactive { + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut reader = stdin.lock(); + let mut writer = stdout.lock(); + configure_interactively(&mut args, &mut reader, &mut writer)?; + } + + let reports = collect_translation_reports(&args)?; + + let mut edits_by_file = BTreeMap::::new(); + if args.apply { + if interactive { + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut reader = stdin.lock(); + let mut writer = stdout.lock(); + edits_by_file = prompt_selective_apply(&reports, &mut reader, &mut writer)?; + } else { + edits_by_file = direct_stub_edits_from_reports(&reports); } } let mut applied = BTreeMap::new(); - if args.apply { - for (path, stubs) in &stubs_by_file { - let count = apply_direct_stubs(path, stubs)?; - if count > 0 { - applied.insert(path.display().to_string(), count); - } + for (path, edits) in &edits_by_file { + let count = apply_overlay_edits(path, edits)?; + if count > 0 { + applied.insert(path.display().to_string(), count); } } @@ -107,13 +71,14 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { .iter() .map(|(file, count)| (file.as_str(), count.to_string())) .collect::>(); - output::print_success(format!("applied {total} translation stub(s)"), &details); + output::print_success(format!("applied {total} translation edit(s)"), &details); } } Ok(()) } +#[derive(Clone)] struct TranslationArgs { manifest_path: PathBuf, target: Option, @@ -127,6 +92,7 @@ struct TranslationArgs { path_prefixes: Vec, apply: bool, json_output: bool, + interactive: Option, } fn parse_translation_args(args: &[String]) -> Result { @@ -143,6 +109,7 @@ fn parse_translation_args(args: &[String]) -> Result { path_prefixes: Vec::new(), apply: false, json_output: false, + interactive: None, }; let mut index = 0; while index < args.len() { @@ -222,22 +189,572 @@ fn parse_translation_args(args: &[String]) -> Result { parsed.json_output = true; index += 1; } + "--interactive" => { + parsed.interactive = Some(true); + index += 1; + } + "--no-interactive" => { + parsed.interactive = Some(false); + index += 1; + } other => return Err(format!("unexpected translations argument {other:?}")), } } if parsed.all_targets && parsed.target.is_some() { return Err("choose --all-targets or --target, not both".to_owned()); } + if parsed.interactive == Some(true) && parsed.json_output { + return Err("choose --interactive or --json, not both".to_owned()); + } Ok(parsed) } +fn should_use_interactive(args: &TranslationArgs) -> bool { + match args.interactive { + Some(true) => !args.json_output, + Some(false) => false, + None => { + !args.json_output + && io::stdin().is_terminal() + && io::stdout().is_terminal() + && (!args.manifest_path.exists() || (args.target.is_none() && !args.all_targets)) + } + } +} + +fn configure_interactively( + args: &mut TranslationArgs, + reader: &mut R, + writer: &mut W, +) -> Result<(), String> { + writeln!( + writer, + "{}", + color_stdout("Brain Brew translation coverage", "1;36") + ) + .map_err(|error| error.to_string())?; + writeln!(writer).map_err(|error| error.to_string())?; + + if !args.manifest_path.exists() { + let manifests = discover_nearby_manifests(); + if manifests.is_empty() { + return Err(missing_manifest_error(&args.manifest_path)); + } + let labels = manifests + .iter() + .map(|path| path.display().to_string()) + .collect::>(); + let choice = choose_index(reader, writer, "Manifest", &labels, 0)?; + args.manifest_path = manifests[choice].clone(); + } + + let manifest = read_manifest(&args.manifest_path)?; + if args.target.is_none() && !args.all_targets { + let mut target_values = manifest.targets.keys().cloned().collect::>(); + target_values.sort(); + let mut labels = target_values.clone(); + labels.push("all targets".to_owned()); + let choice = choose_index(reader, writer, "Target", &labels, 0)?; + if choice == target_values.len() { + args.all_targets = true; + } else { + args.target = Some(target_values[choice].clone()); + } + } + + if args.language.is_none() { + let languages = inferred_languages(&manifest); + if !languages.is_empty() { + let mut labels = vec!["all languages".to_owned()]; + labels.extend(languages.iter().cloned()); + let choice = choose_index(reader, writer, "Language filter", &labels, 0)?; + if choice > 0 { + args.language = Some(languages[choice - 1].clone()); + } + } + } + + if args.overlay.is_none() { + let overlays = translation_overlay_choices(args)?; + if !overlays.is_empty() { + let mut labels = vec!["all translation overlays".to_owned()]; + labels.extend(overlays.iter().map(|overlay| overlay.label.clone())); + let choice = choose_index(reader, writer, "Translation overlay", &labels, 0)?; + if choice > 0 { + args.overlay = Some(overlays[choice - 1].id.clone()); + } + } + } + + if args.note.is_none() && args.field.is_none() && args.path_prefixes.is_empty() { + let reports = collect_translation_reports(args)?; + configure_scope_interactively(args, &reports, reader, writer)?; + } + + if !args.apply { + let labels = vec![ + "report only".to_owned(), + "apply selected source→source stubs".to_owned(), + ]; + let choice = choose_index(reader, writer, "Mode", &labels, 0)?; + if choice == 1 { + args.apply = true; + } + } + + writeln!(writer).map_err(|error| error.to_string())?; + writeln!(writer, "Equivalent command:").map_err(|error| error.to_string())?; + writeln!(writer, " {}", equivalent_command(args)).map_err(|error| error.to_string())?; + writeln!(writer).map_err(|error| error.to_string())?; + writer.flush().map_err(|error| error.to_string())?; + Ok(()) +} + +fn configure_scope_interactively( + args: &mut TranslationArgs, + reports: &[ScopedTranslationReport], + reader: &mut R, + writer: &mut W, +) -> Result<(), String> { + let mut notes = BTreeSet::new(); + let mut fields = BTreeSet::new(); + let mut problem_paths = BTreeSet::new(); + for report in reports { + for entry in &report.report.entries { + if let Some(note) = note_id_from_path(&entry.path) { + notes.insert(note); + } + if let Some(field) = field_id_from_path(&entry.path) { + fields.insert(field); + } + if entry.category.is_problem() { + problem_paths.insert(entry.path.clone()); + } + } + } + + let labels = vec![ + "whole overlay".to_owned(), + "select note".to_owned(), + "select field".to_owned(), + "select path prefix".to_owned(), + "changed-path prefixes from diff".to_owned(), + ]; + let choice = choose_index(reader, writer, "Scope", &labels, 0)?; + match choice { + 0 => {} + 1 => { + let note_labels = notes.into_iter().collect::>(); + if note_labels.is_empty() { + writeln!(writer, "No note paths were found; using whole overlay.") + .map_err(|error| error.to_string())?; + } else { + let choice = choose_index(reader, writer, "Note", ¬e_labels, 0)?; + args.note = Some(note_labels[choice].clone()); + } + } + 2 => { + let field_labels = fields.into_iter().collect::>(); + if field_labels.is_empty() { + writeln!(writer, "No field paths were found; using whole overlay.") + .map_err(|error| error.to_string())?; + } else { + let choice = choose_index(reader, writer, "Field", &field_labels, 0)?; + args.field = Some(field_labels[choice].clone()); + } + } + 3 => { + if !problem_paths.is_empty() { + writeln!(writer, "Known problem paths:").map_err(|error| error.to_string())?; + for path in problem_paths.iter().take(12) { + writeln!(writer, " - {path}").map_err(|error| error.to_string())?; + } + } + let prefix = prompt_line(reader, writer, "Path prefix")?; + if !prefix.trim().is_empty() { + args.path_prefixes.push(prefix.trim().to_owned()); + } + } + 4 => { + let prefixes = prompt_line( + reader, + writer, + "Paste changed deck path prefixes, comma-separated", + )?; + args.path_prefixes.extend( + prefixes + .split(',') + .map(str::trim) + .filter(|prefix| !prefix.is_empty()) + .map(str::to_owned), + ); + } + _ => unreachable!(), + } + Ok(()) +} + +fn choose_index( + reader: &mut R, + writer: &mut W, + label: &str, + options: &[String], + default_index: usize, +) -> Result { + if options.is_empty() { + return Err(format!("no {label} options are available")); + } + writeln!(writer, "{label}:").map_err(|error| error.to_string())?; + for (index, option) in options.iter().enumerate() { + let marker = if index == default_index { ">" } else { " " }; + writeln!(writer, " {marker} {}. {option}", index + 1) + .map_err(|error| error.to_string())?; + } + loop { + write!(writer, "Select {label} [{}]: ", default_index + 1) + .map_err(|error| error.to_string())?; + writer.flush().map_err(|error| error.to_string())?; + let answer = read_trimmed_line(reader)?; + if answer.is_empty() { + return Ok(default_index); + } + if let Ok(number) = answer.parse::() + && (1..=options.len()).contains(&number) + { + return Ok(number - 1); + } + if let Some((index, _)) = options + .iter() + .enumerate() + .find(|(_, option)| option.eq_ignore_ascii_case(&answer)) + { + return Ok(index); + } + writeln!(writer, "Enter a number between 1 and {}.", options.len()) + .map_err(|error| error.to_string())?; + } +} + +fn prompt_line( + reader: &mut R, + writer: &mut W, + prompt: &str, +) -> Result { + write!(writer, "{prompt}: ").map_err(|error| error.to_string())?; + writer.flush().map_err(|error| error.to_string())?; + read_trimmed_line(reader) +} + +fn read_trimmed_line(reader: &mut R) -> Result { + let mut input = String::new(); + let bytes = reader + .read_line(&mut input) + .map_err(|error| error.to_string())?; + if bytes == 0 { + return Err("interactive input ended before a selection was made".to_owned()); + } + Ok(input.trim().to_owned()) +} + +#[derive(Clone)] struct ScopedTranslationReport { target: String, overlay_id: String, overlay_file: String, + overlay_path: PathBuf, report: TranslationCoverageReport, } +fn collect_translation_reports( + args: &TranslationArgs, +) -> Result, String> { + let manifest = read_manifest(&args.manifest_path)?; + let target_names = selected_target_names(&manifest, args); + + let mut reports = Vec::new(); + for target in &target_names { + let plan = plan_manifest_target_with_packages( + &args.manifest_path, + target, + &args.include_paths, + &args.package_roots, + )?; + let mut current = plan.base.clone(); + for (planned, overlay) in &plan.overlays { + if overlay.translations.is_some() { + if overlay_matches_scope(target, planned, overlay, args) + && let Some(report) = current.translation_coverage(overlay) + { + let entries = report + .entries + .into_iter() + .filter(|entry| entry_matches_scope(entry, args)) + .collect::>(); + reports.push(ScopedTranslationReport { + target: target.clone(), + overlay_id: planned.id.clone(), + overlay_file: planned.display_file.clone(), + overlay_path: planned.file.clone(), + report: TranslationCoverageReport { + overlay_id: report.overlay_id, + entries, + }, + }); + } + current = compose_lenient_translation_overlay(¤t, overlay)?; + } else { + current = current + .compose(std::slice::from_ref(overlay)) + .map_err(|error| { + format!( + "failed to compose overlay {} for target {target}: {error}", + planned.id + ) + })?; + } + } + } + Ok(reports) +} + +fn selected_target_names(manifest: &FederatedDeckManifest, args: &TranslationArgs) -> Vec { + if args.all_targets || args.target.is_none() { + manifest.targets.keys().cloned().collect::>() + } else { + vec![args.target.clone().expect("target exists")] + } +} + +#[derive(Clone, Debug, Default)] +struct OverlayEdits { + direct: BTreeSet, + contextual: BTreeMap>, + ignore_paths: BTreeSet, +} + +impl OverlayEdits { + fn is_empty(&self) -> bool { + self.direct.is_empty() && self.contextual.is_empty() && self.ignore_paths.is_empty() + } + + fn count(&self) -> usize { + self.direct.len() + + self.contextual.values().map(BTreeSet::len).sum::() + + self.ignore_paths.len() + } +} + +fn direct_stub_edits_from_reports( + reports: &[ScopedTranslationReport], +) -> BTreeMap { + let mut edits = BTreeMap::::new(); + for report in reports { + let stubs = report + .report + .entries + .iter() + .filter(|entry| entry.category == TranslationCoverageCategory::UntranslatedFallback) + .map(|entry| entry.source.clone()) + .collect::>(); + if !stubs.is_empty() { + edits + .entry(report.overlay_path.clone()) + .or_default() + .direct + .extend(stubs); + } + } + edits +} + +fn prompt_selective_apply( + reports: &[ScopedTranslationReport], + reader: &mut R, + writer: &mut W, +) -> Result, String> { + let rows = reports + .iter() + .enumerate() + .flat_map(|(report_index, report)| { + report + .report + .entries + .iter() + .enumerate() + .filter(|(_, entry)| entry.category.is_problem()) + .map(move |(entry_index, _)| (report_index, entry_index)) + }) + .collect::>(); + + if rows.is_empty() { + writeln!( + writer, + "No missing, stale, or invalid translation rows to apply." + ) + .map_err(|error| error.to_string())?; + return Ok(BTreeMap::new()); + } + + writeln!(writer, "Selectable translation rows:").map_err(|error| error.to_string())?; + for (row_index, (report_index, entry_index)) in rows.iter().enumerate() { + let report = &reports[*report_index]; + let entry = &report.report.entries[*entry_index]; + writeln!( + writer, + " {}. {} {} {} source={}", + row_index + 1, + report.target, + report.overlay_id, + entry.category.as_str(), + yaml_scalar(&entry.source) + ) + .map_err(|error| error.to_string())?; + writeln!(writer, " {}", entry.path).map_err(|error| error.to_string())?; + } + + let selected = loop { + let answer = prompt_line( + reader, + writer, + "Select rows to edit (numbers, all, none) [all]", + )?; + let answer = if answer.is_empty() { + "all".to_owned() + } else { + answer + }; + match parse_row_selection(&answer, rows.len()) { + Ok(selection) => break selection, + Err(error) => writeln!(writer, "{error}").map_err(|error| error.to_string())?, + } + }; + + let mut edits = BTreeMap::::new(); + for row_index in selected { + let (report_index, entry_index) = rows[row_index]; + let report = &reports[report_index]; + let entry = &report.report.entries[entry_index]; + match entry.category { + TranslationCoverageCategory::UntranslatedFallback => { + let context = contextual_context_for_entry(entry); + writeln!( + writer, + "{} at {} source={}", + entry.category.as_str(), + entry.path, + yaml_scalar(&entry.source) + ) + .map_err(|error| error.to_string())?; + writeln!(writer, " d. add direct source→source stub") + .map_err(|error| error.to_string())?; + writeln!( + writer, + " c. add contextual source→source stub at {context}" + ) + .map_err(|error| error.to_string())?; + writeln!(writer, " i. add ignore path for this deck path") + .map_err(|error| error.to_string())?; + writeln!(writer, " s. skip").map_err(|error| error.to_string())?; + let action = + prompt_action(reader, writer, "Action [d]", &["d", "c", "i", "s"], "d")?; + let file_edits = edits.entry(report.overlay_path.clone()).or_default(); + match action.as_str() { + "d" => { + file_edits.direct.insert(entry.source.clone()); + } + "c" => { + file_edits + .contextual + .entry(context) + .or_default() + .insert(entry.source.clone()); + } + "i" => { + file_edits.ignore_paths.insert(entry.path.clone()); + } + "s" => {} + _ => unreachable!(), + } + } + _ => { + writeln!( + writer, + "{} at {} is stale/invalid; no safe automatic rewrite is applied, skipping.", + entry.category.as_str(), + entry.path + ) + .map_err(|error| error.to_string())?; + } + } + } + + edits.retain(|_, edits| !edits.is_empty()); + if edits.is_empty() { + return Ok(edits); + } + + let confirm = prompt_action( + reader, + writer, + "Apply selected changes? [y]", + &["y", "n"], + "y", + )?; + if confirm == "y" { + Ok(edits) + } else { + Ok(BTreeMap::new()) + } +} + +fn parse_row_selection(input: &str, row_count: usize) -> Result, String> { + let input = input.trim(); + if input.eq_ignore_ascii_case("none") || input.eq_ignore_ascii_case("n") { + return Ok(Vec::new()); + } + if input.eq_ignore_ascii_case("all") || input.eq_ignore_ascii_case("a") { + return Ok((0..row_count).collect()); + } + let mut selected = BTreeSet::new(); + for part in input.split(',') { + let part = part.trim(); + let number = part + .parse::() + .map_err(|_| format!("invalid row selection {part:?}"))?; + if !(1..=row_count).contains(&number) { + return Err(format!("row {number} is outside 1..={row_count}")); + } + selected.insert(number - 1); + } + Ok(selected.into_iter().collect()) +} + +fn prompt_action( + reader: &mut R, + writer: &mut W, + prompt: &str, + valid: &[&str], + default: &str, +) -> Result { + loop { + let answer = prompt_line(reader, writer, prompt)?; + let action = if answer.is_empty() { + default.to_owned() + } else { + answer.to_ascii_lowercase() + }; + if valid.iter().any(|candidate| *candidate == action) { + return Ok(action); + } + writeln!(writer, "Choose one of: {}", valid.join(", ")) + .map_err(|error| error.to_string())?; + } +} + +fn contextual_context_for_entry(entry: &TranslationCoverageEntry) -> String { + context_parent_candidate(&entry.path).unwrap_or_else(|| entry.path.clone()) +} + fn overlay_matches_scope( target: &str, planned: &PlannedOverlay, @@ -395,48 +912,54 @@ fn print_human_reports(reports: &[ScopedTranslationReport]) { for report in reports { let counts = category_counts(&report.report.entries); println!( - "Translation coverage for target {} overlay {} ({})", - report.target, report.overlay_id, report.overlay_file + "{}", + color_stdout( + &format!( + "Translation coverage for target {} overlay {} ({})", + report.target, report.overlay_id, report.overlay_file + ), + "1;36" + ) ); println!( - " direct translations: {}", + " {}: {}", + color_stdout("direct translations", "32"), counts.get("direct_translation").copied().unwrap_or(0) ); println!( - " contextual overrides: {}", + " {}: {}", + color_stdout("contextual overrides", "36"), counts.get("contextual_override").copied().unwrap_or(0) ); println!( - " target-language additions: {}", + " {}: {}", + color_stdout("target-language additions", "35"), counts.get("target_language_addition").copied().unwrap_or(0) ); println!( - " stale/invalid keys: {}", + " {}: {}", + color_stdout("ignored entries", "2"), + counts.get("ignored_source").copied().unwrap_or(0) + ); + println!( + " {}: {}", + color_stdout("stale/invalid keys", "33"), report .report .entries .iter() - .filter(|entry| { - matches!( - entry.category, - TranslationCoverageCategory::StaleDirectKey - | TranslationCoverageCategory::StaleContextualKey - | TranslationCoverageCategory::StaleTargetAddition - | TranslationCoverageCategory::StaleVariableKey - | TranslationCoverageCategory::StaleAdapterIdKey - | TranslationCoverageCategory::InvalidTargetAddition - ) - }) + .filter(|entry| is_stale_or_invalid(entry.category)) .count() ); println!( - " missing/untranslated fallbacks: {}", + " {}: {}", + color_stdout("missing/untranslated fallbacks", "31"), counts.get("untranslated_fallback").copied().unwrap_or(0) ); for entry in report.report.problem_entries() { println!( " - {} {} source={} translated={}", - entry.category.as_str(), + color_category(entry.category, entry.category.as_str()), entry.path, yaml_scalar(&entry.source), entry @@ -449,6 +972,18 @@ fn print_human_reports(reports: &[ScopedTranslationReport]) { } } +fn is_stale_or_invalid(category: TranslationCoverageCategory) -> bool { + matches!( + category, + TranslationCoverageCategory::StaleDirectKey + | TranslationCoverageCategory::StaleContextualKey + | TranslationCoverageCategory::StaleTargetAddition + | TranslationCoverageCategory::StaleVariableKey + | TranslationCoverageCategory::StaleAdapterIdKey + | TranslationCoverageCategory::InvalidTargetAddition + ) +} + fn print_json_reports(reports: &[ScopedTranslationReport], applied: &BTreeMap) { let reports_json = reports .iter() @@ -490,74 +1025,148 @@ fn category_counts(entries: &[TranslationCoverageEntry]) -> BTreeMap<&'static st counts } -fn apply_direct_stubs(path: &Path, stubs: &BTreeSet) -> Result { - if stubs.is_empty() { +fn apply_overlay_edits(path: &Path, edits: &OverlayEdits) -> Result { + if edits.is_empty() { return Ok(0); } let input = fs::read_to_string(path).map_err(|error| format!("{}: {error}", path.display()))?; - let stub_lines = stubs + let mut output = input.clone(); + if !edits.ignore_paths.is_empty() { + output = insert_ignore_path_lines(&output, &edits.ignore_paths); + } + if !edits.direct.is_empty() { + output = insert_direct_stub_lines(&output, &edits.direct); + } + if !edits.contextual.is_empty() { + output = insert_contextual_stub_lines(&output, &edits.contextual); + } + if output != input { + fs::write(path, output).map_err(|error| format!("{}: {error}", path.display()))?; + } + Ok(edits.count()) +} + +fn insert_direct_stub_lines(input: &str, stubs: &BTreeSet) -> String { + let entries = stubs .iter() .map(|source| format!(" {}: {}\n", yaml_scalar(source), yaml_scalar(source))) .collect::(); - let output = insert_direct_stub_lines(&input, &stub_lines); - if output != input { - fs::write(path, output).map_err(|error| format!("{}: {error}", path.display()))?; + insert_translation_section( + input, + "direct", + &entries, + &["ignore_paths", "require_complete"], + ) +} + +fn insert_contextual_stub_lines(input: &str, stubs: &BTreeMap>) -> String { + let mut entries = String::new(); + for (context, sources) in stubs { + entries.push_str(&format!(" {}:\n", yaml_scalar(context))); + for source in sources { + entries.push_str(&format!( + " {}: {}\n", + yaml_scalar(source), + yaml_scalar(source) + )); + } } - Ok(stubs.len()) + insert_translation_section( + input, + "contextual", + &entries, + &["direct", "ignore_paths", "require_complete"], + ) } -fn insert_direct_stub_lines(input: &str, stub_lines: &str) -> String { - let spans = line_spans(input); - let translations_line = spans.iter().position(|(start, end)| { - let line = &input[*start..*end]; - line.trim_end() == "translations:" - }); +fn insert_ignore_path_lines(input: &str, paths: &BTreeSet) -> String { + let entries = paths + .iter() + .map(|path| format!(" - {}\n", yaml_scalar(path))) + .collect::(); + insert_translation_section(input, "ignore_paths", &entries, &["require_complete"]) +} - if let Some(translations_index) = translations_line { - let direct_index = spans +fn insert_translation_section( + input: &str, + section_name: &str, + entries: &str, + preferred_after_sections: &[&str], +) -> String { + let spans = line_spans(input); + let Some(translations_index) = find_translations_line(input, &spans) else { + let insert_after = spans .iter() - .enumerate() - .skip(translations_index + 1) - .take_while(|(_, (start, end))| { - let line = &input[*start..*end]; - line.trim().is_empty() - || line.trim_start().starts_with('#') - || line.starts_with(' ') - }) - .find(|(_, (start, end))| input[*start..*end].trim_end() == " direct:") - .map(|(index, _)| index); - if let Some(direct_index) = direct_index { - let insert_at = spans - .iter() - .enumerate() - .skip(direct_index + 1) - .find(|(_, (start, end))| { - let line = &input[*start..*end]; - !line.trim().is_empty() - && !line.trim_start().starts_with('#') - && (line.starts_with(" ") && !line.starts_with(" ") - || !line.starts_with(' ')) - }) - .map(|(_, (start, _))| *start) - .unwrap_or(input.len()); - return insert_at_byte(input, insert_at, stub_lines); - } + .find(|(start, end)| input[*start..*end].trim_start().starts_with("kind:")) + .map(|(_, end)| *end) + .or_else(|| spans.first().map(|(_, end)| *end)) + .unwrap_or(0); + return insert_at_byte( + input, + insert_after, + &format!("translations:\n {section_name}:\n{entries}"), + ); + }; + + if let Some(section_index) = + find_translation_section(input, &spans, translations_index, section_name) + { + let insert_at = translation_section_end(input, &spans, section_index); + return insert_at_byte(input, insert_at, entries); + } - let insert_at = spans[translations_index].1; - return insert_at_byte(input, insert_at, &format!(" direct:\n{stub_lines}")); + for anchor in preferred_after_sections { + if let Some(section_index) = + find_translation_section(input, &spans, translations_index, anchor) + { + let insert_at = translation_section_end(input, &spans, section_index); + return insert_at_byte(input, insert_at, &format!(" {section_name}:\n{entries}")); + } } - let insert_after = spans + let insert_at = spans[translations_index].1; + insert_at_byte(input, insert_at, &format!(" {section_name}:\n{entries}")) +} + +fn find_translations_line(input: &str, spans: &[(usize, usize)]) -> Option { + spans.iter().position(|(start, end)| { + let line = &input[*start..*end]; + line.trim_end() == "translations:" + }) +} + +fn find_translation_section( + input: &str, + spans: &[(usize, usize)], + translations_index: usize, + section_name: &str, +) -> Option { + let needle = format!(" {section_name}:"); + spans .iter() - .find(|(start, end)| input[*start..*end].trim_start().starts_with("kind:")) - .map(|(_, end)| *end) - .or_else(|| spans.first().map(|(_, end)| *end)) - .unwrap_or(0); - insert_at_byte( - input, - insert_after, - &format!("translations:\n direct:\n{stub_lines}"), - ) + .enumerate() + .skip(translations_index + 1) + .take_while(|(_, (start, end))| { + let line = &input[*start..*end]; + line.trim().is_empty() || line.trim_start().starts_with('#') || line.starts_with(' ') + }) + .find(|(_, (start, end))| input[*start..*end].trim_end() == needle) + .map(|(index, _)| index) +} + +fn translation_section_end(input: &str, spans: &[(usize, usize)], section_index: usize) -> usize { + spans + .iter() + .enumerate() + .skip(section_index + 1) + .find(|(_, (start, end))| { + let line = &input[*start..*end]; + !line.trim().is_empty() + && !line.trim_start().starts_with('#') + && ((line.starts_with(" ") && !line.starts_with(" ")) || !line.starts_with(' ')) + }) + .map(|(_, (start, _))| *start) + .unwrap_or(input.len()) } fn insert_at_byte(input: &str, index: usize, insertion: &str) -> String { @@ -586,6 +1195,245 @@ fn line_spans(input: &str) -> Vec<(usize, usize)> { spans } +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd)] +struct OverlayChoice { + id: String, + label: String, +} + +fn translation_overlay_choices(args: &TranslationArgs) -> Result, String> { + let manifest = read_manifest(&args.manifest_path)?; + let target_names = selected_target_names(&manifest, args); + let mut choices = BTreeSet::::new(); + for target in target_names { + let plan = plan_manifest_target_with_packages( + &args.manifest_path, + &target, + &args.include_paths, + &args.package_roots, + )?; + for (planned, overlay) in &plan.overlays { + if overlay.translations.is_some() + && args + .language + .as_ref() + .is_none_or(|language| language_matches(language, &target, planned, overlay)) + { + choices.insert(OverlayChoice { + id: planned.id.clone(), + label: format!("{} {}", planned.id, planned.display_file), + }); + } + } + } + Ok(choices.into_iter().collect()) +} + +fn inferred_languages(manifest: &FederatedDeckManifest) -> Vec { + let mut languages = BTreeSet::new(); + for target in manifest.targets.keys() { + if let Some((language, _)) = target.split_once('-') + && (2..=8).contains(&language.len()) + && language.chars().all(|ch| ch.is_ascii_lowercase()) + { + languages.insert(language.to_owned()); + } + } + for (id, overlay) in &manifest.overlays { + if let Some(language) = id.rsplit(['.', '-']).next() + && (2..=8).contains(&language.len()) + && language.chars().all(|ch| ch.is_ascii_lowercase()) + { + languages.insert(language.to_owned()); + } + if let Some(stem) = Path::new(&overlay.file) + .file_stem() + .and_then(|stem| stem.to_str()) + && (2..=8).contains(&stem.len()) + && stem.chars().all(|ch| ch.is_ascii_lowercase()) + { + languages.insert(stem.to_owned()); + } + } + languages.into_iter().collect() +} + +fn note_id_from_path(path: &str) -> Option { + let rest = path.strip_prefix("notes.")?; + let end = [".fields.", ".tags.", ".variables.", ".adapter_ids."] + .into_iter() + .filter_map(|marker| rest.find(marker)) + .min() + .unwrap_or(rest.len()); + Some(rest[..end].to_owned()) +} + +fn field_id_from_path(path: &str) -> Option { + let rest = path.split_once(".fields.")?.1; + let end = [".variables.", ".adapter_ids."] + .into_iter() + .filter_map(|marker| rest.find(marker)) + .min() + .unwrap_or(rest.len()); + Some(rest[..end].to_owned()) +} + +fn context_parent_candidate(path: &str) -> Option { + if let Some((note_context, _)) = path.split_once(".fields.") + && note_context.starts_with("notes.") + { + return Some(note_context.to_owned()); + } + if let Some((note_context, _)) = path.split_once(".tags.") + && note_context.starts_with("notes.") + { + return Some(note_context.to_owned()); + } + if let Some((note_context, _)) = path.split_once(".variables.") + && note_context.starts_with("notes.") + { + return Some(note_context.to_owned()); + } + if let Some((note_type_context, _)) = path.split_once(".fields.") + && note_type_context.starts_with("note_types.") + { + return Some(note_type_context.to_owned()); + } + if let Some((note_type_context, _)) = path.split_once(".card_templates.") + && note_type_context.starts_with("note_types.") + { + return Some(note_type_context.to_owned()); + } + None +} + +fn discover_nearby_manifests() -> Vec { + let mut results = BTreeSet::new(); + if let Ok(current) = env::current_dir() { + discover_manifests_under(¤t, ¤t, 0, &mut results); + for ancestor in current.ancestors().skip(1).take(4) { + let candidate = ancestor.join("brainbrew.yaml"); + if candidate.exists() { + results.insert(relative_to_current(&candidate)); + } + } + } + results.into_iter().take(20).collect() +} + +fn discover_manifests_under( + root: &Path, + dir: &Path, + depth: usize, + results: &mut BTreeSet, +) { + if depth > 4 { + return; + } + let Ok(entries) = fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + let name = entry.file_name(); + let name = name.to_string_lossy(); + if name == "target" + || name == ".git" + || name == ".jj" + || name == ".devenv" + || name == ".direnv" + || name == "node_modules" + { + continue; + } + if path.is_file() && name == "brainbrew.yaml" { + results.insert(path.strip_prefix(root).unwrap_or(&path).to_path_buf()); + } else if path.is_dir() { + discover_manifests_under(root, &path, depth + 1, results); + } + } +} + +fn relative_to_current(path: &Path) -> PathBuf { + env::current_dir() + .ok() + .and_then(|current| path.strip_prefix(current).ok().map(Path::to_path_buf)) + .unwrap_or_else(|| path.to_path_buf()) +} + +fn missing_manifest_error(path: &Path) -> String { + let mut message = format!("No Brain Brew manifest found at {}.", path.display()); + if path == Path::new("brainbrew.yaml") { + message.push_str("\n\nRun from a deck workspace, or pass --manifest ."); + } + let manifests = discover_nearby_manifests(); + if !manifests.is_empty() { + message.push_str("\n\nFound possible manifests:"); + for manifest in &manifests { + message.push_str(&format!("\n - {}", manifest.display())); + } + if let Some(first) = manifests.first() { + message.push_str("\n\nTry:"); + message.push_str(&format!( + "\n brainbrew translations --manifest {} --target ", + shell_word(&first.display().to_string()) + )); + message.push_str("\n brainbrew translations --interactive"); + } + } + message +} + +fn equivalent_command(args: &TranslationArgs) -> String { + let mut parts = vec!["brainbrew".to_owned(), "translations".to_owned()]; + push_flag_value( + &mut parts, + "--manifest", + &args.manifest_path.display().to_string(), + ); + if args.all_targets { + parts.push("--all-targets".to_owned()); + } else if let Some(target) = &args.target { + push_flag_value(&mut parts, "--target", target); + } + if let Some(language) = &args.language { + push_flag_value(&mut parts, "--language", language); + } + if let Some(overlay) = &args.overlay { + push_flag_value(&mut parts, "--overlay", overlay); + } + if let Some(note) = &args.note { + push_flag_value(&mut parts, "--note", note); + } + if let Some(field) = &args.field { + push_flag_value(&mut parts, "--field", field); + } + for prefix in &args.path_prefixes { + push_flag_value(&mut parts, "--path-prefix", prefix); + } + if args.apply { + parts.push("--apply".to_owned()); + } + parts.join(" ") +} + +fn push_flag_value(parts: &mut Vec, flag: &str, value: &str) { + parts.push(flag.to_owned()); + parts.push(shell_word(value)); +} + +fn shell_word(value: &str) -> String { + if !value.is_empty() + && value + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-' | ':' | '=')) + { + value.to_owned() + } else { + format!("'{}'", value.replace('\'', "'\\''")) + } +} + fn yaml_scalar(value: &str) -> String { if !value.is_empty() && !value.starts_with([ @@ -606,3 +1454,37 @@ fn yaml_scalar(value: &str) -> String { format!("'{}'", value.replace('\'', "''")) } } + +fn color_category(category: TranslationCoverageCategory, text: &str) -> String { + match category { + TranslationCoverageCategory::DirectTranslation => color_stdout(text, "32"), + TranslationCoverageCategory::ContextualOverride => color_stdout(text, "36"), + TranslationCoverageCategory::TargetLanguageAddition => color_stdout(text, "35"), + TranslationCoverageCategory::VariableTranslation + | TranslationCoverageCategory::AdapterIdTranslation + | TranslationCoverageCategory::IgnoredSource => color_stdout(text, "2"), + TranslationCoverageCategory::UntranslatedFallback => color_stdout(text, "31"), + TranslationCoverageCategory::StaleDirectKey + | TranslationCoverageCategory::StaleContextualKey + | TranslationCoverageCategory::StaleTargetAddition + | TranslationCoverageCategory::StaleVariableKey + | TranslationCoverageCategory::StaleAdapterIdKey + | TranslationCoverageCategory::InvalidTargetAddition => color_stdout(text, "33"), + } +} + +fn color_stdout(text: &str, code: &str) -> String { + if color_enabled(io::stdout().is_terminal()) { + format!("\x1b[{code}m{text}\x1b[0m") + } else { + text.to_owned() + } +} + +fn color_enabled(is_terminal: bool) -> bool { + match env::var("BRAINBREW_COLOR") { + Ok(value) if value == "always" => true, + Ok(value) if value == "never" => false, + _ => env::var_os("NO_COLOR").is_none() && is_terminal, + } +} diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index 95e7073..681b58a 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -13,7 +13,7 @@ pub(crate) fn general() -> String { " import Import CrowdAnki into CanonicalDeck YAML\n", " lock Update or verify locked federated package inputs\n", " targets List manifest targets\n", - " translations Report or apply translation coverage stubs\n", + " translations Report/apply translation coverage stubs (aliases: translate, translation)\n", " verify Run manifest formatting, composition, validation, media, and golden checks\n", " explain Explain a manifest target and its overlay stack\n", " diff Compare decks semantically, or emit an overlay draft\n\n", @@ -24,7 +24,7 @@ pub(crate) fn general() -> String { " brainbrew compose --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n", " brainbrew lock update --package anki-geo.ultimate-geography --path ../ultimate-geography\n", " brainbrew export crowdanki --manifest brainbrew.yaml --target de-extended --media-root media/\n", - " brainbrew translations --manifest brainbrew.yaml --target de-standard\n", + " brainbrew translate --manifest brainbrew.yaml --target de-standard\n", " brainbrew verify --manifest brainbrew.yaml --all-targets\n\n", "Run `brainbrew --help` for command-specific examples.\n", ), @@ -55,8 +55,8 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "targets" => Some( "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", ), - "translations" => Some( - "Usage:\n brainbrew translations [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--json]\n\nExamples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n\nReport mode is the default and never edits files. --apply inserts source->source stubs for missing untranslated fallbacks into translations.direct. Use --path-prefix with paths from a diff to scope a changed-base review.\n", + "translations" | "translate" | "translation" => Some( + "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open manifest-aware selectors for target, language, overlay, scope, and mode; the equivalent non-interactive command is printed. --apply inserts source->source stubs for missing untranslated fallbacks into translations.direct in non-interactive mode, while interactive apply lets you choose direct, contextual, ignore-path, or skip actions. Use --path-prefix with paths from a diff to scope a changed-base review.\n", ), "verify" => Some( "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", diff --git a/crates/brain-brew-cli/src/main.rs b/crates/brain-brew-cli/src/main.rs index 5945242..34a09fd 100644 --- a/crates/brain-brew-cli/src/main.rs +++ b/crates/brain-brew-cli/src/main.rs @@ -26,6 +26,8 @@ fn run() -> Result<(), String> { return Ok(()); }; + let command = canonical_command(command); + if args .get(1) .is_some_and(|arg| arg == "--help" || arg == "-h") @@ -55,8 +57,39 @@ fn run() -> Result<(), String> { println!("brainbrew {}", env!("CARGO_PKG_VERSION")); Ok(()) } - other => Err(format!("unknown command {other:?}")), + other => Err(unknown_command_error(other)), + } +} + +fn canonical_command(command: &str) -> &str { + match command { + "translate" | "translation" => "translations", + other => other, + } +} + +fn unknown_command_error(command: &str) -> String { + if command.starts_with("translat") || levenshtein_one_or_two(command, "translations") { + format!("unknown command {command:?}\n\nDid you mean:\n brainbrew translations") + } else { + format!("unknown command {command:?}") + } +} + +fn levenshtein_one_or_two(left: &str, right: &str) -> bool { + let mut previous = (0..=right.len()).collect::>(); + let mut current = vec![0; right.len() + 1]; + for (left_index, left_char) in left.chars().enumerate() { + current[0] = left_index + 1; + for (right_index, right_char) in right.chars().enumerate() { + let substitution = usize::from(left_char != right_char); + current[right_index + 1] = (previous[right_index + 1] + 1) + .min(current[right_index] + 1) + .min(previous[right_index] + substitution); + } + std::mem::swap(&mut previous, &mut current); } + previous[right.len()] <= 2 } fn print_usage() { diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 16bec8d..db16045 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -1,6 +1,7 @@ use std::fs; +use std::io::Write; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; use flate2::Compression; @@ -902,6 +903,175 @@ fn verify_checks_all_manifest_targets() { assert!(out.contains("verified 1 target")); } +#[test] +fn translate_aliases_run_translation_reports() { + let dir = temp_dir("translate-aliases"); + write_translation_workspace(&dir); + + for command in ["translate", "translation"] { + let output = run([ + command, + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--json", + ]); + + assert!( + output.status.success(), + "{command} stderr: {}", + stderr(&output) + ); + let json: serde_json::Value = serde_json::from_str(&stdout(&output)).unwrap(); + assert_eq!(json["reports"][0]["target"], "da-standard"); + } +} + +#[test] +fn unknown_translate_like_command_suggests_translations() { + let output = run(["translatons"]); + + assert!(!output.status.success()); + let err = stderr(&output); + assert!(err.contains("unknown command")); + assert!(err.contains("Did you mean:")); + assert!(err.contains("brainbrew translations")); +} + +#[test] +fn translations_missing_manifest_lists_nearby_manifests() { + let dir = temp_dir("translations-missing-manifest"); + let workspace = dir.join("decks/sample"); + fs::create_dir_all(&workspace).unwrap(); + write_translation_workspace(&workspace); + + let output = run_in_dir(["translations", "--no-interactive"], &dir); + + assert!(!output.status.success()); + let err = stderr(&output); + assert!(err.contains("No Brain Brew manifest found at brainbrew.yaml")); + assert!(err.contains("Found possible manifests:")); + assert!(err.contains("decks/sample/brainbrew.yaml")); + assert!(err.contains("brainbrew translations --manifest decks/sample/brainbrew.yaml")); +} + +#[test] +fn translations_human_output_can_be_colored_but_json_stays_plain() { + let dir = temp_dir("translations-color"); + write_translation_workspace(&dir); + let manifest = dir.join("brainbrew.yaml"); + + let colored = run_with_env( + [ + "translations", + "--manifest", + manifest.to_str().unwrap(), + "--target", + "da-standard", + ], + &[("BRAINBREW_COLOR", "always")], + ); + assert!(colored.status.success(), "stderr: {}", stderr(&colored)); + assert!(stdout(&colored).contains("\u{1b}[")); + + let json = run_with_env( + [ + "translations", + "--manifest", + manifest.to_str().unwrap(), + "--target", + "da-standard", + "--json", + ], + &[("BRAINBREW_COLOR", "always")], + ); + assert!(json.status.success(), "stderr: {}", stderr(&json)); + assert!(!stdout(&json).contains("\u{1b}[")); +} + +#[test] +fn translations_interactive_derives_selector_options_and_prints_equivalent_command() { + let dir = temp_dir("translations-interactive-selectors"); + write_translation_workspace(&dir); + + let output = run_with_stdin( + [ + "translate", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--interactive", + ], + "2\n\n\n\n\n", + ); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("Brain Brew translation coverage")); + assert!(out.contains("Target:")); + assert!(out.contains("da-release")); + assert!(out.contains("da-standard")); + assert!(out.contains("Translation overlay:")); + assert!(out.contains("overlay.translation.da")); + assert!(out.contains("Scope:")); + assert!(out.contains("Equivalent command:")); + assert!(out.contains("brainbrew translations")); + assert!(out.contains("--target da-standard")); +} + +#[test] +fn translations_interactive_apply_can_insert_contextual_stub() { + let dir = temp_dir("translations-interactive-contextual"); + write_translation_workspace(&dir); + let overlay_path = dir.join("da.yaml"); + + let output = run_with_stdin( + [ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--path-prefix", + "notes.note.sweden.fields.field.country", + "--apply", + "--interactive", + ], + "\n\n\nc\ny\n", + ); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let updated = fs::read_to_string(overlay_path).unwrap(); + assert!(updated.contains(" notes.note.sweden:\n Sweden: Sweden\n")); +} + +#[test] +fn translations_interactive_apply_can_insert_ignore_path() { + let dir = temp_dir("translations-interactive-ignore"); + write_translation_workspace(&dir); + let overlay_path = dir.join("da.yaml"); + + let output = run_with_stdin( + [ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--path-prefix", + "notes.note.sweden.fields.field.country", + "--apply", + "--interactive", + ], + "\n\n\ni\ny\n", + ); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let updated = fs::read_to_string(overlay_path).unwrap(); + assert!(updated.contains(" ignore_paths:\n")); + assert!(updated.contains(" - notes.note.sweden.fields.field.country\n")); +} + #[test] fn translations_reports_missing_stale_contextual_and_additions_without_modifying() { let dir = temp_dir("translations-report"); @@ -1636,6 +1806,40 @@ fn run(args: [&str; N]) -> std::process::Output { .expect("command runs") } +fn run_in_dir(args: [&str; N], cwd: &Path) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_brainbrew")) + .args(args) + .current_dir(cwd) + .output() + .expect("command runs") +} + +fn run_with_env(args: [&str; N], envs: &[(&str, &str)]) -> std::process::Output { + let mut command = Command::new(env!("CARGO_BIN_EXE_brainbrew")); + command.args(args); + for (name, value) in envs { + command.env(name, value); + } + command.output().expect("command runs") +} + +fn run_with_stdin(args: [&str; N], stdin: &str) -> std::process::Output { + let mut child = Command::new(env!("CARGO_BIN_EXE_brainbrew")) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("command spawns"); + child + .stdin + .as_mut() + .expect("stdin is piped") + .write_all(stdin.as_bytes()) + .expect("stdin writes"); + child.wait_with_output().expect("command runs") +} + fn run_with_cache(args: [&str; N], cache: &Path) -> std::process::Output { Command::new(env!("CARGO_BIN_EXE_brainbrew")) .args(args) diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index ae86c0b..37a8f09 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -124,16 +124,26 @@ translations: ## Complete coverage, translator context, and sync/apply -Use `brainbrew translations` to inspect coverage before editing a translated overlay: +Use `brainbrew translate` (aliases: `translation`, `translations`) to inspect coverage before editing a translated overlay. In an interactive terminal, running it with missing choices opens a manifest-aware selector: + +```bash +brainbrew translate +brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml +``` + +The interactive workflow lets you choose from the known manifests, targets, languages, translation overlays, notes, fields, path prefixes, and report/apply mode. It prints the equivalent non-interactive command before running so the same review can be repeated in CI or shared with another maintainer. + +For scripts and CI, pass the scope explicitly: ```bash brainbrew translations --manifest brainbrew.yaml --target de-standard brainbrew translations --manifest brainbrew.yaml --all-targets --language de brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.berlin brainbrew translations --manifest brainbrew.yaml --target de-standard --path-prefix notes.note.berlin.fields.field.country +brainbrew translations --manifest brainbrew.yaml --target de-standard --json ``` -Report mode is the default and never modifies files. It groups extracted text into direct translations, contextual overrides, target-language additions, stale/invalid keys, and missing/untranslated fallbacks. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. +Report mode is the default and never modifies files. It groups extracted text into direct translations, contextual overrides, target-language additions, ignored entries, stale/invalid keys, and missing/untranslated fallbacks. Human output is colored when terminal color is enabled; JSON output is stable and uncolored. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. To seed translator work after adding English notes or fields, run apply explicitly: @@ -141,7 +151,7 @@ To seed translator work after adding English notes or fields, run apply explicit brainbrew translations --manifest brainbrew.yaml --target de-standard --apply ``` -`--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing fallbacks in scope. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. +Non-interactive `--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing fallbacks in scope. Interactive apply is selective: choose rows, then choose direct `source: source`, contextual `source: source` at the suggested safe context, an `ignore_paths` entry, or skip. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. When `require_complete: true`, composition fails if any extracted non-empty translatable string is not translated by `direct`, translated by a matching `contextual` entry, or matched by `ignore_paths`. For release workflows, prefer target-level verification policy in `brainbrew.yaml`: From 375a0df6a5b92ec3ac4c1dd640d626d28baf5410 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 14:53:02 +0200 Subject: [PATCH 017/142] make translation workflow a real arrow-key TUI --- Cargo.lock | 163 ++++- crates/brain-brew-cli/Cargo.toml | 1 + .../src/commands/translations.rs | 692 ++++++++++++------ crates/brain-brew-cli/src/help.rs | 2 +- crates/brain-brew-cli/tests/cli.rs | 63 +- documentation/docs/authoring/translations.md | 4 +- 6 files changed, 684 insertions(+), 241 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff8c29c..3a17cd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,6 +57,7 @@ dependencies = [ "base64", "brain-brew-core", "brain-brew-formats", + "crossterm", "flate2", "nix-nar", "serde_json", @@ -106,6 +107,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -403,6 +429,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -415,6 +447,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.29" @@ -437,6 +478,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "nix-nar" version = "0.4.0" @@ -455,6 +508,29 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -504,6 +580,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "ring" version = "0.17.14" @@ -518,6 +603,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -527,7 +625,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -572,6 +670,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.28" @@ -651,6 +755,37 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -723,7 +858,7 @@ dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -903,6 +1038,28 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" @@ -1172,7 +1329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] diff --git a/crates/brain-brew-cli/Cargo.toml b/crates/brain-brew-cli/Cargo.toml index 6c6ae7d..0cf9f0a 100644 --- a/crates/brain-brew-cli/Cargo.toml +++ b/crates/brain-brew-cli/Cargo.toml @@ -20,6 +20,7 @@ path = "src/main.rs" [dependencies] base64 = "0.22" +crossterm = "0.28" flate2 = "1" nix-nar = "0.4" brain-brew-core.workspace = true diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 0885ff7..36f6b7e 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -1,14 +1,15 @@ use std::collections::{BTreeMap, BTreeSet}; use std::env; use std::fs; -use std::io::{self, BufRead, IsTerminal, Write}; +use std::io::{self, IsTerminal, Read, Write}; use std::path::{Path, PathBuf}; use brain_brew_core::{ - CanonicalDeck, Overlay, TranslationCoverageCategory, TranslationCoverageEntry, + CanonicalDeck, Overlay, OverlayKind, TranslationCoverageCategory, TranslationCoverageEntry, TranslationCoverageReport, }; use brain_brew_formats::manifest::FederatedDeckManifest; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use serde_json::json; use crate::help; @@ -31,11 +32,12 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { } if interactive { + let raw_mode = io::stdin().is_terminal() && io::stdout().is_terminal(); let stdin = io::stdin(); let stdout = io::stdout(); let mut reader = stdin.lock(); let mut writer = stdout.lock(); - configure_interactively(&mut args, &mut reader, &mut writer)?; + configure_interactively(&mut args, &mut reader, &mut writer, raw_mode)?; } let reports = collect_translation_reports(&args)?; @@ -43,11 +45,12 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { let mut edits_by_file = BTreeMap::::new(); if args.apply { if interactive { + let raw_mode = io::stdin().is_terminal() && io::stdout().is_terminal(); let stdin = io::stdin(); let stdout = io::stdout(); let mut reader = stdin.lock(); let mut writer = stdout.lock(); - edits_by_file = prompt_selective_apply(&reports, &mut reader, &mut writer)?; + edits_by_file = prompt_selective_apply(&reports, &mut reader, &mut writer, raw_mode)?; } else { edits_by_file = direct_stub_edits_from_reports(&reports); } @@ -64,7 +67,11 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { if args.json_output { print_json_reports(&reports, &applied); } else { - print_human_reports(&reports); + if reports.is_empty() { + print_no_translation_reports(&args)?; + } else { + print_human_reports(&reports); + } if args.apply { let total = applied.values().sum::(); let details = applied @@ -222,18 +229,14 @@ fn should_use_interactive(args: &TranslationArgs) -> bool { } } -fn configure_interactively( +fn configure_interactively( args: &mut TranslationArgs, reader: &mut R, writer: &mut W, + raw_mode: bool, ) -> Result<(), String> { - writeln!( - writer, - "{}", - color_stdout("Brain Brew translation coverage", "1;36") - ) - .map_err(|error| error.to_string())?; - writeln!(writer).map_err(|error| error.to_string())?; + let mut ui = TerminalUi::new(reader, writer, raw_mode)?; + ui.message("Brain Brew translation coverage")?; if !args.manifest_path.exists() { let manifests = discover_nearby_manifests(); @@ -244,7 +247,7 @@ fn configure_interactively( .iter() .map(|path| path.display().to_string()) .collect::>(); - let choice = choose_index(reader, writer, "Manifest", &labels, 0)?; + let choice = ui.select_one("Manifest", &labels, 0)?; args.manifest_path = manifests[choice].clone(); } @@ -254,7 +257,7 @@ fn configure_interactively( target_values.sort(); let mut labels = target_values.clone(); labels.push("all targets".to_owned()); - let choice = choose_index(reader, writer, "Target", &labels, 0)?; + let choice = ui.select_one("Target", &labels, 0)?; if choice == target_values.len() { args.all_targets = true; } else { @@ -267,7 +270,7 @@ fn configure_interactively( if !languages.is_empty() { let mut labels = vec!["all languages".to_owned()]; labels.extend(languages.iter().cloned()); - let choice = choose_index(reader, writer, "Language filter", &labels, 0)?; + let choice = ui.select_one("Language filter", &labels, 0)?; if choice > 0 { args.language = Some(languages[choice - 1].clone()); } @@ -279,7 +282,7 @@ fn configure_interactively( if !overlays.is_empty() { let mut labels = vec!["all translation overlays".to_owned()]; labels.extend(overlays.iter().map(|overlay| overlay.label.clone())); - let choice = choose_index(reader, writer, "Translation overlay", &labels, 0)?; + let choice = ui.select_one("Translation overlay", &labels, 0)?; if choice > 0 { args.overlay = Some(overlays[choice - 1].id.clone()); } @@ -288,7 +291,7 @@ fn configure_interactively( if args.note.is_none() && args.field.is_none() && args.path_prefixes.is_empty() { let reports = collect_translation_reports(args)?; - configure_scope_interactively(args, &reports, reader, writer)?; + configure_scope_interactively(args, &reports, &mut ui)?; } if !args.apply { @@ -296,25 +299,19 @@ fn configure_interactively( "report only".to_owned(), "apply selected source→source stubs".to_owned(), ]; - let choice = choose_index(reader, writer, "Mode", &labels, 0)?; + let choice = ui.select_one("Mode", &labels, 0)?; if choice == 1 { args.apply = true; } } - writeln!(writer).map_err(|error| error.to_string())?; - writeln!(writer, "Equivalent command:").map_err(|error| error.to_string())?; - writeln!(writer, " {}", equivalent_command(args)).map_err(|error| error.to_string())?; - writeln!(writer).map_err(|error| error.to_string())?; - writer.flush().map_err(|error| error.to_string())?; - Ok(()) + ui.finish_with_equivalent_command(args) } -fn configure_scope_interactively( +fn configure_scope_interactively( args: &mut TranslationArgs, reports: &[ScopedTranslationReport], - reader: &mut R, - writer: &mut W, + ui: &mut TerminalUi<'_, R, W>, ) -> Result<(), String> { let mut notes = BTreeSet::new(); let mut fields = BTreeSet::new(); @@ -340,46 +337,44 @@ fn configure_scope_interactively( "select path prefix".to_owned(), "changed-path prefixes from diff".to_owned(), ]; - let choice = choose_index(reader, writer, "Scope", &labels, 0)?; + let choice = ui.select_one("Scope", &labels, 0)?; match choice { 0 => {} 1 => { let note_labels = notes.into_iter().collect::>(); if note_labels.is_empty() { - writeln!(writer, "No note paths were found; using whole overlay.") - .map_err(|error| error.to_string())?; + ui.message("No note paths were found; using whole overlay.")?; } else { - let choice = choose_index(reader, writer, "Note", ¬e_labels, 0)?; + let choice = ui.select_one("Note", ¬e_labels, 0)?; args.note = Some(note_labels[choice].clone()); } } 2 => { let field_labels = fields.into_iter().collect::>(); if field_labels.is_empty() { - writeln!(writer, "No field paths were found; using whole overlay.") - .map_err(|error| error.to_string())?; + ui.message("No field paths were found; using whole overlay.")?; } else { - let choice = choose_index(reader, writer, "Field", &field_labels, 0)?; + let choice = ui.select_one("Field", &field_labels, 0)?; args.field = Some(field_labels[choice].clone()); } } 3 => { + let mut body = Vec::new(); if !problem_paths.is_empty() { - writeln!(writer, "Known problem paths:").map_err(|error| error.to_string())?; + body.push("Known problem paths:".to_owned()); for path in problem_paths.iter().take(12) { - writeln!(writer, " - {path}").map_err(|error| error.to_string())?; + body.push(format!(" - {path}")); } } - let prefix = prompt_line(reader, writer, "Path prefix")?; + let prefix = ui.prompt_text("Path prefix", &body)?; if !prefix.trim().is_empty() { args.path_prefixes.push(prefix.trim().to_owned()); } } 4 => { - let prefixes = prompt_line( - reader, - writer, - "Paste changed deck path prefixes, comma-separated", + let prefixes = ui.prompt_text( + "Changed deck path prefixes", + &["Paste changed deck path prefixes, comma-separated.".to_owned()], )?; args.path_prefixes.extend( prefixes @@ -394,66 +389,297 @@ fn configure_scope_interactively( Ok(()) } -fn choose_index( - reader: &mut R, - writer: &mut W, - label: &str, - options: &[String], - default_index: usize, -) -> Result { - if options.is_empty() { - return Err(format!("no {label} options are available")); - } - writeln!(writer, "{label}:").map_err(|error| error.to_string())?; - for (index, option) in options.iter().enumerate() { - let marker = if index == default_index { ">" } else { " " }; - writeln!(writer, " {marker} {}. {option}", index + 1) +struct TerminalUi<'a, R: Read, W: Write> { + reader: &'a mut R, + writer: &'a mut W, + raw_mode: bool, +} + +impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { + fn new(reader: &'a mut R, writer: &'a mut W, raw_mode: bool) -> Result { + if raw_mode { + enable_raw_mode().map_err(|error| error.to_string())?; + write!(writer, "\x1b[?25l").map_err(|error| error.to_string())?; + } + Ok(Self { + reader, + writer, + raw_mode, + }) + } + + fn message(&mut self, message: &str) -> Result<(), String> { + if self.raw_mode { + self.clear()?; + writeln!(self.writer, "{}", color_stdout(message, "1;36")) + .map_err(|error| error.to_string())?; + writeln!(self.writer).map_err(|error| error.to_string())?; + } else { + writeln!(self.writer, "{}", color_stdout(message, "1;36")) + .map_err(|error| error.to_string())?; + writeln!(self.writer).map_err(|error| error.to_string())?; + } + self.writer.flush().map_err(|error| error.to_string()) + } + + fn finish_with_equivalent_command(&mut self, args: &TranslationArgs) -> Result<(), String> { + if self.raw_mode { + self.clear()?; + } + writeln!(self.writer, "Equivalent command:").map_err(|error| error.to_string())?; + writeln!(self.writer, " {}", equivalent_command(args)) .map_err(|error| error.to_string())?; + writeln!(self.writer).map_err(|error| error.to_string())?; + self.writer.flush().map_err(|error| error.to_string()) + } + + fn select_one( + &mut self, + label: &str, + options: &[String], + default_index: usize, + ) -> Result { + if options.is_empty() { + return Err(format!("no {label} options are available")); + } + let mut selected = default_index.min(options.len() - 1); + loop { + self.render_select_one(label, options, selected)?; + match self.read_key()? { + TuiKey::Up => selected = selected.saturating_sub(1), + TuiKey::Down => selected = (selected + 1).min(options.len() - 1), + TuiKey::Enter => return Ok(selected), + TuiKey::Char('k') => selected = selected.saturating_sub(1), + TuiKey::Char('j') => selected = (selected + 1).min(options.len() - 1), + TuiKey::Char('q') | TuiKey::Esc => { + return Err("interactive selection cancelled".to_owned()); + } + _ => {} + } + } } - loop { - write!(writer, "Select {label} [{}]: ", default_index + 1) + + fn select_many( + &mut self, + label: &str, + options: &[String], + selected_by_default: bool, + ) -> Result, String> { + if options.is_empty() { + return Ok(Vec::new()); + } + let mut cursor = 0; + let mut selected = vec![selected_by_default; options.len()]; + loop { + self.render_select_many(label, options, &selected, cursor)?; + match self.read_key()? { + TuiKey::Up => cursor = cursor.saturating_sub(1), + TuiKey::Down => cursor = (cursor + 1).min(options.len() - 1), + TuiKey::Char('k') => cursor = cursor.saturating_sub(1), + TuiKey::Char('j') => cursor = (cursor + 1).min(options.len() - 1), + TuiKey::Space => selected[cursor] = !selected[cursor], + TuiKey::Char('a') => { + let all_selected = selected.iter().all(|value| *value); + selected.fill(!all_selected); + } + TuiKey::Enter => { + return Ok(selected + .iter() + .enumerate() + .filter_map(|(index, selected)| selected.then_some(index)) + .collect()); + } + TuiKey::Char('q') | TuiKey::Esc => { + return Err("interactive selection cancelled".to_owned()); + } + _ => {} + } + } + } + + fn prompt_text(&mut self, label: &str, body: &[String]) -> Result { + let mut input = String::new(); + loop { + self.render_text_prompt(label, body, &input)?; + match self.read_key()? { + TuiKey::Enter => return Ok(input), + TuiKey::Backspace => { + input.pop(); + } + TuiKey::Char(ch) => input.push(ch), + TuiKey::Esc => return Err("interactive text entry cancelled".to_owned()), + _ => {} + } + } + } + + fn render_select_one( + &mut self, + label: &str, + options: &[String], + selected: usize, + ) -> Result<(), String> { + self.clear_if_raw()?; + writeln!(self.writer, "{}", color_stdout(label, "1;36")) .map_err(|error| error.to_string())?; - writer.flush().map_err(|error| error.to_string())?; - let answer = read_trimmed_line(reader)?; - if answer.is_empty() { - return Ok(default_index); + writeln!(self.writer).map_err(|error| error.to_string())?; + for (index, option) in options.iter().enumerate() { + let marker = if index == selected { "›" } else { " " }; + let option = if index == selected { + color_stdout(option, "1;32") + } else { + option.clone() + }; + writeln!(self.writer, " {marker} {option}").map_err(|error| error.to_string())?; } - if let Ok(number) = answer.parse::() - && (1..=options.len()).contains(&number) - { - return Ok(number - 1); + writeln!(self.writer).map_err(|error| error.to_string())?; + writeln!(self.writer, "↑/↓ move • Enter select • q cancel") + .map_err(|error| error.to_string())?; + self.writer.flush().map_err(|error| error.to_string()) + } + + fn render_select_many( + &mut self, + label: &str, + options: &[String], + selected: &[bool], + cursor: usize, + ) -> Result<(), String> { + self.clear_if_raw()?; + writeln!(self.writer, "{}", color_stdout(label, "1;36")) + .map_err(|error| error.to_string())?; + writeln!(self.writer).map_err(|error| error.to_string())?; + for (index, option) in options.iter().enumerate() { + let cursor_marker = if index == cursor { "›" } else { " " }; + let selected_marker = if selected[index] { "[x]" } else { "[ ]" }; + let line = format!("{selected_marker} {option}"); + let line = if index == cursor { + color_stdout(&line, "1;32") + } else { + line + }; + writeln!(self.writer, " {cursor_marker} {line}").map_err(|error| error.to_string())?; } - if let Some((index, _)) = options - .iter() - .enumerate() - .find(|(_, option)| option.eq_ignore_ascii_case(&answer)) - { - return Ok(index); + writeln!(self.writer).map_err(|error| error.to_string())?; + writeln!( + self.writer, + "↑/↓ move • Space toggle • a toggle all • Enter confirm • q cancel" + ) + .map_err(|error| error.to_string())?; + self.writer.flush().map_err(|error| error.to_string()) + } + + fn render_text_prompt( + &mut self, + label: &str, + body: &[String], + input: &str, + ) -> Result<(), String> { + self.clear_if_raw()?; + writeln!(self.writer, "{}", color_stdout(label, "1;36")) + .map_err(|error| error.to_string())?; + writeln!(self.writer).map_err(|error| error.to_string())?; + for line in body { + writeln!(self.writer, "{line}").map_err(|error| error.to_string())?; } - writeln!(writer, "Enter a number between 1 and {}.", options.len()) + if !body.is_empty() { + writeln!(self.writer).map_err(|error| error.to_string())?; + } + writeln!(self.writer, "> {input}").map_err(|error| error.to_string())?; + writeln!(self.writer).map_err(|error| error.to_string())?; + writeln!(self.writer, "Type text • Enter confirm • Esc cancel") .map_err(|error| error.to_string())?; + self.writer.flush().map_err(|error| error.to_string()) + } + + fn clear_if_raw(&mut self) -> Result<(), String> { + if self.raw_mode { + self.clear()?; + } + Ok(()) + } + + fn clear(&mut self) -> Result<(), String> { + write!(self.writer, "\x1b[2J\x1b[H").map_err(|error| error.to_string()) + } + + fn read_key(&mut self) -> Result { + if self.raw_mode { + return read_terminal_key(); + } + read_scripted_key(self.reader) } } -fn prompt_line( - reader: &mut R, - writer: &mut W, - prompt: &str, -) -> Result { - write!(writer, "{prompt}: ").map_err(|error| error.to_string())?; - writer.flush().map_err(|error| error.to_string())?; - read_trimmed_line(reader) +impl Drop for TerminalUi<'_, R, W> { + fn drop(&mut self) { + if self.raw_mode { + let _ = write!(self.writer, "\x1b[?25h"); + let _ = self.writer.flush(); + let _ = disable_raw_mode(); + } + } } -fn read_trimmed_line(reader: &mut R) -> Result { - let mut input = String::new(); - let bytes = reader - .read_line(&mut input) - .map_err(|error| error.to_string())?; - if bytes == 0 { +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum TuiKey { + Up, + Down, + Enter, + Space, + Backspace, + Esc, + Char(char), + Other, +} + +fn read_terminal_key() -> Result { + loop { + let event = crossterm::event::read().map_err(|error| error.to_string())?; + let crossterm::event::Event::Key(key) = event else { + continue; + }; + use crossterm::event::KeyCode; + return Ok(match key.code { + KeyCode::Up => TuiKey::Up, + KeyCode::Down => TuiKey::Down, + KeyCode::Enter => TuiKey::Enter, + KeyCode::Char(' ') => TuiKey::Space, + KeyCode::Char(ch) => TuiKey::Char(ch), + KeyCode::Backspace => TuiKey::Backspace, + KeyCode::Esc => TuiKey::Esc, + _ => TuiKey::Other, + }); + } +} + +fn read_scripted_key(reader: &mut R) -> Result { + let mut byte = [0_u8; 1]; + let count = reader.read(&mut byte).map_err(|error| error.to_string())?; + if count == 0 { return Err("interactive input ended before a selection was made".to_owned()); } - Ok(input.trim().to_owned()) + match byte[0] { + b'\n' | b'\r' => Ok(TuiKey::Enter), + b' ' => Ok(TuiKey::Space), + 8 | 127 => Ok(TuiKey::Backspace), + 27 => read_escape_sequence(reader), + byte if byte.is_ascii() && !byte.is_ascii_control() => Ok(TuiKey::Char(byte as char)), + _ => Ok(TuiKey::Other), + } +} + +fn read_escape_sequence(reader: &mut R) -> Result { + let mut rest = [0_u8; 2]; + let count = reader.read(&mut rest).map_err(|error| error.to_string())?; + if count < 2 || rest[0] != b'[' { + return Ok(TuiKey::Esc); + } + Ok(match rest[1] { + b'A' => TuiKey::Up, + b'B' => TuiKey::Down, + _ => TuiKey::Other, + }) } #[derive(Clone)] @@ -567,11 +793,13 @@ fn direct_stub_edits_from_reports( edits } -fn prompt_selective_apply( +fn prompt_selective_apply( reports: &[ScopedTranslationReport], reader: &mut R, writer: &mut W, + raw_mode: bool, ) -> Result, String> { + let mut ui = TerminalUi::new(reader, writer, raw_mode)?; let rows = reports .iter() .enumerate() @@ -587,47 +815,26 @@ fn prompt_selective_apply( .collect::>(); if rows.is_empty() { - writeln!( - writer, - "No missing, stale, or invalid translation rows to apply." - ) - .map_err(|error| error.to_string())?; + ui.message("No missing, stale, or invalid translation rows to apply.")?; return Ok(BTreeMap::new()); } - writeln!(writer, "Selectable translation rows:").map_err(|error| error.to_string())?; - for (row_index, (report_index, entry_index)) in rows.iter().enumerate() { - let report = &reports[*report_index]; - let entry = &report.report.entries[*entry_index]; - writeln!( - writer, - " {}. {} {} {} source={}", - row_index + 1, - report.target, - report.overlay_id, - entry.category.as_str(), - yaml_scalar(&entry.source) - ) - .map_err(|error| error.to_string())?; - writeln!(writer, " {}", entry.path).map_err(|error| error.to_string())?; - } - - let selected = loop { - let answer = prompt_line( - reader, - writer, - "Select rows to edit (numbers, all, none) [all]", - )?; - let answer = if answer.is_empty() { - "all".to_owned() - } else { - answer - }; - match parse_row_selection(&answer, rows.len()) { - Ok(selection) => break selection, - Err(error) => writeln!(writer, "{error}").map_err(|error| error.to_string())?, - } - }; + let row_labels = rows + .iter() + .map(|(report_index, entry_index)| { + let report = &reports[*report_index]; + let entry = &report.report.entries[*entry_index]; + format!( + "{} {} {} source={} — {}", + report.target, + report.overlay_id, + entry.category.as_str(), + yaml_scalar(&entry.source), + entry.path + ) + }) + .collect::>(); + let selected = ui.select_many("Rows to edit", &row_labels, true)?; let mut edits = BTreeMap::::new(); for row_index in selected { @@ -637,53 +844,47 @@ fn prompt_selective_apply( match entry.category { TranslationCoverageCategory::UntranslatedFallback => { let context = contextual_context_for_entry(entry); - writeln!( - writer, - "{} at {} source={}", - entry.category.as_str(), - entry.path, - yaml_scalar(&entry.source) - ) - .map_err(|error| error.to_string())?; - writeln!(writer, " d. add direct source→source stub") - .map_err(|error| error.to_string())?; - writeln!( - writer, - " c. add contextual source→source stub at {context}" - ) - .map_err(|error| error.to_string())?; - writeln!(writer, " i. add ignore path for this deck path") - .map_err(|error| error.to_string())?; - writeln!(writer, " s. skip").map_err(|error| error.to_string())?; - let action = - prompt_action(reader, writer, "Action [d]", &["d", "c", "i", "s"], "d")?; + let action_labels = vec![ + "add direct source→source stub".to_owned(), + format!("add contextual source→source stub at {context}"), + "add ignore path for this deck path".to_owned(), + "skip".to_owned(), + ]; + let action = ui.select_one( + &format!( + "{} at {} source={}", + entry.category.as_str(), + entry.path, + yaml_scalar(&entry.source) + ), + &action_labels, + 0, + )?; let file_edits = edits.entry(report.overlay_path.clone()).or_default(); - match action.as_str() { - "d" => { + match action { + 0 => { file_edits.direct.insert(entry.source.clone()); } - "c" => { + 1 => { file_edits .contextual .entry(context) .or_default() .insert(entry.source.clone()); } - "i" => { + 2 => { file_edits.ignore_paths.insert(entry.path.clone()); } - "s" => {} + 3 => {} _ => unreachable!(), } } _ => { - writeln!( - writer, + ui.message(&format!( "{} at {} is stale/invalid; no safe automatic rewrite is applied, skipping.", entry.category.as_str(), entry.path - ) - .map_err(|error| error.to_string())?; + ))?; } } } @@ -693,64 +894,14 @@ fn prompt_selective_apply( return Ok(edits); } - let confirm = prompt_action( - reader, - writer, - "Apply selected changes? [y]", - &["y", "n"], - "y", - )?; - if confirm == "y" { + let confirm_labels = vec!["apply selected changes".to_owned(), "cancel".to_owned()]; + if ui.select_one("Apply selected changes?", &confirm_labels, 0)? == 0 { Ok(edits) } else { Ok(BTreeMap::new()) } } -fn parse_row_selection(input: &str, row_count: usize) -> Result, String> { - let input = input.trim(); - if input.eq_ignore_ascii_case("none") || input.eq_ignore_ascii_case("n") { - return Ok(Vec::new()); - } - if input.eq_ignore_ascii_case("all") || input.eq_ignore_ascii_case("a") { - return Ok((0..row_count).collect()); - } - let mut selected = BTreeSet::new(); - for part in input.split(',') { - let part = part.trim(); - let number = part - .parse::() - .map_err(|_| format!("invalid row selection {part:?}"))?; - if !(1..=row_count).contains(&number) { - return Err(format!("row {number} is outside 1..={row_count}")); - } - selected.insert(number - 1); - } - Ok(selected.into_iter().collect()) -} - -fn prompt_action( - reader: &mut R, - writer: &mut W, - prompt: &str, - valid: &[&str], - default: &str, -) -> Result { - loop { - let answer = prompt_line(reader, writer, prompt)?; - let action = if answer.is_empty() { - default.to_owned() - } else { - answer.to_ascii_lowercase() - }; - if valid.iter().any(|candidate| *candidate == action) { - return Ok(action); - } - writeln!(writer, "Choose one of: {}", valid.join(", ")) - .map_err(|error| error.to_string())?; - } -} - fn contextual_context_for_entry(entry: &TranslationCoverageEntry) -> String { context_parent_candidate(&entry.path).unwrap_or_else(|| entry.path.clone()) } @@ -908,6 +1059,75 @@ fn compose_lenient_translation_overlay( }) } +fn print_no_translation_reports(args: &TranslationArgs) -> Result<(), String> { + let non_dictionary = non_dictionary_translation_overlays(args)?; + if non_dictionary.is_empty() { + println!( + "{}", + color_stdout( + "No translation coverage entries matched the selected target and scope.", + "33" + ) + ); + println!( + "Try a broader scope, or run `brainbrew translations --manifest {} --all-targets`.", + args.manifest_path.display() + ); + return Ok(()); + } + + println!( + "{}", + color_stdout( + "No translation dictionary coverage reports matched the selected target and scope.", + "33" + ) + ); + println!(); + println!( + "The selected target includes translation overlays that do not use a `translations:` dictionary yet:" + ); + for overlay in non_dictionary { + println!(" - {} ({})", overlay.id, overlay.label); + } + println!(); + println!( + "`brainbrew translations` reports source-keyed translation dictionaries such as `translations.direct`, `translations.contextual`, and `translations.target_additions`." + ); + println!( + "Patch-style translation overlays still compose, but they do not expose coverage data for this workflow yet." + ); + Ok(()) +} + +fn non_dictionary_translation_overlays( + args: &TranslationArgs, +) -> Result, String> { + let manifest = read_manifest(&args.manifest_path)?; + let target_names = selected_target_names(&manifest, args); + let mut choices = BTreeSet::::new(); + for target in target_names { + let plan = plan_manifest_target_with_packages( + &args.manifest_path, + &target, + &args.include_paths, + &args.package_roots, + )?; + for (planned, overlay) in &plan.overlays { + if overlay.kind == OverlayKind::Translation + && overlay.translations.is_none() + && overlay_matches_scope(&target, planned, overlay, args) + { + choices.insert(OverlayChoice { + id: planned.id.clone(), + label: planned.display_file.clone(), + }); + } + } + } + Ok(choices.into_iter().collect()) +} + fn print_human_reports(reports: &[ScopedTranslationReport]) { for report in reports { let counts = category_counts(&report.report.entries); @@ -1213,15 +1433,20 @@ fn translation_overlay_choices(args: &TranslationArgs) -> Result Result Vec { let mut languages = BTreeSet::new(); - for target in manifest.targets.keys() { - if let Some((language, _)) = target.split_once('-') - && (2..=8).contains(&language.len()) - && language.chars().all(|ch| ch.is_ascii_lowercase()) - { - languages.insert(language.to_owned()); - } - } for (id, overlay) in &manifest.overlays { - if let Some(language) = id.rsplit(['.', '-']).next() - && (2..=8).contains(&language.len()) - && language.chars().all(|ch| ch.is_ascii_lowercase()) - { - languages.insert(language.to_owned()); + let is_translation_overlay = overlay.kind.as_deref() == Some("translation") + || id.contains("translation.") + || id.contains("translation-"); + if !is_translation_overlay { + continue; } - if let Some(stem) = Path::new(&overlay.file) + maybe_insert_language(id.rsplit(['.', '-']).next(), &mut languages); + let stem = Path::new(&overlay.file) .file_stem() - .and_then(|stem| stem.to_str()) - && (2..=8).contains(&stem.len()) - && stem.chars().all(|ch| ch.is_ascii_lowercase()) - { - languages.insert(stem.to_owned()); + .and_then(|stem| stem.to_str()); + if let Some(stem) = stem { + if let Some((_, suffix)) = stem.rsplit_once(['-', '.']) { + maybe_insert_language(Some(suffix), &mut languages); + } else { + maybe_insert_language(Some(stem), &mut languages); + } } } languages.into_iter().collect() } +fn maybe_insert_language(candidate: Option<&str>, languages: &mut BTreeSet) { + let Some(language) = candidate else { + return; + }; + if (2..=8).contains(&language.len()) + && language + .chars() + .all(|ch| ch.is_ascii_lowercase() || ch == '-') + && language != "translation" + { + languages.insert(language.to_owned()); + } +} + fn note_id_from_path(path: &str) -> Option { let rest = path.strip_prefix("notes.")?; let end = [".fields.", ".tags.", ".variables.", ".adapter_ids."] diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index 681b58a..d2c17b6 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -56,7 +56,7 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", ), "translations" | "translate" | "translation" => Some( - "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open manifest-aware selectors for target, language, overlay, scope, and mode; the equivalent non-interactive command is printed. --apply inserts source->source stubs for missing untranslated fallbacks into translations.direct in non-interactive mode, while interactive apply lets you choose direct, contextual, ignore-path, or skip actions. Use --path-prefix with paths from a diff to scope a changed-base review.\n", + "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, language, overlay, scope, and mode; the equivalent non-interactive command is printed. --apply inserts source->source stubs for missing untranslated fallbacks into translations.direct in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose direct, contextual, ignore-path, or skip actions. Use --path-prefix with paths from a diff to scope a changed-base review.\n", ), "verify" => Some( "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index db16045..85976c0 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -956,6 +956,47 @@ fn translations_missing_manifest_lists_nearby_manifests() { assert!(err.contains("brainbrew translations --manifest decks/sample/brainbrew.yaml")); } +#[test] +fn translations_reports_when_selected_target_has_no_dictionary_overlays() { + let manifest = workspace_root().join("fixtures/ug-style/brainbrew.yaml"); + let output = run([ + "translations", + "--manifest", + manifest.to_str().unwrap(), + "--target", + "full-demo", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("No translation dictionary coverage reports matched")); + assert!(out.contains("translation.es")); + assert!(out.contains("translation-sv.yaml")); + assert!(out.contains("do not use a `translations:` dictionary")); +} + +#[test] +fn translations_interactive_language_options_come_from_translation_overlays_only() { + let manifest = workspace_root().join("fixtures/ug-style/brainbrew.yaml"); + let output = run_with_stdin( + [ + "translate", + "--manifest", + manifest.to_str().unwrap(), + "--interactive", + ], + "\n\nq", + ); + + assert!(!output.status.success()); + let out = stdout(&output); + assert!(out.contains("Language filter")); + assert!(out.contains("es")); + assert!(out.contains("sv")); + assert!(!out.contains("capital")); + assert!(!out.contains("hint")); +} + #[test] fn translations_human_output_can_be_colored_but_json_stays_plain() { let dir = temp_dir("translations-color"); @@ -1002,21 +1043,23 @@ fn translations_interactive_derives_selector_options_and_prints_equivalent_comma dir.join("brainbrew.yaml").to_str().unwrap(), "--interactive", ], - "2\n\n\n\n\n", + "\x1b[B\n\n\n\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); let out = stdout(&output); assert!(out.contains("Brain Brew translation coverage")); - assert!(out.contains("Target:")); + assert!(out.contains("Target")); assert!(out.contains("da-release")); assert!(out.contains("da-standard")); - assert!(out.contains("Translation overlay:")); + assert!(out.contains("Translation overlay")); assert!(out.contains("overlay.translation.da")); - assert!(out.contains("Scope:")); + assert!(out.contains("Scope")); assert!(out.contains("Equivalent command:")); assert!(out.contains("brainbrew translations")); assert!(out.contains("--target da-standard")); + assert!(!out.contains("Select Target")); + assert!(!out.contains("1. da-release")); } #[test] @@ -1037,7 +1080,7 @@ fn translations_interactive_apply_can_insert_contextual_stub() { "--apply", "--interactive", ], - "\n\n\nc\ny\n", + "\n\n\n\x1b[B\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1063,7 +1106,7 @@ fn translations_interactive_apply_can_insert_ignore_path() { "--apply", "--interactive", ], - "\n\n\ni\ny\n", + "\n\n\n\x1b[B\x1b[B\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1848,6 +1891,14 @@ fn run_with_cache(args: [&str; N], cache: &Path) -> std::process .expect("command runs") } +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("crate lives under crates/brain-brew-cli") + .to_path_buf() +} + fn write_manifest_workspace(dir: &Path) { fs::write(dir.join("deck.yaml"), SAMPLE_CANONICAL_YAML).unwrap(); fs::write(dir.join("capital.yaml"), CAPITAL_OVERLAY_YAML).unwrap(); diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index 37a8f09..5d08b59 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -131,7 +131,7 @@ brainbrew translate brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml ``` -The interactive workflow lets you choose from the known manifests, targets, languages, translation overlays, notes, fields, path prefixes, and report/apply mode. It prints the equivalent non-interactive command before running so the same review can be repeated in CI or shared with another maintainer. +The interactive workflow is a real arrow-key terminal UI: use ↑/↓ to move, Enter to select, Space to toggle rows during selective apply, and `q` to cancel. It lets you choose from the known manifests, targets, languages, translation overlays, notes, fields, path prefixes, and report/apply mode. It prints the equivalent non-interactive command before running so the same review can be repeated in CI or shared with another maintainer. For scripts and CI, pass the scope explicitly: @@ -151,7 +151,7 @@ To seed translator work after adding English notes or fields, run apply explicit brainbrew translations --manifest brainbrew.yaml --target de-standard --apply ``` -Non-interactive `--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing fallbacks in scope. Interactive apply is selective: choose rows, then choose direct `source: source`, contextual `source: source` at the suggested safe context, an `ignore_paths` entry, or skip. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. +Non-interactive `--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose direct `source: source`, contextual `source: source` at the suggested safe context, an `ignore_paths` entry, or skip. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. When `require_complete: true`, composition fails if any extracted non-empty translatable string is not translated by `direct`, translated by a matching `contextual` entry, or matched by `ignore_paths`. For release workflows, prefer target-level verification policy in `brainbrew.yaml`: From 9e44eb8c5acfae3ffb623b1314c44b812b3b21c1 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 15:30:01 +0200 Subject: [PATCH 018/142] fix raw-mode translation TUI line endings --- .../src/commands/translations.rs | 111 +++++++++++------- 1 file changed, 68 insertions(+), 43 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 36f6b7e..14a397a 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -409,27 +409,17 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { } fn message(&mut self, message: &str) -> Result<(), String> { - if self.raw_mode { - self.clear()?; - writeln!(self.writer, "{}", color_stdout(message, "1;36")) - .map_err(|error| error.to_string())?; - writeln!(self.writer).map_err(|error| error.to_string())?; - } else { - writeln!(self.writer, "{}", color_stdout(message, "1;36")) - .map_err(|error| error.to_string())?; - writeln!(self.writer).map_err(|error| error.to_string())?; - } + self.clear_if_raw()?; + self.write_line(&color_stdout(message, "1;36"))?; + self.blank_line()?; self.writer.flush().map_err(|error| error.to_string()) } fn finish_with_equivalent_command(&mut self, args: &TranslationArgs) -> Result<(), String> { - if self.raw_mode { - self.clear()?; - } - writeln!(self.writer, "Equivalent command:").map_err(|error| error.to_string())?; - writeln!(self.writer, " {}", equivalent_command(args)) - .map_err(|error| error.to_string())?; - writeln!(self.writer).map_err(|error| error.to_string())?; + self.clear_if_raw()?; + self.write_line("Equivalent command:")?; + self.write_line(&format!(" {}", equivalent_command(args)))?; + self.blank_line()?; self.writer.flush().map_err(|error| error.to_string()) } @@ -520,9 +510,8 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { selected: usize, ) -> Result<(), String> { self.clear_if_raw()?; - writeln!(self.writer, "{}", color_stdout(label, "1;36")) - .map_err(|error| error.to_string())?; - writeln!(self.writer).map_err(|error| error.to_string())?; + self.write_line(&color_stdout(label, "1;36"))?; + self.blank_line()?; for (index, option) in options.iter().enumerate() { let marker = if index == selected { "›" } else { " " }; let option = if index == selected { @@ -530,11 +519,10 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { } else { option.clone() }; - writeln!(self.writer, " {marker} {option}").map_err(|error| error.to_string())?; + self.write_line(&format!(" {marker} {option}"))?; } - writeln!(self.writer).map_err(|error| error.to_string())?; - writeln!(self.writer, "↑/↓ move • Enter select • q cancel") - .map_err(|error| error.to_string())?; + self.blank_line()?; + self.write_line("↑/↓ move • Enter select • q cancel")?; self.writer.flush().map_err(|error| error.to_string()) } @@ -546,9 +534,8 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { cursor: usize, ) -> Result<(), String> { self.clear_if_raw()?; - writeln!(self.writer, "{}", color_stdout(label, "1;36")) - .map_err(|error| error.to_string())?; - writeln!(self.writer).map_err(|error| error.to_string())?; + self.write_line(&color_stdout(label, "1;36"))?; + self.blank_line()?; for (index, option) in options.iter().enumerate() { let cursor_marker = if index == cursor { "›" } else { " " }; let selected_marker = if selected[index] { "[x]" } else { "[ ]" }; @@ -558,14 +545,10 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { } else { line }; - writeln!(self.writer, " {cursor_marker} {line}").map_err(|error| error.to_string())?; + self.write_line(&format!(" {cursor_marker} {line}"))?; } - writeln!(self.writer).map_err(|error| error.to_string())?; - writeln!( - self.writer, - "↑/↓ move • Space toggle • a toggle all • Enter confirm • q cancel" - ) - .map_err(|error| error.to_string())?; + self.blank_line()?; + self.write_line("↑/↓ move • Space toggle • a toggle all • Enter confirm • q cancel")?; self.writer.flush().map_err(|error| error.to_string()) } @@ -576,19 +559,17 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { input: &str, ) -> Result<(), String> { self.clear_if_raw()?; - writeln!(self.writer, "{}", color_stdout(label, "1;36")) - .map_err(|error| error.to_string())?; - writeln!(self.writer).map_err(|error| error.to_string())?; + self.write_line(&color_stdout(label, "1;36"))?; + self.blank_line()?; for line in body { - writeln!(self.writer, "{line}").map_err(|error| error.to_string())?; + self.write_line(line)?; } if !body.is_empty() { - writeln!(self.writer).map_err(|error| error.to_string())?; + self.blank_line()?; } - writeln!(self.writer, "> {input}").map_err(|error| error.to_string())?; - writeln!(self.writer).map_err(|error| error.to_string())?; - writeln!(self.writer, "Type text • Enter confirm • Esc cancel") - .map_err(|error| error.to_string())?; + self.write_line(&format!("> {input}"))?; + self.blank_line()?; + self.write_line("Type text • Enter confirm • Esc cancel")?; self.writer.flush().map_err(|error| error.to_string()) } @@ -603,6 +584,16 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { write!(self.writer, "\x1b[2J\x1b[H").map_err(|error| error.to_string()) } + fn write_line(&mut self, text: &str) -> Result<(), String> { + write!(self.writer, "{}{}", text, terminal_line_end(self.raw_mode)) + .map_err(|error| error.to_string()) + } + + fn blank_line(&mut self) -> Result<(), String> { + write!(self.writer, "{}", terminal_line_end(self.raw_mode)) + .map_err(|error| error.to_string()) + } + fn read_key(&mut self) -> Result { if self.raw_mode { return read_terminal_key(); @@ -611,6 +602,10 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { } } +fn terminal_line_end(raw_mode: bool) -> &'static str { + if raw_mode { "\r\n" } else { "\n" } +} + impl Drop for TerminalUi<'_, R, W> { fn drop(&mut self) { if self.raw_mode { @@ -1722,3 +1717,33 @@ fn color_enabled(is_terminal: bool) -> bool { _ => env::var_os("NO_COLOR").is_none() && is_terminal, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn raw_mode_uses_crlf_line_endings() { + assert_eq!(terminal_line_end(false), "\n"); + assert_eq!(terminal_line_end(true), "\r\n"); + } + + #[test] + fn raw_mode_rendering_returns_cursor_to_column_zero() { + let mut input = io::empty(); + let mut output = Vec::new(); + { + let mut ui = TerminalUi { + reader: &mut input, + writer: &mut output, + raw_mode: true, + }; + ui.render_select_one("Title", &["one".to_owned(), "two".to_owned()], 0) + .unwrap(); + } + let output = String::from_utf8(output).unwrap(); + assert!(output.contains("Title\r\n\r\n")); + assert!(output.contains(" › one\r\n")); + assert!(output.contains(" two\r\n")); + } +} From 58c4202eb5fcdc84070beaaaa35f6b75be68b700 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 15:35:32 +0200 Subject: [PATCH 019/142] scroll long translation TUI option lists --- .../src/commands/translations.rs | 171 +++++++++++++++++- 1 file changed, 163 insertions(+), 8 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 14a397a..d7f1af0 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -393,18 +393,31 @@ struct TerminalUi<'a, R: Read, W: Write> { reader: &'a mut R, writer: &'a mut W, raw_mode: bool, + max_option_rows: Option, + terminal_cols: Option, } impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { fn new(reader: &'a mut R, writer: &'a mut W, raw_mode: bool) -> Result { - if raw_mode { + let (terminal_cols, max_option_rows) = if raw_mode { enable_raw_mode().map_err(|error| error.to_string())?; write!(writer, "\x1b[?25l").map_err(|error| error.to_string())?; - } + match crossterm::terminal::size() { + Ok((cols, rows)) => ( + Some(cols as usize), + Some(usize::from(rows.saturating_sub(6).max(3))), + ), + Err(_) => (None, Some(12)), + } + } else { + (None, None) + }; Ok(Self { reader, writer, raw_mode, + max_option_rows, + terminal_cols, }) } @@ -438,6 +451,15 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { match self.read_key()? { TuiKey::Up => selected = selected.saturating_sub(1), TuiKey::Down => selected = (selected + 1).min(options.len() - 1), + TuiKey::PageUp => { + selected = selected.saturating_sub(self.visible_option_rows(options.len())) + } + TuiKey::PageDown => { + selected = + (selected + self.visible_option_rows(options.len())).min(options.len() - 1) + } + TuiKey::Home => selected = 0, + TuiKey::End => selected = options.len() - 1, TuiKey::Enter => return Ok(selected), TuiKey::Char('k') => selected = selected.saturating_sub(1), TuiKey::Char('j') => selected = (selected + 1).min(options.len() - 1), @@ -465,6 +487,15 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { match self.read_key()? { TuiKey::Up => cursor = cursor.saturating_sub(1), TuiKey::Down => cursor = (cursor + 1).min(options.len() - 1), + TuiKey::PageUp => { + cursor = cursor.saturating_sub(self.visible_option_rows(options.len())) + } + TuiKey::PageDown => { + cursor = + (cursor + self.visible_option_rows(options.len())).min(options.len() - 1) + } + TuiKey::Home => cursor = 0, + TuiKey::End => cursor = options.len() - 1, TuiKey::Char('k') => cursor = cursor.saturating_sub(1), TuiKey::Char('j') => cursor = (cursor + 1).min(options.len() - 1), TuiKey::Space => selected[cursor] = !selected[cursor], @@ -512,17 +543,29 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { self.clear_if_raw()?; self.write_line(&color_stdout(label, "1;36"))?; self.blank_line()?; - for (index, option) in options.iter().enumerate() { + let (start, end, scrolled) = self.option_window(options.len(), selected); + if scrolled { + self.write_line(&format!( + "Showing {}–{} of {}", + start + 1, + end, + options.len() + ))?; + self.blank_line()?; + } + for (relative_index, option) in options[start..end].iter().enumerate() { + let index = start + relative_index; let marker = if index == selected { "›" } else { " " }; + let option = self.truncate_option(option, 4); let option = if index == selected { - color_stdout(option, "1;32") + color_stdout(&option, "1;32") } else { - option.clone() + option }; self.write_line(&format!(" {marker} {option}"))?; } self.blank_line()?; - self.write_line("↑/↓ move • Enter select • q cancel")?; + self.write_line("↑/↓ move • PgUp/PgDn jump • Enter select • q cancel")?; self.writer.flush().map_err(|error| error.to_string()) } @@ -536,9 +579,21 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { self.clear_if_raw()?; self.write_line(&color_stdout(label, "1;36"))?; self.blank_line()?; - for (index, option) in options.iter().enumerate() { + let (start, end, scrolled) = self.option_window(options.len(), cursor); + if scrolled { + self.write_line(&format!( + "Showing {}–{} of {}", + start + 1, + end, + options.len() + ))?; + self.blank_line()?; + } + for (relative_index, option) in options[start..end].iter().enumerate() { + let index = start + relative_index; let cursor_marker = if index == cursor { "›" } else { " " }; let selected_marker = if selected[index] { "[x]" } else { "[ ]" }; + let option = self.truncate_option(option, 8); let line = format!("{selected_marker} {option}"); let line = if index == cursor { color_stdout(&line, "1;32") @@ -548,7 +603,9 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { self.write_line(&format!(" {cursor_marker} {line}"))?; } self.blank_line()?; - self.write_line("↑/↓ move • Space toggle • a toggle all • Enter confirm • q cancel")?; + self.write_line( + "↑/↓ move • PgUp/PgDn jump • Space toggle • a toggle all • Enter confirm • q cancel", + )?; self.writer.flush().map_err(|error| error.to_string()) } @@ -573,6 +630,40 @@ impl<'a, R: Read, W: Write> TerminalUi<'a, R, W> { self.writer.flush().map_err(|error| error.to_string()) } + fn option_window(&self, option_count: usize, cursor: usize) -> (usize, usize, bool) { + if option_count == 0 { + return (0, 0, false); + } + let visible = self + .visible_option_rows(option_count) + .min(option_count) + .max(1); + if visible >= option_count { + return (0, option_count, false); + } + let half = visible / 2; + let mut start = cursor.saturating_sub(half); + if start + visible > option_count { + start = option_count - visible; + } + (start, start + visible, true) + } + + fn visible_option_rows(&self, option_count: usize) -> usize { + self.max_option_rows + .unwrap_or(option_count) + .min(option_count) + .max(1) + } + + fn truncate_option(&self, option: &str, prefix_width: usize) -> String { + let Some(cols) = self.terminal_cols else { + return option.to_owned(); + }; + let width = cols.saturating_sub(prefix_width).max(1); + truncate_chars(option, width) + } + fn clear_if_raw(&mut self) -> Result<(), String> { if self.raw_mode { self.clear()?; @@ -606,6 +697,17 @@ fn terminal_line_end(raw_mode: bool) -> &'static str { if raw_mode { "\r\n" } else { "\n" } } +fn truncate_chars(value: &str, max_chars: usize) -> String { + let char_count = value.chars().count(); + if char_count <= max_chars { + return value.to_owned(); + } + if max_chars <= 1 { + return "…".to_owned(); + } + value.chars().take(max_chars - 1).chain(['…']).collect() +} + impl Drop for TerminalUi<'_, R, W> { fn drop(&mut self) { if self.raw_mode { @@ -620,6 +722,10 @@ impl Drop for TerminalUi<'_, R, W> { enum TuiKey { Up, Down, + PageUp, + PageDown, + Home, + End, Enter, Space, Backspace, @@ -638,6 +744,10 @@ fn read_terminal_key() -> Result { return Ok(match key.code { KeyCode::Up => TuiKey::Up, KeyCode::Down => TuiKey::Down, + KeyCode::PageUp => TuiKey::PageUp, + KeyCode::PageDown => TuiKey::PageDown, + KeyCode::Home => TuiKey::Home, + KeyCode::End => TuiKey::End, KeyCode::Enter => TuiKey::Enter, KeyCode::Char(' ') => TuiKey::Space, KeyCode::Char(ch) => TuiKey::Char(ch), @@ -1737,6 +1847,8 @@ mod tests { reader: &mut input, writer: &mut output, raw_mode: true, + max_option_rows: None, + terminal_cols: None, }; ui.render_select_one("Title", &["one".to_owned(), "two".to_owned()], 0) .unwrap(); @@ -1746,4 +1858,47 @@ mod tests { assert!(output.contains(" › one\r\n")); assert!(output.contains(" two\r\n")); } + + #[test] + fn long_raw_mode_lists_scroll_to_keep_selection_visible() { + let mut input = io::empty(); + let mut output = Vec::new(); + { + let mut ui = TerminalUi { + reader: &mut input, + writer: &mut output, + raw_mode: true, + max_option_rows: Some(3), + terminal_cols: Some(40), + }; + let options = (1..=10) + .map(|index| format!("option-{index}")) + .collect::>(); + ui.render_select_one("Title", &options, 8).unwrap(); + } + let output = String::from_utf8(output).unwrap(); + assert!(output.contains("Showing 8–10 of 10\r\n")); + assert!(!output.contains("option-1\r\n")); + assert!(output.contains(" › option-9\r\n")); + } + + #[test] + fn long_options_are_truncated_to_prevent_wrapping() { + assert_eq!(truncate_chars("abcdef", 4), "abc…"); + let mut input = io::empty(); + let mut output = Vec::new(); + { + let mut ui = TerminalUi { + reader: &mut input, + writer: &mut output, + raw_mode: true, + max_option_rows: None, + terminal_cols: Some(14), + }; + ui.render_select_one("Title", &["very-long-option".to_owned()], 0) + .unwrap(); + } + let output = String::from_utf8(output).unwrap(); + assert!(output.contains(" › very-long…\r\n")); + } } From 5261555d0025edccbe207afd3baa6d3e4b0e0c14 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 15:48:52 +0200 Subject: [PATCH 020/142] focus translation reports on actionable text --- .../src/commands/translations.rs | 154 +++++++++++++++--- crates/brain-brew-cli/src/help.rs | 2 +- crates/brain-brew-cli/tests/cli.rs | 57 ++++++- documentation/docs/authoring/translations.md | 7 +- 4 files changed, 190 insertions(+), 30 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index d7f1af0..264ffbc 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -50,9 +50,10 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { let stdout = io::stdout(); let mut reader = stdin.lock(); let mut writer = stdout.lock(); - edits_by_file = prompt_selective_apply(&reports, &mut reader, &mut writer, raw_mode)?; + edits_by_file = + prompt_selective_apply(&reports, &mut reader, &mut writer, raw_mode, args.full)?; } else { - edits_by_file = direct_stub_edits_from_reports(&reports); + edits_by_file = direct_stub_edits_from_reports(&reports, args.full); } } @@ -70,7 +71,7 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { if reports.is_empty() { print_no_translation_reports(&args)?; } else { - print_human_reports(&reports); + print_human_reports(&reports, args.full); } if args.apply { let total = applied.values().sum::(); @@ -100,6 +101,7 @@ struct TranslationArgs { apply: bool, json_output: bool, interactive: Option, + full: bool, } fn parse_translation_args(args: &[String]) -> Result { @@ -117,6 +119,7 @@ fn parse_translation_args(args: &[String]) -> Result { apply: false, json_output: false, interactive: None, + full: false, }; let mut index = 0; while index < args.len() { @@ -196,6 +199,10 @@ fn parse_translation_args(args: &[String]) -> Result { parsed.json_output = true; index += 1; } + "--full" => { + parsed.full = true; + index += 1; + } "--interactive" => { parsed.interactive = Some(true); index += 1; @@ -265,7 +272,7 @@ fn configure_interactively( } } - if args.language.is_none() { + if args.language.is_none() && args.all_targets { let languages = inferred_languages(&manifest); if !languages.is_empty() { let mut labels = vec!["all languages".to_owned()]; @@ -279,7 +286,7 @@ fn configure_interactively( if args.overlay.is_none() { let overlays = translation_overlay_choices(args)?; - if !overlays.is_empty() { + if overlays.len() > 1 { let mut labels = vec!["all translation overlays".to_owned()]; labels.extend(overlays.iter().map(|overlay| overlay.label.clone())); let choice = ui.select_one("Translation overlay", &labels, 0)?; @@ -877,6 +884,7 @@ impl OverlayEdits { fn direct_stub_edits_from_reports( reports: &[ScopedTranslationReport], + full: bool, ) -> BTreeMap { let mut edits = BTreeMap::::new(); for report in reports { @@ -884,7 +892,10 @@ fn direct_stub_edits_from_reports( .report .entries .iter() - .filter(|entry| entry.category == TranslationCoverageCategory::UntranslatedFallback) + .filter(|entry| { + entry.category == TranslationCoverageCategory::UntranslatedFallback + && (full || default_report_includes_entry(entry)) + }) .map(|entry| entry.source.clone()) .collect::>(); if !stubs.is_empty() { @@ -903,6 +914,7 @@ fn prompt_selective_apply( reader: &mut R, writer: &mut W, raw_mode: bool, + full: bool, ) -> Result, String> { let mut ui = TerminalUi::new(reader, writer, raw_mode)?; let rows = reports @@ -914,7 +926,12 @@ fn prompt_selective_apply( .entries .iter() .enumerate() - .filter(|(_, entry)| entry.category.is_problem()) + .filter(|(_, entry)| { + entry.category.is_problem() + && (entry.category != TranslationCoverageCategory::UntranslatedFallback + || full + || default_report_includes_entry(entry)) + }) .map(move |(entry_index, _)| (report_index, entry_index)) }) .collect::>(); @@ -1233,9 +1250,12 @@ fn non_dictionary_translation_overlays( Ok(choices.into_iter().collect()) } -fn print_human_reports(reports: &[ScopedTranslationReport]) { +fn print_human_reports(reports: &[ScopedTranslationReport], full: bool) { for report in reports { - let counts = category_counts(&report.report.entries); + let display_entries = display_entries(&report.report.entries, full); + let display_counts = category_counts_refs(&display_entries); + let all_counts = category_counts(&report.report.entries); + let hidden_untranslated = hidden_untranslated_fallback_count(&report.report.entries, full); println!( "{}", color_stdout( @@ -1249,22 +1269,40 @@ fn print_human_reports(reports: &[ScopedTranslationReport]) { println!( " {}: {}", color_stdout("direct translations", "32"), - counts.get("direct_translation").copied().unwrap_or(0) + all_counts.get("direct_translation").copied().unwrap_or(0) ); println!( " {}: {}", color_stdout("contextual overrides", "36"), - counts.get("contextual_override").copied().unwrap_or(0) + all_counts.get("contextual_override").copied().unwrap_or(0) ); println!( " {}: {}", color_stdout("target-language additions", "35"), - counts.get("target_language_addition").copied().unwrap_or(0) + all_counts + .get("target_language_addition") + .copied() + .unwrap_or(0) ); + println!( + " {}: {}", + color_stdout("missing text translations", "31"), + display_counts + .get("untranslated_fallback") + .copied() + .unwrap_or(0) + ); + if hidden_untranslated > 0 { + println!( + " {}: {}", + color_stdout("hidden structural/media/tag fallbacks", "2"), + hidden_untranslated + ); + } println!( " {}: {}", color_stdout("ignored entries", "2"), - counts.get("ignored_source").copied().unwrap_or(0) + all_counts.get("ignored_source").copied().unwrap_or(0) ); println!( " {}: {}", @@ -1276,15 +1314,20 @@ fn print_human_reports(reports: &[ScopedTranslationReport]) { .filter(|entry| is_stale_or_invalid(entry.category)) .count() ); - println!( - " {}: {}", - color_stdout("missing/untranslated fallbacks", "31"), - counts.get("untranslated_fallback").copied().unwrap_or(0) - ); - for entry in report.report.problem_entries() { + if hidden_untranslated > 0 && !full { + println!(" hint: use --full to include structural/media/tag fallbacks"); + } + + let problem_entries = display_entries + .iter() + .filter(|entry| entry.category.is_problem()) + .copied() + .collect::>(); + let shown_problem_limit = 40; + for entry in problem_entries.iter().take(shown_problem_limit) { println!( " - {} {} source={} translated={}", - color_category(entry.category, entry.category.as_str()), + color_category(entry.category, display_category_name(entry.category)), entry.path, yaml_scalar(&entry.source), entry @@ -1294,6 +1337,66 @@ fn print_human_reports(reports: &[ScopedTranslationReport]) { .unwrap_or_else(|| "''".to_owned()) ); } + if problem_entries.len() > shown_problem_limit { + println!( + " … {} more problem entries not shown; narrow the scope or use --json", + problem_entries.len() - shown_problem_limit + ); + } + } +} + +fn display_entries( + entries: &[TranslationCoverageEntry], + full: bool, +) -> Vec<&TranslationCoverageEntry> { + entries + .iter() + .filter(|entry| full || default_report_includes_entry(entry)) + .collect() +} + +fn hidden_untranslated_fallback_count(entries: &[TranslationCoverageEntry], full: bool) -> usize { + if full { + return 0; + } + entries + .iter() + .filter(|entry| { + entry.category == TranslationCoverageCategory::UntranslatedFallback + && !default_report_includes_entry(entry) + }) + .count() +} + +fn default_report_includes_entry(entry: &TranslationCoverageEntry) -> bool { + if entry.category != TranslationCoverageCategory::UntranslatedFallback { + return true; + } + default_report_includes_untranslated_fallback(entry) +} + +fn default_report_includes_untranslated_fallback(entry: &TranslationCoverageEntry) -> bool { + let path = entry.path.as_str(); + path.starts_with("notes.") + && path.contains(".fields.") + && !is_media_or_structural_field_path(path) + && !looks_like_media_markup(&entry.source) +} + +fn is_media_or_structural_field_path(path: &str) -> bool { + path.ends_with(".fields.field.flag") || path.ends_with(".fields.field.map") +} + +fn looks_like_media_markup(source: &str) -> bool { + let trimmed = source.trim_start(); + trimmed.starts_with(" &'static str { + match category { + TranslationCoverageCategory::UntranslatedFallback => "missing_translation", + other => other.as_str(), } } @@ -1350,6 +1453,14 @@ fn category_counts(entries: &[TranslationCoverageEntry]) -> BTreeMap<&'static st counts } +fn category_counts_refs(entries: &[&TranslationCoverageEntry]) -> BTreeMap<&'static str, usize> { + let mut counts = BTreeMap::new(); + for entry in entries { + *counts.entry(entry.category.as_str()).or_insert(0) += 1; + } + counts +} + fn apply_overlay_edits(path: &Path, edits: &OverlayEdits) -> Result { if edits.is_empty() { return Ok(0); @@ -1753,6 +1864,9 @@ fn equivalent_command(args: &TranslationArgs) -> String { if args.apply { parts.push("--apply".to_owned()); } + if args.full { + parts.push("--full".to_owned()); + } parts.join(" ") } diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index d2c17b6..60604c7 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -56,7 +56,7 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", ), "translations" | "translate" | "translation" => Some( - "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, language, overlay, scope, and mode; the equivalent non-interactive command is printed. --apply inserts source->source stubs for missing untranslated fallbacks into translations.direct in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose direct, contextual, ignore-path, or skip actions. Use --path-prefix with paths from a diff to scope a changed-base review.\n", + "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --apply inserts source->source stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose direct, contextual, ignore-path, or skip actions. Use --path-prefix with paths from a diff to scope a changed-base review.\n", ), "verify" => Some( "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 85976c0..838e56f 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -975,6 +975,49 @@ fn translations_reports_when_selected_target_has_no_dictionary_overlays() { assert!(out.contains("do not use a `translations:` dictionary")); } +#[test] +fn translations_default_report_focuses_on_translatable_note_text() { + let manifest = workspace_root().join("fixtures/ultimate-geography/brainbrew.yaml"); + let output = run([ + "translations", + "--manifest", + manifest.to_str().unwrap(), + "--target", + "da-standard", + "--overlay", + "overlay.translation.da", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("missing text translations:")); + assert!(out.contains("hidden structural/media/tag fallbacks:")); + assert!(out.contains("hint: use --full")); + assert!(!out.contains("deck.description source=")); + assert!(!out.contains("notes.note.abkhazia.fields.field.flag")); + assert!(out.contains("notes.note.abkhazia.fields.field.capital")); +} + +#[test] +fn translations_full_report_includes_structural_fallbacks() { + let manifest = workspace_root().join("fixtures/ultimate-geography/brainbrew.yaml"); + let output = run([ + "translations", + "--manifest", + manifest.to_str().unwrap(), + "--target", + "da-standard", + "--overlay", + "overlay.translation.da", + "--full", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("deck.description source=")); + assert!(!out.contains("hidden structural/media/tag fallbacks:")); +} + #[test] fn translations_interactive_language_options_come_from_translation_overlays_only() { let manifest = workspace_root().join("fixtures/ug-style/brainbrew.yaml"); @@ -985,7 +1028,7 @@ fn translations_interactive_language_options_come_from_translation_overlays_only manifest.to_str().unwrap(), "--interactive", ], - "\n\nq", + "\x1b[B\nq", ); assert!(!output.status.success()); @@ -1052,12 +1095,14 @@ fn translations_interactive_derives_selector_options_and_prints_equivalent_comma assert!(out.contains("Target")); assert!(out.contains("da-release")); assert!(out.contains("da-standard")); - assert!(out.contains("Translation overlay")); - assert!(out.contains("overlay.translation.da")); + assert!(!out.contains("Language filter")); + assert!(!out.contains("Translation overlay")); assert!(out.contains("Scope")); assert!(out.contains("Equivalent command:")); assert!(out.contains("brainbrew translations")); assert!(out.contains("--target da-standard")); + assert!(!out.contains("--language")); + assert!(!out.contains("--overlay")); assert!(!out.contains("Select Target")); assert!(!out.contains("1. da-release")); } @@ -1080,7 +1125,7 @@ fn translations_interactive_apply_can_insert_contextual_stub() { "--apply", "--interactive", ], - "\n\n\n\x1b[B\n\n", + "\n\x1b[B\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1106,7 +1151,7 @@ fn translations_interactive_apply_can_insert_ignore_path() { "--apply", "--interactive", ], - "\n\n\n\x1b[B\x1b[B\n\n", + "\n\x1b[B\x1b[B\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1136,7 +1181,7 @@ fn translations_reports_missing_stale_contextual_and_additions_without_modifying assert!(out.contains("contextual overrides: 1")); assert!(out.contains("target-language additions: 1")); assert!(out.contains("stale_direct_key translations.direct.Removed source")); - assert!(out.contains("untranslated_fallback notes.note.sweden.fields.field.country")); + assert!(out.contains("missing_translation notes.note.sweden.fields.field.country")); assert_eq!(fs::read_to_string(overlay_path).unwrap(), before); } diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index 5d08b59..2065ab1 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -131,7 +131,7 @@ brainbrew translate brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml ``` -The interactive workflow is a real arrow-key terminal UI: use ↑/↓ to move, Enter to select, Space to toggle rows during selective apply, and `q` to cancel. It lets you choose from the known manifests, targets, languages, translation overlays, notes, fields, path prefixes, and report/apply mode. It prints the equivalent non-interactive command before running so the same review can be repeated in CI or shared with another maintainer. +The interactive workflow is a real arrow-key terminal UI: use ↑/↓ to move, Enter to select, Space to toggle rows during selective apply, and `q` to cancel. It lets you choose from the known manifests, targets, notes, fields, path prefixes, and report/apply mode. Language and overlay selectors appear only when they disambiguate the selected targets; for example, after choosing `da-standard`, Brain Brew already knows the Danish translation overlay. It prints the equivalent non-interactive command before running so the same review can be repeated in CI or shared with another maintainer. For scripts and CI, pass the scope explicitly: @@ -141,9 +141,10 @@ brainbrew translations --manifest brainbrew.yaml --all-targets --language de brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.berlin brainbrew translations --manifest brainbrew.yaml --target de-standard --path-prefix notes.note.berlin.fields.field.country brainbrew translations --manifest brainbrew.yaml --target de-standard --json +brainbrew translations --manifest brainbrew.yaml --target de-standard --full ``` -Report mode is the default and never modifies files. It groups extracted text into direct translations, contextual overrides, target-language additions, ignored entries, stale/invalid keys, and missing/untranslated fallbacks. Human output is colored when terminal color is enabled; JSON output is stable and uncolored. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. +Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. To seed translator work after adding English notes or fields, run apply explicitly: @@ -151,7 +152,7 @@ To seed translator work after adding English notes or fields, run apply explicit brainbrew translations --manifest brainbrew.yaml --target de-standard --apply ``` -Non-interactive `--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose direct `source: source`, contextual `source: source` at the suggested safe context, an `ignore_paths` entry, or skip. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. +Non-interactive `--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing text fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose direct `source: source`, contextual `source: source` at the suggested safe context, an `ignore_paths` entry, or skip. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. When `require_complete: true`, composition fails if any extracted non-empty translatable string is not translated by `direct`, translated by a matching `contextual` entry, or matched by `ignore_paths`. For release workflows, prefer target-level verification policy in `brainbrew.yaml`: From 6a77210745f2e9939e2a80c1f831fe4015ad1824 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 16:00:49 +0200 Subject: [PATCH 021/142] bulk apply translation TUI actions --- .../src/commands/translations.rs | 142 +++++++++++++----- crates/brain-brew-cli/src/help.rs | 2 +- crates/brain-brew-cli/tests/cli.rs | 30 ++++ documentation/docs/authoring/translations.md | 2 +- 4 files changed, 140 insertions(+), 36 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 264ffbc..938590d 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -958,6 +958,45 @@ fn prompt_selective_apply( .collect::>(); let selected = ui.select_many("Rows to edit", &row_labels, true)?; + let selected_missing = selected + .iter() + .copied() + .filter(|row_index| { + let (_, entry_index) = rows[*row_index]; + reports[rows[*row_index].0].report.entries[entry_index].category + == TranslationCoverageCategory::UntranslatedFallback + }) + .collect::>(); + + let bulk_action = if selected_missing.is_empty() { + MissingApplyAction::Skip + } else { + let labels = vec![ + format!( + "add direct source→source stubs for all {} selected missing translations", + selected_missing.len() + ), + format!( + "add contextual source→source stubs for all {} selected missing translations", + selected_missing.len() + ), + format!( + "add ignore_paths entries for all {} selected missing translations", + selected_missing.len() + ), + "decide per row".to_owned(), + "skip selected missing translations".to_owned(), + ]; + match ui.select_one("Action for selected missing translations", &labels, 0)? { + 0 => MissingApplyAction::Direct, + 1 => MissingApplyAction::Contextual, + 2 => MissingApplyAction::IgnorePath, + 3 => MissingApplyAction::DecidePerRow, + 4 => MissingApplyAction::Skip, + _ => unreachable!(), + } + }; + let mut edits = BTreeMap::::new(); for row_index in selected { let (report_index, entry_index) = rows[row_index]; @@ -965,41 +1004,13 @@ fn prompt_selective_apply( let entry = &report.report.entries[entry_index]; match entry.category { TranslationCoverageCategory::UntranslatedFallback => { - let context = contextual_context_for_entry(entry); - let action_labels = vec![ - "add direct source→source stub".to_owned(), - format!("add contextual source→source stub at {context}"), - "add ignore path for this deck path".to_owned(), - "skip".to_owned(), - ]; - let action = ui.select_one( - &format!( - "{} at {} source={}", - entry.category.as_str(), - entry.path, - yaml_scalar(&entry.source) - ), - &action_labels, - 0, - )?; - let file_edits = edits.entry(report.overlay_path.clone()).or_default(); - match action { - 0 => { - file_edits.direct.insert(entry.source.clone()); + let action = match bulk_action { + MissingApplyAction::DecidePerRow => { + prompt_missing_apply_action(&mut ui, entry)? } - 1 => { - file_edits - .contextual - .entry(context) - .or_default() - .insert(entry.source.clone()); - } - 2 => { - file_edits.ignore_paths.insert(entry.path.clone()); - } - 3 => {} - _ => unreachable!(), - } + other => other, + }; + apply_missing_translation_action(&mut edits, &report.overlay_path, entry, action); } _ => { ui.message(&format!( @@ -1024,6 +1035,69 @@ fn prompt_selective_apply( } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum MissingApplyAction { + Direct, + Contextual, + IgnorePath, + DecidePerRow, + Skip, +} + +fn prompt_missing_apply_action( + ui: &mut TerminalUi<'_, R, W>, + entry: &TranslationCoverageEntry, +) -> Result { + let context = contextual_context_for_entry(entry); + let action_labels = vec![ + "add direct source→source stub".to_owned(), + format!("add contextual source→source stub at {context}"), + "add ignore path for this deck path".to_owned(), + "skip".to_owned(), + ]; + match ui.select_one( + &format!( + "{} at {} source={}", + entry.category.as_str(), + entry.path, + yaml_scalar(&entry.source) + ), + &action_labels, + 0, + )? { + 0 => Ok(MissingApplyAction::Direct), + 1 => Ok(MissingApplyAction::Contextual), + 2 => Ok(MissingApplyAction::IgnorePath), + 3 => Ok(MissingApplyAction::Skip), + _ => unreachable!(), + } +} + +fn apply_missing_translation_action( + edits: &mut BTreeMap, + overlay_path: &Path, + entry: &TranslationCoverageEntry, + action: MissingApplyAction, +) { + let file_edits = edits.entry(overlay_path.to_path_buf()).or_default(); + match action { + MissingApplyAction::Direct => { + file_edits.direct.insert(entry.source.clone()); + } + MissingApplyAction::Contextual => { + file_edits + .contextual + .entry(contextual_context_for_entry(entry)) + .or_default() + .insert(entry.source.clone()); + } + MissingApplyAction::IgnorePath => { + file_edits.ignore_paths.insert(entry.path.clone()); + } + MissingApplyAction::DecidePerRow | MissingApplyAction::Skip => {} + } +} + fn contextual_context_for_entry(entry: &TranslationCoverageEntry) -> String { context_parent_candidate(&entry.path).unwrap_or_else(|| entry.path.clone()) } diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index 60604c7..e3e5651 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -56,7 +56,7 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", ), "translations" | "translate" | "translation" => Some( - "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --apply inserts source->source stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose direct, contextual, ignore-path, or skip actions. Use --path-prefix with paths from a diff to scope a changed-base review.\n", + "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --apply inserts source->source stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (direct, contextual, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", ), "verify" => Some( "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 838e56f..80ba855 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -1107,6 +1107,36 @@ fn translations_interactive_derives_selector_options_and_prints_equivalent_comma assert!(!out.contains("1. da-release")); } +#[test] +fn translations_interactive_apply_can_use_one_action_for_all_selected_rows() { + let dir = temp_dir("translations-interactive-direct-all"); + write_translation_workspace(&dir); + let overlay_path = dir.join("da.yaml"); + + let output = run_with_stdin( + [ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--path-prefix", + "notes.note.sweden.fields.field", + "--apply", + "--interactive", + ], + "\n\n\n", + ); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("Action for selected missing translations")); + assert!(out.contains("add direct source→source stubs for all 2 selected missing translations")); + let updated = fs::read_to_string(overlay_path).unwrap(); + assert!(updated.contains(" Stockholm: Stockholm\n")); + assert!(updated.contains(" Sweden: Sweden\n")); +} + #[test] fn translations_interactive_apply_can_insert_contextual_stub() { let dir = temp_dir("translations-interactive-contextual"); diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index 2065ab1..00a3eff 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -152,7 +152,7 @@ To seed translator work after adding English notes or fields, run apply explicit brainbrew translations --manifest brainbrew.yaml --target de-standard --apply ``` -Non-interactive `--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing text fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose direct `source: source`, contextual `source: source` at the suggested safe context, an `ignore_paths` entry, or skip. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. +Non-interactive `--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing text fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose one action for all selected rows — direct `source: source`, contextual `source: source` at the suggested safe context, `ignore_paths`, skip — or choose to decide per row. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. When `require_complete: true`, composition fails if any extracted non-empty translatable string is not translated by `direct`, translated by a matching `contextual` entry, or matched by `ignore_paths`. For release workflows, prefer target-level verification policy in `brainbrew.yaml`: From b72fe4e55fe511d53f01457c8745f893a4332029 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 16:16:00 +0200 Subject: [PATCH 022/142] add explicit no-change translations --- CONTEXT.md | 2 +- .../src/commands/translations.rs | 255 ++++++++++++++- crates/brain-brew-cli/src/help.rs | 2 +- crates/brain-brew-cli/tests/cli.rs | 127 +++++++- crates/brain-brew-core/src/lib.rs | 137 +++++++- .../brain-brew-core/tests/overlay_compose.rs | 113 ++++++- .../brain-brew-formats/src/canonical_yaml.rs | 144 ++++++++- .../tests/canonical_yaml.rs | 30 ++ .../brain-brew-formats/tests/overlay_yaml.rs | 18 +- documentation/docs/authoring/translations.md | 42 ++- ...-variables-and-translation-dictionaries.md | 7 +- documentation/docs/reference/yaml.md | 8 +- .../overlays/languages/da.yaml | 303 ++++++++++++++++++ skills/federated-deck-extensions/SKILL.md | 2 +- 14 files changed, 1148 insertions(+), 42 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 47399ce..1096017 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -113,7 +113,7 @@ A named text value defined on a deck, note type, card template, or note and refe _Avoid_: recipe variable, runtime Anki field **Translation Dictionary**: -A translation overlay section mapping direct source text, contextual source text, target-language additions for blank source fields, source variables, and adapter IDs to their translated values, with non-blank source keys acting as implicit expected bases. +A translation overlay section mapping direct source text, contextual source text, explicit no-change decisions for reviewed unchanged text, target-language additions for blank source fields, source variables, and adapter IDs, with non-blank source keys acting as implicit expected bases. _Avoid_: CSV importer, global localization database **Field Fill**: diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 938590d..ad2b6fd 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -867,17 +867,29 @@ fn selected_target_names(manifest: &FederatedDeckManifest, args: &TranslationArg struct OverlayEdits { direct: BTreeSet, contextual: BTreeMap>, + no_change_direct: BTreeSet, + no_change_contextual: BTreeMap>, ignore_paths: BTreeSet, } impl OverlayEdits { fn is_empty(&self) -> bool { - self.direct.is_empty() && self.contextual.is_empty() && self.ignore_paths.is_empty() + self.direct.is_empty() + && self.contextual.is_empty() + && self.no_change_direct.is_empty() + && self.no_change_contextual.is_empty() + && self.ignore_paths.is_empty() } fn count(&self) -> usize { self.direct.len() + self.contextual.values().map(BTreeSet::len).sum::() + + self.no_change_direct.len() + + self + .no_change_contextual + .values() + .map(BTreeSet::len) + .sum::() + self.ignore_paths.len() } } @@ -973,11 +985,19 @@ fn prompt_selective_apply( } else { let labels = vec![ format!( - "add direct source→source stubs for all {} selected missing translations", + "mark direct no-change for all {} selected missing translations", selected_missing.len() ), format!( - "add contextual source→source stubs for all {} selected missing translations", + "mark contextual no-change for all {} selected missing translations", + selected_missing.len() + ), + format!( + "add direct source→source translation stubs for all {} selected missing translations", + selected_missing.len() + ), + format!( + "add contextual source→source translation stubs for all {} selected missing translations", selected_missing.len() ), format!( @@ -988,11 +1008,13 @@ fn prompt_selective_apply( "skip selected missing translations".to_owned(), ]; match ui.select_one("Action for selected missing translations", &labels, 0)? { - 0 => MissingApplyAction::Direct, - 1 => MissingApplyAction::Contextual, - 2 => MissingApplyAction::IgnorePath, - 3 => MissingApplyAction::DecidePerRow, - 4 => MissingApplyAction::Skip, + 0 => MissingApplyAction::NoChangeDirect, + 1 => MissingApplyAction::NoChangeContextual, + 2 => MissingApplyAction::Direct, + 3 => MissingApplyAction::Contextual, + 4 => MissingApplyAction::IgnorePath, + 5 => MissingApplyAction::DecidePerRow, + 6 => MissingApplyAction::Skip, _ => unreachable!(), } }; @@ -1037,6 +1059,8 @@ fn prompt_selective_apply( #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum MissingApplyAction { + NoChangeDirect, + NoChangeContextual, Direct, Contextual, IgnorePath, @@ -1050,8 +1074,10 @@ fn prompt_missing_apply_action( ) -> Result { let context = contextual_context_for_entry(entry); let action_labels = vec![ - "add direct source→source stub".to_owned(), - format!("add contextual source→source stub at {context}"), + "mark direct no-change".to_owned(), + format!("mark contextual no-change at {context}"), + "add direct source→source translation stub".to_owned(), + format!("add contextual source→source translation stub at {context}"), "add ignore path for this deck path".to_owned(), "skip".to_owned(), ]; @@ -1065,10 +1091,12 @@ fn prompt_missing_apply_action( &action_labels, 0, )? { - 0 => Ok(MissingApplyAction::Direct), - 1 => Ok(MissingApplyAction::Contextual), - 2 => Ok(MissingApplyAction::IgnorePath), - 3 => Ok(MissingApplyAction::Skip), + 0 => Ok(MissingApplyAction::NoChangeDirect), + 1 => Ok(MissingApplyAction::NoChangeContextual), + 2 => Ok(MissingApplyAction::Direct), + 3 => Ok(MissingApplyAction::Contextual), + 4 => Ok(MissingApplyAction::IgnorePath), + 5 => Ok(MissingApplyAction::Skip), _ => unreachable!(), } } @@ -1081,6 +1109,16 @@ fn apply_missing_translation_action( ) { let file_edits = edits.entry(overlay_path.to_path_buf()).or_default(); match action { + MissingApplyAction::NoChangeDirect => { + file_edits.no_change_direct.insert(entry.source.clone()); + } + MissingApplyAction::NoChangeContextual => { + file_edits + .no_change_contextual + .entry(contextual_context_for_entry(entry)) + .or_default() + .insert(entry.source.clone()); + } MissingApplyAction::Direct => { file_edits.direct.insert(entry.source.clone()); } @@ -1216,6 +1254,20 @@ fn compose_lenient_translation_overlay( } } } + TranslationCoverageCategory::StaleNoChangeDirectKey => { + translations.no_change.direct.remove(&entry.source); + } + TranslationCoverageCategory::StaleNoChangeContextualKey => { + if let Some(context) = &entry.context + && let Some(sources) = + translations.no_change.contextual.get_mut(context) + { + sources.remove(&entry.source); + if sources.is_empty() { + translations.no_change.contextual.remove(context); + } + } + } TranslationCoverageCategory::StaleTargetAddition | TranslationCoverageCategory::InvalidTargetAddition => { let path = entry.context.as_deref().unwrap_or(&entry.path); @@ -1358,6 +1410,12 @@ fn print_human_reports(reports: &[ScopedTranslationReport], full: bool) { .copied() .unwrap_or(0) ); + println!( + " {}: {}", + color_stdout("intentionally unchanged text", "34"), + all_counts.get("no_change_direct").copied().unwrap_or(0) + + all_counts.get("no_change_contextual").copied().unwrap_or(0) + ); println!( " {}: {}", color_stdout("missing text translations", "31"), @@ -1479,6 +1537,8 @@ fn is_stale_or_invalid(category: TranslationCoverageCategory) -> bool { category, TranslationCoverageCategory::StaleDirectKey | TranslationCoverageCategory::StaleContextualKey + | TranslationCoverageCategory::StaleNoChangeDirectKey + | TranslationCoverageCategory::StaleNoChangeContextualKey | TranslationCoverageCategory::StaleTargetAddition | TranslationCoverageCategory::StaleVariableKey | TranslationCoverageCategory::StaleAdapterIdKey @@ -1550,6 +1610,13 @@ fn apply_overlay_edits(path: &Path, edits: &OverlayEdits) -> Result, + contextual: &BTreeMap>, +) -> String { + let spans = line_spans(input); + let Some(translations_index) = find_translations_line(input, &spans) else { + return insert_new_no_change_section(input, direct, contextual); + }; + if find_translation_section(input, &spans, translations_index, "no_change").is_none() { + return insert_new_no_change_section(input, direct, contextual); + } + + let mut output = input.to_owned(); + if !direct.is_empty() { + let entries = direct + .iter() + .map(|source| format!(" - {}\n", yaml_scalar(source))) + .collect::(); + output = insert_no_change_subsection(&output, "direct", &entries, &[]); + } + if !contextual.is_empty() { + let mut entries = String::new(); + for (context, sources) in contextual { + entries.push_str(&format!(" {}:\n", yaml_scalar(context))); + for source in sources { + entries.push_str(&format!(" - {}\n", yaml_scalar(source))); + } + } + output = insert_no_change_subsection(&output, "contextual", &entries, &["direct"]); + } + output +} + +fn insert_new_no_change_section( + input: &str, + direct: &BTreeSet, + contextual: &BTreeMap>, +) -> String { + let mut entries = String::new(); + if !direct.is_empty() { + entries.push_str(" direct:\n"); + for source in direct { + entries.push_str(&format!(" - {}\n", yaml_scalar(source))); + } + } + if !contextual.is_empty() { + entries.push_str(" contextual:\n"); + for (context, sources) in contextual { + entries.push_str(&format!(" {}:\n", yaml_scalar(context))); + for source in sources { + entries.push_str(&format!(" - {}\n", yaml_scalar(source))); + } + } + } + insert_translation_section( + input, + "no_change", + &entries, + &["contextual", "direct", "ignore_paths", "require_complete"], + ) +} + +fn insert_no_change_subsection( + input: &str, + subsection_name: &str, + entries: &str, + preferred_after_subsections: &[&str], +) -> String { + let spans = line_spans(input); + let Some(translations_index) = find_translations_line(input, &spans) else { + return input.to_owned(); + }; + let Some(no_change_index) = + find_translation_section(input, &spans, translations_index, "no_change") + else { + return input.to_owned(); + }; + let no_change_end = translation_section_end(input, &spans, no_change_index); + + if let Some(section_index) = find_no_change_subsection( + input, + &spans, + no_change_index, + no_change_end, + subsection_name, + ) { + let insert_at = indented_section_end(input, &spans, section_index, 4); + return insert_at_byte(input, insert_at, entries); + } + + for anchor in preferred_after_subsections { + if let Some(section_index) = + find_no_change_subsection(input, &spans, no_change_index, no_change_end, anchor) + { + let insert_at = indented_section_end(input, &spans, section_index, 4); + return insert_at_byte( + input, + insert_at, + &format!(" {subsection_name}:\n{entries}"), + ); + } + } + + let insert_at = spans[no_change_index].1; + insert_at_byte( + input, + insert_at, + &format!(" {subsection_name}:\n{entries}"), + ) +} + +fn find_no_change_subsection( + input: &str, + spans: &[(usize, usize)], + no_change_index: usize, + no_change_end: usize, + subsection_name: &str, +) -> Option { + let needle = format!(" {subsection_name}:"); + spans + .iter() + .enumerate() + .skip(no_change_index + 1) + .take_while(|(_, (start, _))| *start < no_change_end) + .find(|(_, (start, end))| input[*start..*end].trim_end() == needle) + .map(|(index, _)| index) +} + +fn indented_section_end( + input: &str, + spans: &[(usize, usize)], + section_index: usize, + section_indent: usize, +) -> usize { + spans + .iter() + .enumerate() + .skip(section_index + 1) + .find(|(_, (start, end))| { + let line = &input[*start..*end]; + !line.trim().is_empty() + && !line.trim_start().starts_with('#') + && leading_spaces(line) <= section_indent + }) + .map(|(_, (start, _))| *start) + .unwrap_or(input.len()) +} + +fn leading_spaces(line: &str) -> usize { + line.as_bytes() + .iter() + .take_while(|byte| **byte == b' ') + .count() +} + fn insert_ignore_path_lines(input: &str, paths: &BTreeSet) -> String { let entries = paths .iter() @@ -1986,6 +2209,8 @@ fn color_category(category: TranslationCoverageCategory, text: &str) -> String { match category { TranslationCoverageCategory::DirectTranslation => color_stdout(text, "32"), TranslationCoverageCategory::ContextualOverride => color_stdout(text, "36"), + TranslationCoverageCategory::NoChangeDirect + | TranslationCoverageCategory::NoChangeContextual => color_stdout(text, "34"), TranslationCoverageCategory::TargetLanguageAddition => color_stdout(text, "35"), TranslationCoverageCategory::VariableTranslation | TranslationCoverageCategory::AdapterIdTranslation @@ -1993,6 +2218,8 @@ fn color_category(category: TranslationCoverageCategory, text: &str) -> String { TranslationCoverageCategory::UntranslatedFallback => color_stdout(text, "31"), TranslationCoverageCategory::StaleDirectKey | TranslationCoverageCategory::StaleContextualKey + | TranslationCoverageCategory::StaleNoChangeDirectKey + | TranslationCoverageCategory::StaleNoChangeContextualKey | TranslationCoverageCategory::StaleTargetAddition | TranslationCoverageCategory::StaleVariableKey | TranslationCoverageCategory::StaleAdapterIdKey diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index e3e5651..e3c302b 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -56,7 +56,7 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", ), "translations" | "translate" | "translation" => Some( - "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --apply inserts source->source stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (direct, contextual, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", + "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --apply inserts source->source translation stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (mark no-change, direct/contextual stub, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", ), "verify" => Some( "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 80ba855..093593d 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -990,12 +990,13 @@ fn translations_default_report_focuses_on_translatable_note_text() { assert!(output.status.success(), "stderr: {}", stderr(&output)); let out = stdout(&output); - assert!(out.contains("missing text translations:")); + assert!(out.contains("missing text translations: 0")); + assert!(out.contains("intentionally unchanged text: 306")); assert!(out.contains("hidden structural/media/tag fallbacks:")); assert!(out.contains("hint: use --full")); assert!(!out.contains("deck.description source=")); assert!(!out.contains("notes.note.abkhazia.fields.field.flag")); - assert!(out.contains("notes.note.abkhazia.fields.field.capital")); + assert!(!out.contains("notes.note.abkhazia.fields.field.capital")); } #[test] @@ -1108,8 +1109,8 @@ fn translations_interactive_derives_selector_options_and_prints_equivalent_comma } #[test] -fn translations_interactive_apply_can_use_one_action_for_all_selected_rows() { - let dir = temp_dir("translations-interactive-direct-all"); +fn translations_interactive_apply_can_mark_all_selected_rows_no_change() { + let dir = temp_dir("translations-interactive-no-change-all"); write_translation_workspace(&dir); let overlay_path = dir.join("da.yaml"); @@ -1131,7 +1132,39 @@ fn translations_interactive_apply_can_use_one_action_for_all_selected_rows() { assert!(output.status.success(), "stderr: {}", stderr(&output)); let out = stdout(&output); assert!(out.contains("Action for selected missing translations")); - assert!(out.contains("add direct source→source stubs for all 2 selected missing translations")); + assert!(out.contains("mark direct no-change for all 2 selected missing translations")); + let updated = fs::read_to_string(overlay_path).unwrap(); + assert!(updated.contains(" no_change:\n direct:\n")); + assert!(updated.contains(" - Stockholm\n")); + assert!(updated.contains(" - Sweden\n")); +} + +#[test] +fn translations_interactive_apply_can_use_one_translation_stub_action_for_all_selected_rows() { + let dir = temp_dir("translations-interactive-direct-all"); + write_translation_workspace(&dir); + let overlay_path = dir.join("da.yaml"); + + let output = run_with_stdin( + [ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--path-prefix", + "notes.note.sweden.fields.field", + "--apply", + "--interactive", + ], + "\n\x1b[B\x1b[B\n\n", + ); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains( + "add direct source→source translation stubs for all 2 selected missing translations" + )); let updated = fs::read_to_string(overlay_path).unwrap(); assert!(updated.contains(" Stockholm: Stockholm\n")); assert!(updated.contains(" Sweden: Sweden\n")); @@ -1155,7 +1188,7 @@ fn translations_interactive_apply_can_insert_contextual_stub() { "--apply", "--interactive", ], - "\n\x1b[B\n\n", + "\n\x1b[B\x1b[B\x1b[B\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1181,7 +1214,7 @@ fn translations_interactive_apply_can_insert_ignore_path() { "--apply", "--interactive", ], - "\n\x1b[B\x1b[B\n\n", + "\n\x1b[B\x1b[B\x1b[B\x1b[B\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1190,6 +1223,47 @@ fn translations_interactive_apply_can_insert_ignore_path() { assert!(updated.contains(" - notes.note.sweden.fields.field.country\n")); } +#[test] +fn translations_report_counts_no_change_as_intentionally_unchanged_text() { + let dir = temp_dir("translations-no-change-report"); + write_translation_workspace(&dir); + fs::write( + dir.join("da.yaml"), + r#"id: overlay.translation.da +kind: translation +translations: + ignore_paths: + - deck.* + - note_types.* + - notes.*.tags.* + - notes.*.fields.field.flag + no_change: + direct: + - Sweden + contextual: + notes.note.sweden: + - Stockholm +"#, + ) + .unwrap(); + + let output = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--path-prefix", + "notes.note.sweden.fields.field", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("intentionally unchanged text: 2")); + assert!(out.contains("missing text translations: 0")); + assert!(!out.contains("missing_translation notes.note.sweden.fields.field.country")); +} + #[test] fn translations_reports_missing_stale_contextual_and_additions_without_modifying() { let dir = temp_dir("translations-report"); @@ -1290,6 +1364,45 @@ translations: assert!(!strict_output.status.success()); assert!(stderr(&strict_output).contains("translation coverage strict policy failed")); assert!(stderr(&strict_output).contains("notes.note.sweden.fields.field.country")); + + fs::write( + &overlay_path, + r#"id: overlay.translation.da +kind: translation +translations: + ignore_paths: + - 'deck.*' + - 'note_types.*' + - 'notes.*.fields.field.flag' + - 'notes.*.tags.*' + direct: + Finland: Finland + contextual: + notes.note: + finland: + Helsinki: Helsingfors + no_change: + direct: + - Stockholm + - Sweden + target_additions: + notes.note.finland.fields.field.flag: '' +"#, + ) + .unwrap(); + + let strict_output = run([ + "verify", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-release", + ]); + assert!( + strict_output.status.success(), + "stderr: {}", + stderr(&strict_output) + ); } #[test] diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs index 3f7371b..bedaa15 100644 --- a/crates/brain-brew-core/src/lib.rs +++ b/crates/brain-brew-core/src/lib.rs @@ -386,6 +386,8 @@ fn translation_coverage_report( entries: Vec::new(), seen_direct: BTreeSet::new(), seen_contextual: BTreeSet::new(), + seen_no_change_direct: BTreeSet::new(), + seen_no_change_contextual: BTreeSet::new(), seen_target_additions: BTreeSet::new(), seen_variables: BTreeSet::new(), seen_adapter_ids: BTreeSet::new(), @@ -460,6 +462,8 @@ struct TranslationCoverageBuilder<'a> { entries: Vec, seen_direct: BTreeSet, seen_contextual: BTreeSet<(String, String)>, + seen_no_change_direct: BTreeSet, + seen_no_change_contextual: BTreeSet<(String, String)>, seen_target_additions: BTreeSet, seen_variables: BTreeSet<(String, String)>, seen_adapter_ids: BTreeSet<(String, String)>, @@ -526,6 +530,10 @@ impl TranslationCoverageBuilder<'_> { if direct_translation.is_some() { self.seen_direct.insert(source.clone()); } + let direct_no_change = self.translations.no_change.direct.contains(value); + if direct_no_change { + self.seen_no_change_direct.insert(source.clone()); + } let mut contextual_translation: Option<(&String, &String)> = None; for (context_path, replacements) in &self.translations.contextual { @@ -543,6 +551,20 @@ impl TranslationCoverageBuilder<'_> { } } + let mut contextual_no_change: Option<&String> = None; + for (context_path, sources) in &self.translations.no_change.contextual { + if context_matches_path(context_path, &path) && sources.contains(value) { + self.seen_no_change_contextual + .insert((context_path.clone(), source.clone())); + if contextual_no_change + .as_ref() + .is_none_or(|current_context| context_path.len() > current_context.len()) + { + contextual_no_change = Some(context_path); + } + } + } + if let Some((context_path, translated)) = contextual_translation { self.entries.push(TranslationCoverageEntry { category: TranslationCoverageCategory::ContextualOverride, @@ -559,6 +581,22 @@ impl TranslationCoverageBuilder<'_> { translated: Some(translated.clone()), context: None, }); + } else if let Some(context_path) = contextual_no_change { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::NoChangeContextual, + path, + source: source.clone(), + translated: Some(source), + context: Some(context_path.clone()), + }); + } else if direct_no_change { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::NoChangeDirect, + path, + source: source.clone(), + translated: Some(source), + context: None, + }); } else { self.entries.push(TranslationCoverageEntry { category: TranslationCoverageCategory::UntranslatedFallback, @@ -624,6 +662,33 @@ impl TranslationCoverageBuilder<'_> { } } } + for source in &self.translations.no_change.direct { + if !self.seen_no_change_direct.contains(source) { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::StaleNoChangeDirectKey, + path: format!("translations.no_change.direct.{source}"), + source: source.clone(), + translated: Some(source.clone()), + context: None, + }); + } + } + for (context_path, sources) in &self.translations.no_change.contextual { + for source in sources { + if !self + .seen_no_change_contextual + .contains(&(context_path.clone(), source.clone())) + { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::StaleNoChangeContextualKey, + path: format!("translations.no_change.contextual.{context_path}.{source}"), + source: source.clone(), + translated: Some(source.clone()), + context: Some(context_path.clone()), + }); + } + } + } for (path, translated) in &self.translations.target_additions { if !self.seen_target_additions.contains(path) { self.entries.push(TranslationCoverageEntry { @@ -691,6 +756,8 @@ fn apply_translation_dictionary( ) { let mut seen_direct = BTreeSet::new(); let mut seen_contextual = BTreeSet::new(); + let mut seen_no_change_direct = BTreeSet::new(); + let mut seen_no_change_contextual = BTreeSet::new(); let mut seen_target_additions = BTreeSet::new(); let mut seen_variables = BTreeSet::new(); let mut seen_adapter_ids = BTreeSet::new(); @@ -702,6 +769,8 @@ fn apply_translation_dictionary( translations, seen_direct: &mut seen_direct, seen_contextual: &mut seen_contextual, + seen_no_change_direct: &mut seen_no_change_direct, + seen_no_change_contextual: &mut seen_no_change_contextual, seen_target_additions: &mut seen_target_additions, seen_variables: &mut seen_variables, seen_adapter_ids: &mut seen_adapter_ids, @@ -818,6 +887,30 @@ fn apply_translation_dictionary( } } } + for source in &translations.no_change.direct { + if !seen_no_change_direct.contains(source) { + errors.push(ComposeError::new( + ComposeErrorKind::StaleTranslationEntry, + format!("translations.no_change.direct.{source}"), + format!( + "stale no-change source {source:?} did not match any extracted non-empty source text" + ), + )); + } + } + for (context_path, sources) in &translations.no_change.contextual { + for source in sources { + if !seen_no_change_contextual.contains(&(context_path.clone(), source.clone())) { + errors.push(ComposeError::new( + ComposeErrorKind::StaleTranslationEntry, + format!("translations.no_change.contextual.{context_path}.{source}"), + format!( + "invalid contextual no-change: source {source:?} did not match any extracted text under {context_path}; the source key may be stale or the context may be invalid" + ), + )); + } + } + } for path in translations.target_additions.keys() { if !seen_target_additions.contains(path) { errors.push(ComposeError::new( @@ -862,6 +955,8 @@ struct TranslationApplyContext<'a, 'b> { translations: &'a TranslationDictionary, seen_direct: &'b mut BTreeSet, seen_contextual: &'b mut BTreeSet<(String, String)>, + seen_no_change_direct: &'b mut BTreeSet, + seen_no_change_contextual: &'b mut BTreeSet<(String, String)>, seen_target_additions: &'b mut BTreeSet, seen_variables: &'b mut BTreeSet<(String, String)>, seen_adapter_ids: &'b mut BTreeSet<(String, String)>, @@ -938,6 +1033,10 @@ impl TranslationApplyContext<'_, '_> { if direct_translation.is_some() { self.seen_direct.insert(source.clone()); } + let direct_no_change = self.translations.no_change.direct.contains(source.as_str()); + if direct_no_change { + self.seen_no_change_direct.insert(source.clone()); + } let mut contextual_translation: Option<(&String, &String)> = None; for (context_path, replacements) in &self.translations.contextual { @@ -955,6 +1054,15 @@ impl TranslationApplyContext<'_, '_> { } } + let mut contextual_no_change = false; + for (context_path, sources) in &self.translations.no_change.contextual { + if context_matches_path(context_path, &path) && sources.contains(source.as_str()) { + self.seen_no_change_contextual + .insert((context_path.clone(), source.clone())); + contextual_no_change = true; + } + } + let translated = contextual_translation .map(|(_, translated)| translated) .or(direct_translation); @@ -974,12 +1082,16 @@ impl TranslationApplyContext<'_, '_> { return; } + if contextual_no_change || direct_no_change { + return; + } + if self.translations.require_complete { self.errors.push(ComposeError::new( ComposeErrorKind::MissingTranslation, path.clone(), format!( - "missing direct or contextual translation for {value:?} at {path}; add translations.direct, add a translations.contextual entry for path-specific text, or ignore the path" + "missing direct or contextual translation for {value:?} at {path}; add translations.direct, add translations.no_change for intentionally unchanged text, add a translations.contextual entry for path-specific text, or ignore the path" ), )); } @@ -2784,6 +2896,15 @@ pub struct Overlay { pub media_changes: BTreeMap, } +/// Explicitly reviewed source text that intentionally remains unchanged in the target language. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TranslationNoChange { + /// Reusable exact non-empty source strings that intentionally remain unchanged. + pub direct: BTreeSet, + /// Path-scoped exact source strings that intentionally remain unchanged. + pub contextual: BTreeMap>, +} + /// Translation dictionary applied by a translation overlay. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TranslationDictionary { @@ -2791,6 +2912,8 @@ pub struct TranslationDictionary { pub direct: BTreeMap, /// Contextual replacements keyed by stable deck path prefix and exact source text. pub contextual: BTreeMap>, + /// Explicitly reviewed source text that intentionally remains unchanged. + pub no_change: TranslationNoChange, /// Stable deck paths to fill only when the current source value is intentionally blank. pub target_additions: BTreeMap, /// Variable-specific source text to translated text replacements by variable key. @@ -2832,6 +2955,8 @@ impl TranslationCoverageReport { entry.category, TranslationCoverageCategory::StaleDirectKey | TranslationCoverageCategory::StaleContextualKey + | TranslationCoverageCategory::StaleNoChangeDirectKey + | TranslationCoverageCategory::StaleNoChangeContextualKey | TranslationCoverageCategory::StaleTargetAddition | TranslationCoverageCategory::StaleVariableKey | TranslationCoverageCategory::StaleAdapterIdKey @@ -2856,6 +2981,8 @@ pub struct TranslationCoverageEntry { pub enum TranslationCoverageCategory { DirectTranslation, ContextualOverride, + NoChangeDirect, + NoChangeContextual, TargetLanguageAddition, VariableTranslation, AdapterIdTranslation, @@ -2863,6 +2990,8 @@ pub enum TranslationCoverageCategory { UntranslatedFallback, StaleDirectKey, StaleContextualKey, + StaleNoChangeDirectKey, + StaleNoChangeContextualKey, StaleTargetAddition, StaleVariableKey, StaleAdapterIdKey, @@ -2874,6 +3003,8 @@ impl TranslationCoverageCategory { match self { Self::DirectTranslation => "direct_translation", Self::ContextualOverride => "contextual_override", + Self::NoChangeDirect => "no_change_direct", + Self::NoChangeContextual => "no_change_contextual", Self::TargetLanguageAddition => "target_language_addition", Self::VariableTranslation => "variable_translation", Self::AdapterIdTranslation => "adapter_id_translation", @@ -2881,6 +3012,8 @@ impl TranslationCoverageCategory { Self::UntranslatedFallback => "untranslated_fallback", Self::StaleDirectKey => "stale_direct_key", Self::StaleContextualKey => "stale_contextual_key", + Self::StaleNoChangeDirectKey => "stale_no_change_direct_key", + Self::StaleNoChangeContextualKey => "stale_no_change_contextual_key", Self::StaleTargetAddition => "stale_target_addition", Self::StaleVariableKey => "stale_variable_key", Self::StaleAdapterIdKey => "stale_adapter_id_key", @@ -2894,6 +3027,8 @@ impl TranslationCoverageCategory { Self::UntranslatedFallback | Self::StaleDirectKey | Self::StaleContextualKey + | Self::StaleNoChangeDirectKey + | Self::StaleNoChangeContextualKey | Self::StaleTargetAddition | Self::StaleVariableKey | Self::StaleAdapterIdKey diff --git a/crates/brain-brew-core/tests/overlay_compose.rs b/crates/brain-brew-core/tests/overlay_compose.rs index ae1c35b..eb9c548 100644 --- a/crates/brain-brew-core/tests/overlay_compose.rs +++ b/crates/brain-brew-core/tests/overlay_compose.rs @@ -5,7 +5,7 @@ use brain_brew_core::{ ComposeErrorKind, DeckChange, ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, Overlay, OverlayKind, PropertyChange, StableId, TagChange, TranslationCoverageCategory, - TranslationDictionary, + TranslationDictionary, TranslationNoChange, }; #[test] @@ -220,6 +220,7 @@ fn translation_dictionary_reports_stale_direct_entries() { translations: Some(TranslationDictionary { direct: BTreeMap::from([("Missing source".to_owned(), "Mangler".to_owned())]), contextual: BTreeMap::new(), + no_change: TranslationNoChange::default(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -277,6 +278,7 @@ fn translation_dictionary_distinguishes_direct_contextual_and_target_additions() BTreeMap::from([("Shared source".to_owned(), "Svensk kontekst".to_owned())]), ), ]), + no_change: TranslationNoChange::default(), target_additions: BTreeMap::from([( "notes.note.sweden.fields.field.capital".to_owned(), "Stockholm".to_owned(), @@ -328,6 +330,7 @@ fn translation_dictionary_can_translate_tags() { "notes.note.finland".to_owned(), BTreeMap::from([("Europe".to_owned(), "UG::Europe".to_owned())]), )]), + no_change: TranslationNoChange::default(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -349,6 +352,108 @@ fn translation_dictionary_can_translate_tags() { assert!(!note.tags.contains("Nordic")); } +#[test] +fn translation_dictionary_no_change_counts_as_reviewed_without_modifying_output() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::new(), + contextual: BTreeMap::new(), + no_change: TranslationNoChange { + direct: BTreeSet::from(["Finland".to_owned()]), + contextual: BTreeMap::from([( + "notes.note.finland".to_owned(), + BTreeSet::from(["Helsinki".to_owned()]), + )]), + }, + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: true, + ignore_paths: BTreeSet::from([ + "deck.*".to_owned(), + "note_types.*".to_owned(), + "notes.*.fields.field.flag".to_owned(), + "notes.*.tags.*".to_owned(), + ]), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let resolved = base + .compose(std::slice::from_ref(&overlay)) + .expect("no-change covers strict translation checks"); + assert_eq!(resolved, base, "no-change must not modify composed output"); + + let report = base + .translation_coverage(&overlay) + .expect("translation overlay has coverage"); + assert!(!report.has_untranslated_fallbacks()); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::NoChangeDirect + && entry.path == "notes.note.finland.fields.field.country" + && entry.source == "Finland" + })); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::NoChangeContextual + && entry.path == "notes.note.finland.fields.field.capital" + && entry.context.as_deref() == Some("notes.note.finland") + })); +} + +#[test] +fn translation_dictionary_reports_stale_no_change_entries() { + let base = ug_style_deck(); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::new(), + contextual: BTreeMap::new(), + no_change: TranslationNoChange { + direct: BTreeSet::from(["Removed source".to_owned()]), + contextual: BTreeMap::from([( + "notes.note.finland".to_owned(), + BTreeSet::from(["Removed contextual".to_owned()]), + )]), + }, + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::new(), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = base + .translation_coverage(&overlay) + .expect("translation overlay has coverage"); + assert!(report.has_stale_or_invalid_entries()); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::StaleNoChangeDirectKey + && entry.path == "translations.no_change.direct.Removed source" + })); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::StaleNoChangeContextualKey + && entry.path + == "translations.no_change.contextual.notes.note.finland.Removed contextual" + })); + + let report = base + .compose(&[overlay]) + .expect_err("stale no-change entries fail"); + assert!(report.has_kind(ComposeErrorKind::StaleTranslationEntry)); +} + #[test] fn translation_dictionary_reports_missing_direct_translation_when_complete() { let base = ug_style_deck(); @@ -358,6 +463,7 @@ fn translation_dictionary_reports_missing_direct_translation_when_complete() { translations: Some(TranslationDictionary { direct: BTreeMap::new(), contextual: BTreeMap::new(), + no_change: TranslationNoChange::default(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -395,6 +501,7 @@ fn translation_dictionary_contextual_reports_unnecessary_specificity() { "notes.note.finland.fields.field.capital".to_owned(), BTreeMap::from([("Helsinki".to_owned(), "Helsingfors".to_owned())]), )]), + no_change: TranslationNoChange::default(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -427,6 +534,7 @@ fn translation_dictionary_target_addition_fails_when_base_is_not_blank() { translations: Some(TranslationDictionary { direct: BTreeMap::new(), contextual: BTreeMap::new(), + no_change: TranslationNoChange::default(), target_additions: BTreeMap::from([( "notes.note.finland.fields.field.capital".to_owned(), "Helsinki translated".to_owned(), @@ -480,6 +588,7 @@ fn translation_coverage_reports_new_note_and_new_field_fallbacks() { ("Helsinki".to_owned(), "Helsingfors".to_owned()), ]), contextual: BTreeMap::new(), + no_change: TranslationNoChange::default(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -528,6 +637,7 @@ fn translation_coverage_reports_stale_changed_or_removed_source_keys() { translations: Some(TranslationDictionary { direct: BTreeMap::from([("Helsinki".to_owned(), "Helsingfors".to_owned())]), contextual: BTreeMap::new(), + no_change: TranslationNoChange::default(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -573,6 +683,7 @@ fn translation_coverage_reports_path_specific_overrides_and_additions() { "notes.note.finland".to_owned(), BTreeMap::from([("Helsinki".to_owned(), "Helsingfors".to_owned())]), )]), + no_change: TranslationNoChange::default(), target_additions: BTreeMap::from([( "notes.note.finland.fields.field.flag".to_owned(), "".to_owned(), diff --git a/crates/brain-brew-formats/src/canonical_yaml.rs b/crates/brain-brew-formats/src/canonical_yaml.rs index aef67b8..2b56bba 100644 --- a/crates/brain-brew-formats/src/canonical_yaml.rs +++ b/crates/brain-brew-formats/src/canonical_yaml.rs @@ -5,7 +5,8 @@ use brain_brew_core::{ AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplate, CardTemplateChange, ChangeIntent, DeckChange, ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, InvalidStableId, MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, Overlay, OverlayKind, - PropertyChange, StableId, TagChange, TranslationDictionary, ValidationReport, + PropertyChange, StableId, TagChange, TranslationDictionary, TranslationNoChange, + ValidationReport, }; use serde::Deserialize; @@ -558,6 +559,9 @@ fn write_translation_dictionary(out: &mut String, translations: &TranslationDict let nodes = contextual_format_tree(&translations.contextual); write_contextual_nodes(out, " ", &nodes); } + if !translations.no_change.direct.is_empty() || !translations.no_change.contextual.is_empty() { + write_no_change(out, &translations.no_change); + } if !translations.target_additions.is_empty() { writeln!(out, " target_additions:").expect("writing to a string cannot fail"); for (path, value) in &translations.target_additions { @@ -661,6 +665,89 @@ fn write_contextual_nodes( } } +fn write_no_change(out: &mut String, no_change: &TranslationNoChange) { + writeln!(out, " no_change:").expect("writing to a string cannot fail"); + if !no_change.direct.is_empty() { + writeln!(out, " direct:").expect("writing to a string cannot fail"); + for source in &no_change.direct { + writeln!(out, " - {}", yaml_scalar(source)) + .expect("writing to a string cannot fail"); + } + } + if !no_change.contextual.is_empty() { + writeln!(out, " contextual:").expect("writing to a string cannot fail"); + let nodes = no_change_contextual_format_tree(&no_change.contextual); + write_no_change_contextual_nodes(out, " ", &nodes); + } +} + +#[derive(Default)] +struct NoChangeContextualFormatNode { + sources: BTreeSet, + children: BTreeMap, +} + +fn no_change_contextual_format_tree( + contextual: &BTreeMap>, +) -> BTreeMap { + let mut nodes = BTreeMap::new(); + let has_notes_root = contextual.contains_key("notes.note"); + let has_note_types_root = contextual.contains_key("note_types.note-type"); + for (context_path, sources) in contextual { + if !has_notes_root && let Some(suffix) = context_path.strip_prefix("notes.note.") { + insert_no_change_contextual_format_node(&mut nodes, "notes.note", suffix, sources); + } else if !has_note_types_root + && let Some(suffix) = context_path.strip_prefix("note_types.note-type.") + { + insert_no_change_contextual_format_node( + &mut nodes, + "note_types.note-type", + suffix, + sources, + ); + } else { + nodes + .entry(context_path.clone()) + .or_default() + .sources + .extend(sources.clone()); + } + } + nodes +} + +fn insert_no_change_contextual_format_node( + nodes: &mut BTreeMap, + group: &str, + suffix: &str, + sources: &BTreeSet, +) { + nodes + .entry(group.to_owned()) + .or_default() + .children + .entry(suffix.to_owned()) + .or_default() + .sources + .extend(sources.clone()); +} + +fn write_no_change_contextual_nodes( + out: &mut String, + indent: &str, + nodes: &BTreeMap, +) { + for (key, node) in nodes { + writeln!(out, "{indent}{}:", yaml_scalar(key)).expect("writing to a string cannot fail"); + let child_indent = format!("{indent} "); + for source in &node.sources { + writeln!(out, "{}- {}", child_indent, yaml_scalar(source)) + .expect("writing to a string cannot fail"); + } + write_no_change_contextual_nodes(out, &child_indent, &node.children); + } +} + fn write_variables(out: &mut String, indent: &str, variables: &BTreeMap) { if variables.is_empty() { return; @@ -1191,6 +1278,8 @@ struct TranslationDictionaryYaml { #[serde(default)] contextual: BTreeMap, #[serde(default)] + no_change: NoChangeYaml, + #[serde(default)] target_additions: BTreeMap, #[serde(default)] variables: BTreeMap>, @@ -1206,10 +1295,12 @@ impl TranslationDictionaryYaml { fn into_translation_dictionary(self) -> Result { let mut contextual = BTreeMap::new(); flatten_contextual_translations(None, self.contextual, &mut contextual)?; + let no_change = self.no_change.into_no_change()?; Ok(TranslationDictionary { direct: self.direct, contextual, + no_change, target_additions: self.target_additions, variables: self.variables, adapter_ids: self.adapter_ids, @@ -1219,6 +1310,57 @@ impl TranslationDictionaryYaml { } } +#[derive(Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct NoChangeYaml { + #[serde(default)] + direct: BTreeSet, + #[serde(default)] + contextual: BTreeMap, +} + +impl NoChangeYaml { + fn into_no_change(self) -> Result { + let mut contextual = BTreeMap::new(); + flatten_no_change_contextual(None, self.contextual, &mut contextual)?; + Ok(TranslationNoChange { + direct: self.direct, + contextual, + }) + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum NoChangeContextualYaml { + Sources(BTreeSet), + Nested(BTreeMap), +} + +fn flatten_no_change_contextual( + context_path: Option, + entries: BTreeMap, + contextual: &mut BTreeMap>, +) -> Result<(), CanonicalYamlError> { + for (key, entry) in entries { + match entry { + NoChangeContextualYaml::Sources(sources) => { + let context_path = context_path + .as_ref() + .map_or_else(|| key.clone(), |prefix| format!("{prefix}.{key}")); + contextual.entry(context_path).or_default().extend(sources); + } + NoChangeContextualYaml::Nested(nested) => { + let nested_context = context_path + .as_ref() + .map_or_else(|| key.clone(), |prefix| format!("{prefix}.{key}")); + flatten_no_change_contextual(Some(nested_context), nested, contextual)?; + } + } + } + Ok(()) +} + #[derive(Deserialize)] #[serde(untagged)] enum ContextualTranslationYaml { diff --git a/crates/brain-brew-formats/tests/canonical_yaml.rs b/crates/brain-brew-formats/tests/canonical_yaml.rs index 3126952..ff0920a 100644 --- a/crates/brain-brew-formats/tests/canonical_yaml.rs +++ b/crates/brain-brew-formats/tests/canonical_yaml.rs @@ -166,6 +166,36 @@ translations: assert_eq!(json["notes"][0]["guid"], "note-guid-da"); } +#[test] +fn translation_dictionary_parses_nested_no_change_entries() { + let overlay = canonical_yaml::overlay_from_str( + r#"id: overlay.translation.da +kind: translation +translations: + no_change: + direct: + - Andorra + - Sukhumi + contextual: + notes.note: + djibouti.fields.field.capital: + - Djibouti + deck.description: + - Ultimate Geography +"#, + ) + .expect("no-change sections parse"); + + let translations = overlay.translations.expect("translation dictionary"); + assert!(translations.no_change.direct.contains("Andorra")); + assert!(translations.no_change.direct.contains("Sukhumi")); + assert!( + translations.no_change.contextual["notes.note.djibouti.fields.field.capital"] + .contains("Djibouti") + ); + assert!(translations.no_change.contextual["deck.description"].contains("Ultimate Geography")); +} + #[test] fn translation_dictionary_parses_nested_contextual_translations() { let overlay = canonical_yaml::overlay_from_str( diff --git a/crates/brain-brew-formats/tests/overlay_yaml.rs b/crates/brain-brew-formats/tests/overlay_yaml.rs index 8807109..2f2f8ce 100644 --- a/crates/brain-brew-formats/tests/overlay_yaml.rs +++ b/crates/brain-brew-formats/tests/overlay_yaml.rs @@ -437,6 +437,13 @@ translations: notes.note: georgia.fields.field.country: Georgia: Georgien + no_change: + contextual: + notes.note: + canada.fields.field.country: + - Canada + direct: + - Andorra direct: Germany: Deutschland "#, @@ -446,14 +453,21 @@ translations: assert!(formatted.contains( " notes.note:\n georgia.fields.field.country:\n Georgia: Georgien\n" )); + assert!(formatted.contains( + " no_change:\n direct:\n - Andorra\n contextual:\n notes.note:\n canada.fields.field.country:\n - Canada\n" + )); assert!( formatted.find(" direct:\n").unwrap() < formatted.find(" contextual:\n").unwrap(), "direct translations are emitted before contextual translations" ); assert!( - formatted.find(" contextual:\n").unwrap() + formatted.find(" contextual:\n").unwrap() < formatted.find(" no_change:\n").unwrap(), + "contextual translations are emitted before no-change entries" + ); + assert!( + formatted.find(" no_change:\n").unwrap() < formatted.find(" target_additions:\n").unwrap(), - "contextual translations are emitted before target additions" + "no-change entries are emitted before target additions" ); assert!( formatted.find(" target_additions:\n").unwrap() diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index 00a3eff..f5fcefa 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -12,11 +12,12 @@ Translation dictionaries separate source-keyed translations from target-only tex | --- | --- | --- | | `direct` | reusable translations of exact non-empty source strings | `source text: target text` | | `contextual` | path-scoped translations for a source string inside a deck context | `context path -> source text: target text` | +| `no_change` | translator-reviewed text that intentionally stays identical to the source | `direct: [source text]`, `contextual: context path -> [source text]` | | `target_additions` | target-language text for fields intentionally blank in the source deck | `stable deck path: target text` | -Contextual translations win over direct translations. When multiple contextual scopes match, the longest matching context path wins. +Contextual translations win over direct translations. When multiple contextual scopes match, the longest matching context path wins. No-change entries cover missing translation checks but never modify composed output. -The source key in `direct` and `contextual` is the expected base. If the English/source text changes, composition fails with a stale source-key error instead of silently applying the wrong translation. +The source key in `direct`, `contextual`, and `no_change` is the expected base. If the English/source text changes, composition fails with a stale source-key error instead of silently applying the wrong translation or no-change decision. ## Direct translations @@ -67,6 +68,28 @@ A context applies to any extracted string at that path or below it. A note-level Contextual entries may exist with or without a `direct` fallback. If both exist, contextual wins for matching paths and `direct` remains the fallback elsewhere. +## Explicit no-change entries + +Use `translations.no_change` when a translator has reviewed source text and intentionally left it identical in the target language. This is different from a missing translation: future source additions will still be reported until they are either translated or explicitly marked no-change. + +```yaml +translations: + direct: + Germany: Deutschland + no_change: + direct: + - Andorra + - Canada + contextual: + notes.note: + djibouti.fields.field.capital: + - Djibouti +``` + +Use `no_change.direct` for reusable names that should stay identical everywhere. Use `no_change.contextual` when an identical string is only intentional under a specific note, field, or other stable deck context. + +Keep `ignore_paths` for structural paths that translators should not review at all, such as tags, flag/map HTML, or adapter metadata. Use `no_change` for translator-facing text that was reviewed and intentionally left as-is. + ## Target-language additions for blank source fields Use `translations.target_additions` only when blank localized text genuinely belongs to the translation overlay. This is valid when the source deck intentionally has no English text for the field but a target language should supply text. @@ -152,9 +175,9 @@ To seed translator work after adding English notes or fields, run apply explicit brainbrew translations --manifest brainbrew.yaml --target de-standard --apply ``` -Non-interactive `--apply` inserts deterministic `source: source` stubs into `translations.direct` for the missing text fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose one action for all selected rows — direct `source: source`, contextual `source: source` at the suggested safe context, `ignore_paths`, skip — or choose to decide per row. It keeps direct reusable translations distinct from `contextual` overrides and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. +Non-interactive `--apply` preserves the existing scriptable behavior: it inserts deterministic `source: source` translation stubs into `translations.direct` for the missing text fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose one action for all selected rows — mark direct no-change, mark contextual no-change at the suggested safe context, add direct `source: source`, add contextual `source: source`, add `ignore_paths`, skip — or choose to decide per row. It keeps reviewed no-change decisions distinct from real translations and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. -When `require_complete: true`, composition fails if any extracted non-empty translatable string is not translated by `direct`, translated by a matching `contextual` entry, or matched by `ignore_paths`. For release workflows, prefer target-level verification policy in `brainbrew.yaml`: +When `require_complete: true`, composition fails if any extracted non-empty translatable string is not translated by `direct`, translated by a matching `contextual` entry, marked by `no_change`, or matched by `ignore_paths`. For release workflows, prefer target-level verification policy in `brainbrew.yaml`: ```yaml targets: @@ -168,8 +191,8 @@ targets: Translator context views should present extracted strings in the same categories: -- source strings that occur once or can be safely reused are candidates for `direct`; -- repeated source strings should show their stable deck contexts so translators can choose a reusable `direct` translation, `contextual` entries, or both; +- source strings that occur once or can be safely reused are candidates for `direct` or `no_change.direct`; +- repeated source strings should show their stable deck contexts so translators can choose a reusable `direct` translation, `no_change.direct`, `contextual` entries, contextual no-change entries, or both; - blank source fields should be shown as target-language addition opportunities and written to `target_additions` only when the blank text belongs to the translation overlay. During sync/apply, stale source-key errors mean the translator should refresh against the current source deck before editing the target text. Missing direct/contextual translation errors indicate an untranslated extracted string. Invalid contextual errors indicate either a stale source key or an invalid context path. Invalid target-addition errors indicate the source is no longer blank. @@ -182,8 +205,9 @@ The formatter emits translation dictionary sections in this order: 2. `ignore_paths` 3. `direct` 4. `contextual` -5. `target_additions` -6. `variables` -7. `adapter_ids` +5. `no_change` +6. `target_additions` +7. `variables` +8. `adapter_ids` A file with no `direct` section starts at the next non-empty section. That is still deterministic. diff --git a/documentation/docs/reference/decisions/0008-use-source-variables-and-translation-dictionaries.md b/documentation/docs/reference/decisions/0008-use-source-variables-and-translation-dictionaries.md index c501f22..c9ca553 100644 --- a/documentation/docs/reference/decisions/0008-use-source-variables-and-translation-dictionaries.md +++ b/documentation/docs/reference/decisions/0008-use-source-variables-and-translation-dictionaries.md @@ -12,7 +12,7 @@ Translation overlays become hard to review when a small phrase change requires r Canonical Deck entities may define source variables, and translation overlays may use translation dictionaries. -Text can reference variables with `${variable.name}`. Variables are resolved from the most specific scope to the broadest scope: card template, note, note type, then deck. Translation dictionaries map direct reusable source strings, contextual source-string translations scoped by stable deck path, variable values, target-language additions for blank fields, and adapter IDs to translated values. The `direct` and `contextual` source keys act as expected bases; `target_additions` requires the source field to remain blank. +Text can reference variables with `${variable.name}`. Variables are resolved from the most specific scope to the broadest scope: card template, note, note type, then deck. Translation dictionaries map direct reusable source strings, contextual source-string translations scoped by stable deck path, explicit no-change decisions for reviewed text that intentionally stays identical, variable values, target-language additions for blank fields, and adapter IDs to translated values. The `direct`, `contextual`, and `no_change` source keys act as expected bases; `target_additions` requires the source field to remain blank. CrowdAnki and other adapter exports render variables before writing output, so distributable decks contain ordinary adapter-compatible text and HTML. @@ -25,12 +25,13 @@ CrowdAnki and other adapter exports render variables before writing output, so d - Makes translations readable as source text next to translated text. - Fails stale entries when the source text or target path no longer matches. - Supports context-specific translations where the same source phrase needs different target text. +- Distinguishes intentionally unchanged translator-reviewed text from genuinely missing translations. **Cons:** - Variable scoping adds one more concept to Canonical Deck source. - Translation extraction and coverage checks must be deterministic. -- Ambiguous source strings need `contextual` dictionary entries. +- Ambiguous source strings need `contextual` dictionary entries or contextual no-change entries. ## Alternatives Considered @@ -42,6 +43,6 @@ CrowdAnki and other adapter exports render variables before writing output, so d ## Implications - Translation overlays should prefer variable and dictionary changes over structural template forks. -- Stale direct/path-specific dictionary entries and non-blank target additions fail composition. +- Stale direct/path-specific dictionary entries, stale no-change entries, and non-blank target additions fail composition. - Export parity is based on rendered deck semantics, not on preserving variable syntax in adapter output. - Documentation and skills should teach the variable-first workflow for UG-style variants. diff --git a/documentation/docs/reference/yaml.md b/documentation/docs/reference/yaml.md index baf6c73..b76dd5a 100644 --- a/documentation/docs/reference/yaml.md +++ b/documentation/docs/reference/yaml.md @@ -68,6 +68,12 @@ translations: Georgia: Georgien us-georgia.fields.field.region: Georgia: Georgia + no_change: + direct: + - Andorra + contextual: + notes.note.djibouti.fields.field.capital: + - Djibouti target_additions: notes.note.example.fields.field.country-info: Localized blank text. variables: @@ -78,7 +84,7 @@ translations: old-guid: new-guid ``` -Formatter order is deterministic: `require_complete`, `ignore_paths`, `direct`, `contextual`, `target_additions`, `variables`, `adapter_ids`. +Formatter order is deterministic: `require_complete`, `ignore_paths`, `direct`, `contextual`, `no_change`, `target_additions`, `variables`, `adapter_ids`. ## Field additions diff --git a/fixtures/ultimate-geography/overlays/languages/da.yaml b/fixtures/ultimate-geography/overlays/languages/da.yaml index e93127f..497842f 100644 --- a/fixtures/ultimate-geography/overlays/languages/da.yaml +++ b/fixtures/ultimate-geography/overlays/languages/da.yaml @@ -372,6 +372,309 @@ translations: Overseas territory of the United Kingdom.: 'Oversøisk territorie i Storbritannien.' wallis-and-futuna: Overseas territory of France.: 'Fransk oversøisk departement.' + no_change: + direct: + - Abu Dhabi + - Abuja + - Accra + - Afghanistan + - Alaska + - Alofi + - Amman + - Amsterdam + - Andorra + - Andorra la Vella + - Angola + - Anguilla + - Ankara + - Antananarivo + - Apia + - Argentina + - Aruba + - Asmara + - Astana + - 'Asunción' + - Avarua + - Bahrain + - Baku + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangladesh + - Bangui + - Banjul + - Barbados + - Basseterre + - Beijing + - Beirut + - Belarus + - Belfast + - Belize + - Belmopan + - Benin + - Berlin + - Bermuda + - Bern + - Bhutan + - Bissau + - 'Bogotá' + - Bolivia + - Botswana + - Bougainville + - Bratislava + - Brazzaville + - Bridgetown + - Brunei + - Budapest + - Buenos Aires + - Burkina Faso + - Burundi + - Cairo + - Canada + - Canberra + - Caracas + - Cardiff + - Castries + - Charlotte Amalie + - Chile + - Ciudad de la Paz + - Colombia + - Conakry + - Costa Rica + - Cuba + - 'Curaçao' + - Dakar + - Dhaka + - Dili + - Djibouti + - Dodoma + - Doha + - Dominica + - Dublin + - Ecuador + - Edinburgh + - El Salvador + - England + - Eritrea + - Eswatini + - Fiji + - Finland + - Freetown + - Funafuti + - Gabon + - Gaborone + - Georgetown + - Ghana + - Gibraltar + - Gitega + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guatemala City + - Guernsey + - Guinea + - Guinea-Bissau + - Guyana + - Haiti + - Hanoi + - Harare + - Hargeisa + - Hawaii + - Helsinki + - Honduras + - Honiara + - Iran + - Islamabad + - Isle of Man + - Israel + - Jamaica + - Japan + - Java + - Jersey + - Jerusalem + - Jordan + - Juba + - Kabul + - Kaliningrad Oblast + - Kampala + - Kenya + - Khartoum + - Kigali + - Kingston + - Kingstown + - Kinshasa + - Kiribati + - Kosovo + - Kuala Lumpur + - Kuwait + - Kyiv + - Laos + - Lesotho + - Liberia + - Libreville + - Liechtenstein + - Lilongwe + - Lima + - Ljubljana + - 'Lomé' + - London + - Luanda + - Lusaka + - Luxembourg + - Madeira + - Madrid + - Majuro + - Malawi + - Malaysia + - Mali + - Malta + - 'Malé' + - Managua + - Manama + - Manila + - Maputo + - Mariehamn + - Martinique + - Maseru + - Mauritius + - Mayotte + - Mbabane + - Mexico + - Mexico City + - Minsk + - Mogadishu + - Moldova + - Monaco + - Monrovia + - Montenegro + - Montevideo + - Moroni + - Mozambique + - Muscat + - Myanmar + - Nairobi + - Namibia + - Nassau + - Nauru + - Naypyidaw + - Nepal + - New Delhi + - New Zealand + - Ngerulmud + - Niamey + - Nicaragua + - Nicosia + - Niger + - Nigeria + - Niue + - Nouakchott + - 'Nouméa' + - Nuuk + - Oman + - Oranjestad + - Oslo + - Ottawa + - Ouagadougou + - Pakistan + - Palau + - Palikir + - Panama + - Panama City + - Papeete + - Paraguay + - Paramaribo + - Paris + - Peru + - Phnom Penh + - Podgorica + - Port Louis + - Port Moresby + - Port Vila + - Port of Spain + - Port-au-Prince + - Portugal + - Praia + - Pretoria, Cape Town, Bloemfontein + - Pristina + - Puerto Rico + - Pyongyang + - Qatar + - Quito + - Rabat + - 'Reykjavík' + - Riga + - Riyadh + - Roseau + - Rwanda + - 'Réunion' + - Saint Lucia + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Santiago + - Santo Domingo + - Sarajevo + - Senegal + - Seoul + - Sierra Leone + - Singapore + - Skopje + - Sofia + - Somalia + - Somaliland + - Sri Lanka + - Stockholm + - Sucre + - Sudan + - Sukhumi + - Sumatra + - Suva + - Svalbard + - 'São Tomé' + - Taipei + - Taiwan + - Tallinn + - Tanzania + - Tbilisi + - Tegucigalpa + - Thailand + - Tirana + - Tiraspol + - Togo + - Tokyo + - Tonga + - Tripoli + - Tskhinvali + - Tunis + - Turkmenistan + - Tuvalu + - Uganda + - Ukraine + - Uruguay + - Vaduz + - Valletta + - Vanuatu + - Venezuela + - Victoria + - Vientiane + - Vietnam + - Vilnius + - Wales + - Wellington + - Willemstad + - Windhoek + - Yamoussoukro + - 'Yaoundé' + - Yaren + - Yemen + - Zagreb + - Zambia + - Zanzibar + - Zimbabwe target_additions: notes.note.arctic-ocean.fields.field.country-info: 'Også kendt som Det Arktiske Ocean.' notes.note.balkan-peninsula.fields.field.country-info: 'Også kendt som Balkan.' diff --git a/skills/federated-deck-extensions/SKILL.md b/skills/federated-deck-extensions/SKILL.md index 17baa5a..25a74f5 100644 --- a/skills/federated-deck-extensions/SKILL.md +++ b/skills/federated-deck-extensions/SKILL.md @@ -79,7 +79,7 @@ translations: 'Flag similar to {{Flag similarity}}.': 'Flaget ligner {{Flag similarity}}.' ``` -Use `translations.direct` for reusable content text and `translations.contextual` for path-scoped/content-specific exceptions. Do **not** path-scope metadata like `note_types.note-type.ultimate-geography.name` if a variable can express it. +Use `translations.direct` for reusable content text, `translations.contextual` for path-scoped/content-specific exceptions, and `translations.no_change` for translator-reviewed text that intentionally stays identical to the source. Do **not** path-scope metadata like `note_types.note-type.ultimate-geography.name` if a variable can express it. ### 3. Use field fills for non-translation blank content From fcc5fd4b8421bd11002d4ab0ed49bc069fb67d0c Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 16:38:40 +0200 Subject: [PATCH 023/142] mark unchanged UG language text explicitly --- crates/brain-brew-cli/tests/cli.rs | 45 +++ .../overlays/languages/cs.yaml | 203 +++++++++++ .../overlays/languages/de.yaml | 268 +++++++++++++++ .../overlays/languages/es.yaml | 196 +++++++++++ .../overlays/languages/fr.yaml | 227 ++++++++++++ .../overlays/languages/it.yaml | 281 +++++++++++++++ .../overlays/languages/nb.yaml | 323 ++++++++++++++++++ .../overlays/languages/nl.yaml | 273 +++++++++++++++ .../overlays/languages/pl.yaml | 196 +++++++++++ .../overlays/languages/pt.yaml | 125 +++++++ .../overlays/languages/sv.yaml | 301 +++++++++++++++- 11 files changed, 2436 insertions(+), 2 deletions(-) diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 093593d..7c5a393 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -999,6 +999,51 @@ fn translations_default_report_focuses_on_translatable_note_text() { assert!(!out.contains("notes.note.abkhazia.fields.field.capital")); } +#[test] +fn ultimate_geography_language_overlays_have_no_actionable_missing_text() { + let manifest = workspace_root().join("fixtures/ultimate-geography/brainbrew.yaml"); + let output = run([ + "translations", + "--manifest", + manifest.to_str().unwrap(), + "--all-targets", + "--json", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let json: serde_json::Value = serde_json::from_str(&stdout(&output)).unwrap(); + let mut missing = Vec::new(); + for report in json["reports"].as_array().unwrap() { + let file = report["file"].as_str().unwrap(); + if !file.starts_with("overlays/languages/") { + continue; + } + let target = report["target"].as_str().unwrap(); + let overlay = report["overlay"].as_str().unwrap(); + for entry in report["entries"].as_array().unwrap() { + if entry["category"] != "untranslated_fallback" { + continue; + } + let path = entry["path"].as_str().unwrap(); + let source = entry["source"].as_str().unwrap(); + let actionable_note_field = path.starts_with("notes.") + && path.contains(".fields.") + && !path.ends_with(".fields.field.flag") + && !path.ends_with(".fields.field.map") + && !source.trim_start().starts_with(" Date: Mon, 22 Jun 2026 17:06:21 +0200 Subject: [PATCH 024/142] simplify no-change translations to source lists --- .../src/commands/translations.rs | 248 +------ crates/brain-brew-cli/tests/cli.rs | 26 +- crates/brain-brew-core/src/lib.rs | 137 +--- .../brain-brew-core/tests/overlay_compose.rs | 54 +- .../brain-brew-formats/src/canonical_yaml.rs | 144 +--- .../tests/canonical_yaml.rs | 22 +- .../brain-brew-formats/tests/overlay_yaml.rs | 12 +- documentation/docs/authoring/translations.md | 20 +- ...-variables-and-translation-dictionaries.md | 4 +- documentation/docs/reference/yaml.md | 7 +- .../overlays/languages/cs.yaml | 403 ++++++----- .../overlays/languages/da.yaml | 603 ++++++++-------- .../overlays/languages/de.yaml | 533 ++++++++------- .../overlays/languages/es.yaml | 389 ++++++----- .../overlays/languages/fr.yaml | 451 ++++++------ .../overlays/languages/it.yaml | 559 ++++++++------- .../overlays/languages/nb.yaml | 643 +++++++++--------- .../overlays/languages/nl.yaml | 543 ++++++++------- .../overlays/languages/pl.yaml | 389 ++++++----- .../overlays/languages/pt.yaml | 247 ++++--- .../overlays/languages/sv.yaml | 591 ++++++++-------- 21 files changed, 2788 insertions(+), 3237 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index ad2b6fd..894b9a9 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -867,8 +867,7 @@ fn selected_target_names(manifest: &FederatedDeckManifest, args: &TranslationArg struct OverlayEdits { direct: BTreeSet, contextual: BTreeMap>, - no_change_direct: BTreeSet, - no_change_contextual: BTreeMap>, + no_change: BTreeSet, ignore_paths: BTreeSet, } @@ -876,20 +875,14 @@ impl OverlayEdits { fn is_empty(&self) -> bool { self.direct.is_empty() && self.contextual.is_empty() - && self.no_change_direct.is_empty() - && self.no_change_contextual.is_empty() + && self.no_change.is_empty() && self.ignore_paths.is_empty() } fn count(&self) -> usize { self.direct.len() + self.contextual.values().map(BTreeSet::len).sum::() - + self.no_change_direct.len() - + self - .no_change_contextual - .values() - .map(BTreeSet::len) - .sum::() + + self.no_change.len() + self.ignore_paths.len() } } @@ -985,11 +978,7 @@ fn prompt_selective_apply( } else { let labels = vec![ format!( - "mark direct no-change for all {} selected missing translations", - selected_missing.len() - ), - format!( - "mark contextual no-change for all {} selected missing translations", + "mark no-change for all {} selected missing translations", selected_missing.len() ), format!( @@ -1008,13 +997,12 @@ fn prompt_selective_apply( "skip selected missing translations".to_owned(), ]; match ui.select_one("Action for selected missing translations", &labels, 0)? { - 0 => MissingApplyAction::NoChangeDirect, - 1 => MissingApplyAction::NoChangeContextual, - 2 => MissingApplyAction::Direct, - 3 => MissingApplyAction::Contextual, - 4 => MissingApplyAction::IgnorePath, - 5 => MissingApplyAction::DecidePerRow, - 6 => MissingApplyAction::Skip, + 0 => MissingApplyAction::NoChange, + 1 => MissingApplyAction::Direct, + 2 => MissingApplyAction::Contextual, + 3 => MissingApplyAction::IgnorePath, + 4 => MissingApplyAction::DecidePerRow, + 5 => MissingApplyAction::Skip, _ => unreachable!(), } }; @@ -1059,8 +1047,7 @@ fn prompt_selective_apply( #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum MissingApplyAction { - NoChangeDirect, - NoChangeContextual, + NoChange, Direct, Contextual, IgnorePath, @@ -1074,8 +1061,7 @@ fn prompt_missing_apply_action( ) -> Result { let context = contextual_context_for_entry(entry); let action_labels = vec![ - "mark direct no-change".to_owned(), - format!("mark contextual no-change at {context}"), + "mark no-change".to_owned(), "add direct source→source translation stub".to_owned(), format!("add contextual source→source translation stub at {context}"), "add ignore path for this deck path".to_owned(), @@ -1091,12 +1077,11 @@ fn prompt_missing_apply_action( &action_labels, 0, )? { - 0 => Ok(MissingApplyAction::NoChangeDirect), - 1 => Ok(MissingApplyAction::NoChangeContextual), - 2 => Ok(MissingApplyAction::Direct), - 3 => Ok(MissingApplyAction::Contextual), - 4 => Ok(MissingApplyAction::IgnorePath), - 5 => Ok(MissingApplyAction::Skip), + 0 => Ok(MissingApplyAction::NoChange), + 1 => Ok(MissingApplyAction::Direct), + 2 => Ok(MissingApplyAction::Contextual), + 3 => Ok(MissingApplyAction::IgnorePath), + 4 => Ok(MissingApplyAction::Skip), _ => unreachable!(), } } @@ -1109,15 +1094,8 @@ fn apply_missing_translation_action( ) { let file_edits = edits.entry(overlay_path.to_path_buf()).or_default(); match action { - MissingApplyAction::NoChangeDirect => { - file_edits.no_change_direct.insert(entry.source.clone()); - } - MissingApplyAction::NoChangeContextual => { - file_edits - .no_change_contextual - .entry(contextual_context_for_entry(entry)) - .or_default() - .insert(entry.source.clone()); + MissingApplyAction::NoChange => { + file_edits.no_change.insert(entry.source.clone()); } MissingApplyAction::Direct => { file_edits.direct.insert(entry.source.clone()); @@ -1254,19 +1232,8 @@ fn compose_lenient_translation_overlay( } } } - TranslationCoverageCategory::StaleNoChangeDirectKey => { - translations.no_change.direct.remove(&entry.source); - } - TranslationCoverageCategory::StaleNoChangeContextualKey => { - if let Some(context) = &entry.context - && let Some(sources) = - translations.no_change.contextual.get_mut(context) - { - sources.remove(&entry.source); - if sources.is_empty() { - translations.no_change.contextual.remove(context); - } - } + TranslationCoverageCategory::StaleNoChangeKey => { + translations.no_change.remove(&entry.source); } TranslationCoverageCategory::StaleTargetAddition | TranslationCoverageCategory::InvalidTargetAddition => { @@ -1413,8 +1380,7 @@ fn print_human_reports(reports: &[ScopedTranslationReport], full: bool) { println!( " {}: {}", color_stdout("intentionally unchanged text", "34"), - all_counts.get("no_change_direct").copied().unwrap_or(0) - + all_counts.get("no_change_contextual").copied().unwrap_or(0) + all_counts.get("no_change").copied().unwrap_or(0) ); println!( " {}: {}", @@ -1537,8 +1503,7 @@ fn is_stale_or_invalid(category: TranslationCoverageCategory) -> bool { category, TranslationCoverageCategory::StaleDirectKey | TranslationCoverageCategory::StaleContextualKey - | TranslationCoverageCategory::StaleNoChangeDirectKey - | TranslationCoverageCategory::StaleNoChangeContextualKey + | TranslationCoverageCategory::StaleNoChangeKey | TranslationCoverageCategory::StaleTargetAddition | TranslationCoverageCategory::StaleVariableKey | TranslationCoverageCategory::StaleAdapterIdKey @@ -1610,12 +1575,8 @@ fn apply_overlay_edits(path: &Path, edits: &OverlayEdits) -> Result, - contextual: &BTreeMap>, -) -> String { - let spans = line_spans(input); - let Some(translations_index) = find_translations_line(input, &spans) else { - return insert_new_no_change_section(input, direct, contextual); - }; - if find_translation_section(input, &spans, translations_index, "no_change").is_none() { - return insert_new_no_change_section(input, direct, contextual); - } - - let mut output = input.to_owned(); - if !direct.is_empty() { - let entries = direct - .iter() - .map(|source| format!(" - {}\n", yaml_scalar(source))) - .collect::(); - output = insert_no_change_subsection(&output, "direct", &entries, &[]); - } - if !contextual.is_empty() { - let mut entries = String::new(); - for (context, sources) in contextual { - entries.push_str(&format!(" {}:\n", yaml_scalar(context))); - for source in sources { - entries.push_str(&format!(" - {}\n", yaml_scalar(source))); - } - } - output = insert_no_change_subsection(&output, "contextual", &entries, &["direct"]); - } - output -} - -fn insert_new_no_change_section( - input: &str, - direct: &BTreeSet, - contextual: &BTreeMap>, -) -> String { - let mut entries = String::new(); - if !direct.is_empty() { - entries.push_str(" direct:\n"); - for source in direct { - entries.push_str(&format!(" - {}\n", yaml_scalar(source))); - } - } - if !contextual.is_empty() { - entries.push_str(" contextual:\n"); - for (context, sources) in contextual { - entries.push_str(&format!(" {}:\n", yaml_scalar(context))); - for source in sources { - entries.push_str(&format!(" - {}\n", yaml_scalar(source))); - } - } - } +fn insert_no_change_lines(input: &str, sources: &BTreeSet) -> String { + let entries = sources + .iter() + .map(|source| format!(" - {}\n", yaml_scalar(source))) + .collect::(); insert_translation_section( input, "no_change", @@ -1719,99 +1630,6 @@ fn insert_new_no_change_section( ) } -fn insert_no_change_subsection( - input: &str, - subsection_name: &str, - entries: &str, - preferred_after_subsections: &[&str], -) -> String { - let spans = line_spans(input); - let Some(translations_index) = find_translations_line(input, &spans) else { - return input.to_owned(); - }; - let Some(no_change_index) = - find_translation_section(input, &spans, translations_index, "no_change") - else { - return input.to_owned(); - }; - let no_change_end = translation_section_end(input, &spans, no_change_index); - - if let Some(section_index) = find_no_change_subsection( - input, - &spans, - no_change_index, - no_change_end, - subsection_name, - ) { - let insert_at = indented_section_end(input, &spans, section_index, 4); - return insert_at_byte(input, insert_at, entries); - } - - for anchor in preferred_after_subsections { - if let Some(section_index) = - find_no_change_subsection(input, &spans, no_change_index, no_change_end, anchor) - { - let insert_at = indented_section_end(input, &spans, section_index, 4); - return insert_at_byte( - input, - insert_at, - &format!(" {subsection_name}:\n{entries}"), - ); - } - } - - let insert_at = spans[no_change_index].1; - insert_at_byte( - input, - insert_at, - &format!(" {subsection_name}:\n{entries}"), - ) -} - -fn find_no_change_subsection( - input: &str, - spans: &[(usize, usize)], - no_change_index: usize, - no_change_end: usize, - subsection_name: &str, -) -> Option { - let needle = format!(" {subsection_name}:"); - spans - .iter() - .enumerate() - .skip(no_change_index + 1) - .take_while(|(_, (start, _))| *start < no_change_end) - .find(|(_, (start, end))| input[*start..*end].trim_end() == needle) - .map(|(index, _)| index) -} - -fn indented_section_end( - input: &str, - spans: &[(usize, usize)], - section_index: usize, - section_indent: usize, -) -> usize { - spans - .iter() - .enumerate() - .skip(section_index + 1) - .find(|(_, (start, end))| { - let line = &input[*start..*end]; - !line.trim().is_empty() - && !line.trim_start().starts_with('#') - && leading_spaces(line) <= section_indent - }) - .map(|(_, (start, _))| *start) - .unwrap_or(input.len()) -} - -fn leading_spaces(line: &str) -> usize { - line.as_bytes() - .iter() - .take_while(|byte| **byte == b' ') - .count() -} - fn insert_ignore_path_lines(input: &str, paths: &BTreeSet) -> String { let entries = paths .iter() @@ -2209,8 +2027,7 @@ fn color_category(category: TranslationCoverageCategory, text: &str) -> String { match category { TranslationCoverageCategory::DirectTranslation => color_stdout(text, "32"), TranslationCoverageCategory::ContextualOverride => color_stdout(text, "36"), - TranslationCoverageCategory::NoChangeDirect - | TranslationCoverageCategory::NoChangeContextual => color_stdout(text, "34"), + TranslationCoverageCategory::NoChange => color_stdout(text, "34"), TranslationCoverageCategory::TargetLanguageAddition => color_stdout(text, "35"), TranslationCoverageCategory::VariableTranslation | TranslationCoverageCategory::AdapterIdTranslation @@ -2218,8 +2035,7 @@ fn color_category(category: TranslationCoverageCategory, text: &str) -> String { TranslationCoverageCategory::UntranslatedFallback => color_stdout(text, "31"), TranslationCoverageCategory::StaleDirectKey | TranslationCoverageCategory::StaleContextualKey - | TranslationCoverageCategory::StaleNoChangeDirectKey - | TranslationCoverageCategory::StaleNoChangeContextualKey + | TranslationCoverageCategory::StaleNoChangeKey | TranslationCoverageCategory::StaleTargetAddition | TranslationCoverageCategory::StaleVariableKey | TranslationCoverageCategory::StaleAdapterIdKey diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 7c5a393..51cb466 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -1177,11 +1177,11 @@ fn translations_interactive_apply_can_mark_all_selected_rows_no_change() { assert!(output.status.success(), "stderr: {}", stderr(&output)); let out = stdout(&output); assert!(out.contains("Action for selected missing translations")); - assert!(out.contains("mark direct no-change for all 2 selected missing translations")); + assert!(out.contains("mark no-change for all 2 selected missing translations")); let updated = fs::read_to_string(overlay_path).unwrap(); - assert!(updated.contains(" no_change:\n direct:\n")); - assert!(updated.contains(" - Stockholm\n")); - assert!(updated.contains(" - Sweden\n")); + assert!(updated.contains(" no_change:\n")); + assert!(updated.contains(" - Stockholm\n")); + assert!(updated.contains(" - Sweden\n")); } #[test] @@ -1202,7 +1202,7 @@ fn translations_interactive_apply_can_use_one_translation_stub_action_for_all_se "--apply", "--interactive", ], - "\n\x1b[B\x1b[B\n\n", + "\n\x1b[B\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1233,7 +1233,7 @@ fn translations_interactive_apply_can_insert_contextual_stub() { "--apply", "--interactive", ], - "\n\x1b[B\x1b[B\x1b[B\n\n", + "\n\x1b[B\x1b[B\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1259,7 +1259,7 @@ fn translations_interactive_apply_can_insert_ignore_path() { "--apply", "--interactive", ], - "\n\x1b[B\x1b[B\x1b[B\x1b[B\n\n", + "\n\x1b[B\x1b[B\x1b[B\n\n", ); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1283,11 +1283,8 @@ translations: - notes.*.tags.* - notes.*.fields.field.flag no_change: - direct: - - Sweden - contextual: - notes.note.sweden: - - Stockholm + - Stockholm + - Sweden "#, ) .unwrap(); @@ -1427,9 +1424,8 @@ translations: finland: Helsinki: Helsingfors no_change: - direct: - - Stockholm - - Sweden + - Stockholm + - Sweden target_additions: notes.note.finland.fields.field.flag: '' "#, diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs index bedaa15..40ea45a 100644 --- a/crates/brain-brew-core/src/lib.rs +++ b/crates/brain-brew-core/src/lib.rs @@ -384,10 +384,10 @@ fn translation_coverage_report( let mut builder = TranslationCoverageBuilder { translations, entries: Vec::new(), + seen_sources: BTreeSet::new(), seen_direct: BTreeSet::new(), seen_contextual: BTreeSet::new(), - seen_no_change_direct: BTreeSet::new(), - seen_no_change_contextual: BTreeSet::new(), + seen_no_change: BTreeSet::new(), seen_target_additions: BTreeSet::new(), seen_variables: BTreeSet::new(), seen_adapter_ids: BTreeSet::new(), @@ -460,10 +460,10 @@ fn translation_coverage_report( struct TranslationCoverageBuilder<'a> { translations: &'a TranslationDictionary, entries: Vec, + seen_sources: BTreeSet, seen_direct: BTreeSet, seen_contextual: BTreeSet<(String, String)>, - seen_no_change_direct: BTreeSet, - seen_no_change_contextual: BTreeSet<(String, String)>, + seen_no_change: BTreeSet, seen_target_additions: BTreeSet, seen_variables: BTreeSet<(String, String)>, seen_adapter_ids: BTreeSet<(String, String)>, @@ -526,14 +526,12 @@ impl TranslationCoverageBuilder<'_> { } let source = value.to_owned(); + self.seen_sources.insert(source.clone()); let direct_translation = self.translations.direct.get(value); if direct_translation.is_some() { self.seen_direct.insert(source.clone()); } - let direct_no_change = self.translations.no_change.direct.contains(value); - if direct_no_change { - self.seen_no_change_direct.insert(source.clone()); - } + let no_change = self.translations.no_change.contains(value); let mut contextual_translation: Option<(&String, &String)> = None; for (context_path, replacements) in &self.translations.contextual { @@ -551,20 +549,6 @@ impl TranslationCoverageBuilder<'_> { } } - let mut contextual_no_change: Option<&String> = None; - for (context_path, sources) in &self.translations.no_change.contextual { - if context_matches_path(context_path, &path) && sources.contains(value) { - self.seen_no_change_contextual - .insert((context_path.clone(), source.clone())); - if contextual_no_change - .as_ref() - .is_none_or(|current_context| context_path.len() > current_context.len()) - { - contextual_no_change = Some(context_path); - } - } - } - if let Some((context_path, translated)) = contextual_translation { self.entries.push(TranslationCoverageEntry { category: TranslationCoverageCategory::ContextualOverride, @@ -581,17 +565,10 @@ impl TranslationCoverageBuilder<'_> { translated: Some(translated.clone()), context: None, }); - } else if let Some(context_path) = contextual_no_change { - self.entries.push(TranslationCoverageEntry { - category: TranslationCoverageCategory::NoChangeContextual, - path, - source: source.clone(), - translated: Some(source), - context: Some(context_path.clone()), - }); - } else if direct_no_change { + } else if no_change { + self.seen_no_change.insert(source.clone()); self.entries.push(TranslationCoverageEntry { - category: TranslationCoverageCategory::NoChangeDirect, + category: TranslationCoverageCategory::NoChange, path, source: source.clone(), translated: Some(source), @@ -662,33 +639,17 @@ impl TranslationCoverageBuilder<'_> { } } } - for source in &self.translations.no_change.direct { - if !self.seen_no_change_direct.contains(source) { + for source in &self.translations.no_change { + if !self.seen_sources.contains(source) { self.entries.push(TranslationCoverageEntry { - category: TranslationCoverageCategory::StaleNoChangeDirectKey, - path: format!("translations.no_change.direct.{source}"), + category: TranslationCoverageCategory::StaleNoChangeKey, + path: format!("translations.no_change.{source}"), source: source.clone(), translated: Some(source.clone()), context: None, }); } } - for (context_path, sources) in &self.translations.no_change.contextual { - for source in sources { - if !self - .seen_no_change_contextual - .contains(&(context_path.clone(), source.clone())) - { - self.entries.push(TranslationCoverageEntry { - category: TranslationCoverageCategory::StaleNoChangeContextualKey, - path: format!("translations.no_change.contextual.{context_path}.{source}"), - source: source.clone(), - translated: Some(source.clone()), - context: Some(context_path.clone()), - }); - } - } - } for (path, translated) in &self.translations.target_additions { if !self.seen_target_additions.contains(path) { self.entries.push(TranslationCoverageEntry { @@ -756,8 +717,6 @@ fn apply_translation_dictionary( ) { let mut seen_direct = BTreeSet::new(); let mut seen_contextual = BTreeSet::new(); - let mut seen_no_change_direct = BTreeSet::new(); - let mut seen_no_change_contextual = BTreeSet::new(); let mut seen_target_additions = BTreeSet::new(); let mut seen_variables = BTreeSet::new(); let mut seen_adapter_ids = BTreeSet::new(); @@ -769,8 +728,6 @@ fn apply_translation_dictionary( translations, seen_direct: &mut seen_direct, seen_contextual: &mut seen_contextual, - seen_no_change_direct: &mut seen_no_change_direct, - seen_no_change_contextual: &mut seen_no_change_contextual, seen_target_additions: &mut seen_target_additions, seen_variables: &mut seen_variables, seen_adapter_ids: &mut seen_adapter_ids, @@ -887,30 +844,17 @@ fn apply_translation_dictionary( } } } - for source in &translations.no_change.direct { - if !seen_no_change_direct.contains(source) { + for source in &translations.no_change { + if !source_paths.contains_key(source) { errors.push(ComposeError::new( ComposeErrorKind::StaleTranslationEntry, - format!("translations.no_change.direct.{source}"), + format!("translations.no_change.{source}"), format!( "stale no-change source {source:?} did not match any extracted non-empty source text" ), )); } } - for (context_path, sources) in &translations.no_change.contextual { - for source in sources { - if !seen_no_change_contextual.contains(&(context_path.clone(), source.clone())) { - errors.push(ComposeError::new( - ComposeErrorKind::StaleTranslationEntry, - format!("translations.no_change.contextual.{context_path}.{source}"), - format!( - "invalid contextual no-change: source {source:?} did not match any extracted text under {context_path}; the source key may be stale or the context may be invalid" - ), - )); - } - } - } for path in translations.target_additions.keys() { if !seen_target_additions.contains(path) { errors.push(ComposeError::new( @@ -955,8 +899,6 @@ struct TranslationApplyContext<'a, 'b> { translations: &'a TranslationDictionary, seen_direct: &'b mut BTreeSet, seen_contextual: &'b mut BTreeSet<(String, String)>, - seen_no_change_direct: &'b mut BTreeSet, - seen_no_change_contextual: &'b mut BTreeSet<(String, String)>, seen_target_additions: &'b mut BTreeSet, seen_variables: &'b mut BTreeSet<(String, String)>, seen_adapter_ids: &'b mut BTreeSet<(String, String)>, @@ -1033,10 +975,7 @@ impl TranslationApplyContext<'_, '_> { if direct_translation.is_some() { self.seen_direct.insert(source.clone()); } - let direct_no_change = self.translations.no_change.direct.contains(source.as_str()); - if direct_no_change { - self.seen_no_change_direct.insert(source.clone()); - } + let no_change = self.translations.no_change.contains(source.as_str()); let mut contextual_translation: Option<(&String, &String)> = None; for (context_path, replacements) in &self.translations.contextual { @@ -1054,15 +993,6 @@ impl TranslationApplyContext<'_, '_> { } } - let mut contextual_no_change = false; - for (context_path, sources) in &self.translations.no_change.contextual { - if context_matches_path(context_path, &path) && sources.contains(source.as_str()) { - self.seen_no_change_contextual - .insert((context_path.clone(), source.clone())); - contextual_no_change = true; - } - } - let translated = contextual_translation .map(|(_, translated)| translated) .or(direct_translation); @@ -1082,7 +1012,7 @@ impl TranslationApplyContext<'_, '_> { return; } - if contextual_no_change || direct_no_change { + if no_change { return; } @@ -2896,15 +2826,6 @@ pub struct Overlay { pub media_changes: BTreeMap, } -/// Explicitly reviewed source text that intentionally remains unchanged in the target language. -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct TranslationNoChange { - /// Reusable exact non-empty source strings that intentionally remain unchanged. - pub direct: BTreeSet, - /// Path-scoped exact source strings that intentionally remain unchanged. - pub contextual: BTreeMap>, -} - /// Translation dictionary applied by a translation overlay. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TranslationDictionary { @@ -2912,8 +2833,8 @@ pub struct TranslationDictionary { pub direct: BTreeMap, /// Contextual replacements keyed by stable deck path prefix and exact source text. pub contextual: BTreeMap>, - /// Explicitly reviewed source text that intentionally remains unchanged. - pub no_change: TranslationNoChange, + /// Exact non-empty source strings reviewed as intentionally unchanged. + pub no_change: BTreeSet, /// Stable deck paths to fill only when the current source value is intentionally blank. pub target_additions: BTreeMap, /// Variable-specific source text to translated text replacements by variable key. @@ -2955,8 +2876,7 @@ impl TranslationCoverageReport { entry.category, TranslationCoverageCategory::StaleDirectKey | TranslationCoverageCategory::StaleContextualKey - | TranslationCoverageCategory::StaleNoChangeDirectKey - | TranslationCoverageCategory::StaleNoChangeContextualKey + | TranslationCoverageCategory::StaleNoChangeKey | TranslationCoverageCategory::StaleTargetAddition | TranslationCoverageCategory::StaleVariableKey | TranslationCoverageCategory::StaleAdapterIdKey @@ -2981,8 +2901,7 @@ pub struct TranslationCoverageEntry { pub enum TranslationCoverageCategory { DirectTranslation, ContextualOverride, - NoChangeDirect, - NoChangeContextual, + NoChange, TargetLanguageAddition, VariableTranslation, AdapterIdTranslation, @@ -2990,8 +2909,7 @@ pub enum TranslationCoverageCategory { UntranslatedFallback, StaleDirectKey, StaleContextualKey, - StaleNoChangeDirectKey, - StaleNoChangeContextualKey, + StaleNoChangeKey, StaleTargetAddition, StaleVariableKey, StaleAdapterIdKey, @@ -3003,8 +2921,7 @@ impl TranslationCoverageCategory { match self { Self::DirectTranslation => "direct_translation", Self::ContextualOverride => "contextual_override", - Self::NoChangeDirect => "no_change_direct", - Self::NoChangeContextual => "no_change_contextual", + Self::NoChange => "no_change", Self::TargetLanguageAddition => "target_language_addition", Self::VariableTranslation => "variable_translation", Self::AdapterIdTranslation => "adapter_id_translation", @@ -3012,8 +2929,7 @@ impl TranslationCoverageCategory { Self::UntranslatedFallback => "untranslated_fallback", Self::StaleDirectKey => "stale_direct_key", Self::StaleContextualKey => "stale_contextual_key", - Self::StaleNoChangeDirectKey => "stale_no_change_direct_key", - Self::StaleNoChangeContextualKey => "stale_no_change_contextual_key", + Self::StaleNoChangeKey => "stale_no_change_key", Self::StaleTargetAddition => "stale_target_addition", Self::StaleVariableKey => "stale_variable_key", Self::StaleAdapterIdKey => "stale_adapter_id_key", @@ -3027,8 +2943,7 @@ impl TranslationCoverageCategory { Self::UntranslatedFallback | Self::StaleDirectKey | Self::StaleContextualKey - | Self::StaleNoChangeDirectKey - | Self::StaleNoChangeContextualKey + | Self::StaleNoChangeKey | Self::StaleTargetAddition | Self::StaleVariableKey | Self::StaleAdapterIdKey diff --git a/crates/brain-brew-core/tests/overlay_compose.rs b/crates/brain-brew-core/tests/overlay_compose.rs index eb9c548..6f31747 100644 --- a/crates/brain-brew-core/tests/overlay_compose.rs +++ b/crates/brain-brew-core/tests/overlay_compose.rs @@ -5,7 +5,7 @@ use brain_brew_core::{ ComposeErrorKind, DeckChange, ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, Overlay, OverlayKind, PropertyChange, StableId, TagChange, TranslationCoverageCategory, - TranslationDictionary, TranslationNoChange, + TranslationDictionary, }; #[test] @@ -220,7 +220,7 @@ fn translation_dictionary_reports_stale_direct_entries() { translations: Some(TranslationDictionary { direct: BTreeMap::from([("Missing source".to_owned(), "Mangler".to_owned())]), contextual: BTreeMap::new(), - no_change: TranslationNoChange::default(), + no_change: BTreeSet::new(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -278,7 +278,7 @@ fn translation_dictionary_distinguishes_direct_contextual_and_target_additions() BTreeMap::from([("Shared source".to_owned(), "Svensk kontekst".to_owned())]), ), ]), - no_change: TranslationNoChange::default(), + no_change: BTreeSet::new(), target_additions: BTreeMap::from([( "notes.note.sweden.fields.field.capital".to_owned(), "Stockholm".to_owned(), @@ -330,7 +330,7 @@ fn translation_dictionary_can_translate_tags() { "notes.note.finland".to_owned(), BTreeMap::from([("Europe".to_owned(), "UG::Europe".to_owned())]), )]), - no_change: TranslationNoChange::default(), + no_change: BTreeSet::new(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -361,13 +361,7 @@ fn translation_dictionary_no_change_counts_as_reviewed_without_modifying_output( translations: Some(TranslationDictionary { direct: BTreeMap::new(), contextual: BTreeMap::new(), - no_change: TranslationNoChange { - direct: BTreeSet::from(["Finland".to_owned()]), - contextual: BTreeMap::from([( - "notes.note.finland".to_owned(), - BTreeSet::from(["Helsinki".to_owned()]), - )]), - }, + no_change: BTreeSet::from(["Finland".to_owned(), "Helsinki".to_owned()]), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -395,14 +389,14 @@ fn translation_dictionary_no_change_counts_as_reviewed_without_modifying_output( .expect("translation overlay has coverage"); assert!(!report.has_untranslated_fallbacks()); assert!(report.entries.iter().any(|entry| { - entry.category == TranslationCoverageCategory::NoChangeDirect + entry.category == TranslationCoverageCategory::NoChange && entry.path == "notes.note.finland.fields.field.country" && entry.source == "Finland" })); assert!(report.entries.iter().any(|entry| { - entry.category == TranslationCoverageCategory::NoChangeContextual + entry.category == TranslationCoverageCategory::NoChange && entry.path == "notes.note.finland.fields.field.capital" - && entry.context.as_deref() == Some("notes.note.finland") + && entry.source == "Helsinki" })); } @@ -415,13 +409,10 @@ fn translation_dictionary_reports_stale_no_change_entries() { translations: Some(TranslationDictionary { direct: BTreeMap::new(), contextual: BTreeMap::new(), - no_change: TranslationNoChange { - direct: BTreeSet::from(["Removed source".to_owned()]), - contextual: BTreeMap::from([( - "notes.note.finland".to_owned(), - BTreeSet::from(["Removed contextual".to_owned()]), - )]), - }, + no_change: BTreeSet::from([ + "Removed source".to_owned(), + "Removed contextual".to_owned(), + ]), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -439,13 +430,12 @@ fn translation_dictionary_reports_stale_no_change_entries() { .expect("translation overlay has coverage"); assert!(report.has_stale_or_invalid_entries()); assert!(report.entries.iter().any(|entry| { - entry.category == TranslationCoverageCategory::StaleNoChangeDirectKey - && entry.path == "translations.no_change.direct.Removed source" + entry.category == TranslationCoverageCategory::StaleNoChangeKey + && entry.path == "translations.no_change.Removed source" })); assert!(report.entries.iter().any(|entry| { - entry.category == TranslationCoverageCategory::StaleNoChangeContextualKey - && entry.path - == "translations.no_change.contextual.notes.note.finland.Removed contextual" + entry.category == TranslationCoverageCategory::StaleNoChangeKey + && entry.path == "translations.no_change.Removed contextual" })); let report = base @@ -463,7 +453,7 @@ fn translation_dictionary_reports_missing_direct_translation_when_complete() { translations: Some(TranslationDictionary { direct: BTreeMap::new(), contextual: BTreeMap::new(), - no_change: TranslationNoChange::default(), + no_change: BTreeSet::new(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -501,7 +491,7 @@ fn translation_dictionary_contextual_reports_unnecessary_specificity() { "notes.note.finland.fields.field.capital".to_owned(), BTreeMap::from([("Helsinki".to_owned(), "Helsingfors".to_owned())]), )]), - no_change: TranslationNoChange::default(), + no_change: BTreeSet::new(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -534,7 +524,7 @@ fn translation_dictionary_target_addition_fails_when_base_is_not_blank() { translations: Some(TranslationDictionary { direct: BTreeMap::new(), contextual: BTreeMap::new(), - no_change: TranslationNoChange::default(), + no_change: BTreeSet::new(), target_additions: BTreeMap::from([( "notes.note.finland.fields.field.capital".to_owned(), "Helsinki translated".to_owned(), @@ -588,7 +578,7 @@ fn translation_coverage_reports_new_note_and_new_field_fallbacks() { ("Helsinki".to_owned(), "Helsingfors".to_owned()), ]), contextual: BTreeMap::new(), - no_change: TranslationNoChange::default(), + no_change: BTreeSet::new(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -637,7 +627,7 @@ fn translation_coverage_reports_stale_changed_or_removed_source_keys() { translations: Some(TranslationDictionary { direct: BTreeMap::from([("Helsinki".to_owned(), "Helsingfors".to_owned())]), contextual: BTreeMap::new(), - no_change: TranslationNoChange::default(), + no_change: BTreeSet::new(), target_additions: BTreeMap::new(), variables: BTreeMap::new(), adapter_ids: BTreeMap::new(), @@ -683,7 +673,7 @@ fn translation_coverage_reports_path_specific_overrides_and_additions() { "notes.note.finland".to_owned(), BTreeMap::from([("Helsinki".to_owned(), "Helsingfors".to_owned())]), )]), - no_change: TranslationNoChange::default(), + no_change: BTreeSet::new(), target_additions: BTreeMap::from([( "notes.note.finland.fields.field.flag".to_owned(), "".to_owned(), diff --git a/crates/brain-brew-formats/src/canonical_yaml.rs b/crates/brain-brew-formats/src/canonical_yaml.rs index 2b56bba..0ea69e2 100644 --- a/crates/brain-brew-formats/src/canonical_yaml.rs +++ b/crates/brain-brew-formats/src/canonical_yaml.rs @@ -5,8 +5,7 @@ use brain_brew_core::{ AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplate, CardTemplateChange, ChangeIntent, DeckChange, ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, InvalidStableId, MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, Overlay, OverlayKind, - PropertyChange, StableId, TagChange, TranslationDictionary, TranslationNoChange, - ValidationReport, + PropertyChange, StableId, TagChange, TranslationDictionary, ValidationReport, }; use serde::Deserialize; @@ -559,7 +558,7 @@ fn write_translation_dictionary(out: &mut String, translations: &TranslationDict let nodes = contextual_format_tree(&translations.contextual); write_contextual_nodes(out, " ", &nodes); } - if !translations.no_change.direct.is_empty() || !translations.no_change.contextual.is_empty() { + if !translations.no_change.is_empty() { write_no_change(out, &translations.no_change); } if !translations.target_additions.is_empty() { @@ -665,86 +664,10 @@ fn write_contextual_nodes( } } -fn write_no_change(out: &mut String, no_change: &TranslationNoChange) { +fn write_no_change(out: &mut String, no_change: &BTreeSet) { writeln!(out, " no_change:").expect("writing to a string cannot fail"); - if !no_change.direct.is_empty() { - writeln!(out, " direct:").expect("writing to a string cannot fail"); - for source in &no_change.direct { - writeln!(out, " - {}", yaml_scalar(source)) - .expect("writing to a string cannot fail"); - } - } - if !no_change.contextual.is_empty() { - writeln!(out, " contextual:").expect("writing to a string cannot fail"); - let nodes = no_change_contextual_format_tree(&no_change.contextual); - write_no_change_contextual_nodes(out, " ", &nodes); - } -} - -#[derive(Default)] -struct NoChangeContextualFormatNode { - sources: BTreeSet, - children: BTreeMap, -} - -fn no_change_contextual_format_tree( - contextual: &BTreeMap>, -) -> BTreeMap { - let mut nodes = BTreeMap::new(); - let has_notes_root = contextual.contains_key("notes.note"); - let has_note_types_root = contextual.contains_key("note_types.note-type"); - for (context_path, sources) in contextual { - if !has_notes_root && let Some(suffix) = context_path.strip_prefix("notes.note.") { - insert_no_change_contextual_format_node(&mut nodes, "notes.note", suffix, sources); - } else if !has_note_types_root - && let Some(suffix) = context_path.strip_prefix("note_types.note-type.") - { - insert_no_change_contextual_format_node( - &mut nodes, - "note_types.note-type", - suffix, - sources, - ); - } else { - nodes - .entry(context_path.clone()) - .or_default() - .sources - .extend(sources.clone()); - } - } - nodes -} - -fn insert_no_change_contextual_format_node( - nodes: &mut BTreeMap, - group: &str, - suffix: &str, - sources: &BTreeSet, -) { - nodes - .entry(group.to_owned()) - .or_default() - .children - .entry(suffix.to_owned()) - .or_default() - .sources - .extend(sources.clone()); -} - -fn write_no_change_contextual_nodes( - out: &mut String, - indent: &str, - nodes: &BTreeMap, -) { - for (key, node) in nodes { - writeln!(out, "{indent}{}:", yaml_scalar(key)).expect("writing to a string cannot fail"); - let child_indent = format!("{indent} "); - for source in &node.sources { - writeln!(out, "{}- {}", child_indent, yaml_scalar(source)) - .expect("writing to a string cannot fail"); - } - write_no_change_contextual_nodes(out, &child_indent, &node.children); + for source in no_change { + writeln!(out, " - {}", yaml_scalar(source)).expect("writing to a string cannot fail"); } } @@ -1278,7 +1201,7 @@ struct TranslationDictionaryYaml { #[serde(default)] contextual: BTreeMap, #[serde(default)] - no_change: NoChangeYaml, + no_change: BTreeSet, #[serde(default)] target_additions: BTreeMap, #[serde(default)] @@ -1295,12 +1218,10 @@ impl TranslationDictionaryYaml { fn into_translation_dictionary(self) -> Result { let mut contextual = BTreeMap::new(); flatten_contextual_translations(None, self.contextual, &mut contextual)?; - let no_change = self.no_change.into_no_change()?; - Ok(TranslationDictionary { direct: self.direct, contextual, - no_change, + no_change: self.no_change, target_additions: self.target_additions, variables: self.variables, adapter_ids: self.adapter_ids, @@ -1310,57 +1231,6 @@ impl TranslationDictionaryYaml { } } -#[derive(Default, Deserialize)] -#[serde(deny_unknown_fields)] -struct NoChangeYaml { - #[serde(default)] - direct: BTreeSet, - #[serde(default)] - contextual: BTreeMap, -} - -impl NoChangeYaml { - fn into_no_change(self) -> Result { - let mut contextual = BTreeMap::new(); - flatten_no_change_contextual(None, self.contextual, &mut contextual)?; - Ok(TranslationNoChange { - direct: self.direct, - contextual, - }) - } -} - -#[derive(Deserialize)] -#[serde(untagged)] -enum NoChangeContextualYaml { - Sources(BTreeSet), - Nested(BTreeMap), -} - -fn flatten_no_change_contextual( - context_path: Option, - entries: BTreeMap, - contextual: &mut BTreeMap>, -) -> Result<(), CanonicalYamlError> { - for (key, entry) in entries { - match entry { - NoChangeContextualYaml::Sources(sources) => { - let context_path = context_path - .as_ref() - .map_or_else(|| key.clone(), |prefix| format!("{prefix}.{key}")); - contextual.entry(context_path).or_default().extend(sources); - } - NoChangeContextualYaml::Nested(nested) => { - let nested_context = context_path - .as_ref() - .map_or_else(|| key.clone(), |prefix| format!("{prefix}.{key}")); - flatten_no_change_contextual(Some(nested_context), nested, contextual)?; - } - } - } - Ok(()) -} - #[derive(Deserialize)] #[serde(untagged)] enum ContextualTranslationYaml { diff --git a/crates/brain-brew-formats/tests/canonical_yaml.rs b/crates/brain-brew-formats/tests/canonical_yaml.rs index ff0920a..bdf1664 100644 --- a/crates/brain-brew-formats/tests/canonical_yaml.rs +++ b/crates/brain-brew-formats/tests/canonical_yaml.rs @@ -167,33 +167,21 @@ translations: } #[test] -fn translation_dictionary_parses_nested_no_change_entries() { +fn translation_dictionary_parses_no_change_entries() { let overlay = canonical_yaml::overlay_from_str( r#"id: overlay.translation.da kind: translation translations: no_change: - direct: - - Andorra - - Sukhumi - contextual: - notes.note: - djibouti.fields.field.capital: - - Djibouti - deck.description: - - Ultimate Geography + - Andorra + - Sukhumi "#, ) .expect("no-change sections parse"); let translations = overlay.translations.expect("translation dictionary"); - assert!(translations.no_change.direct.contains("Andorra")); - assert!(translations.no_change.direct.contains("Sukhumi")); - assert!( - translations.no_change.contextual["notes.note.djibouti.fields.field.capital"] - .contains("Djibouti") - ); - assert!(translations.no_change.contextual["deck.description"].contains("Ultimate Geography")); + assert!(translations.no_change.contains("Andorra")); + assert!(translations.no_change.contains("Sukhumi")); } #[test] diff --git a/crates/brain-brew-formats/tests/overlay_yaml.rs b/crates/brain-brew-formats/tests/overlay_yaml.rs index 2f2f8ce..c565f80 100644 --- a/crates/brain-brew-formats/tests/overlay_yaml.rs +++ b/crates/brain-brew-formats/tests/overlay_yaml.rs @@ -438,12 +438,8 @@ translations: georgia.fields.field.country: Georgia: Georgien no_change: - contextual: - notes.note: - canada.fields.field.country: - - Canada - direct: - - Andorra + - Canada + - Andorra direct: Germany: Deutschland "#, @@ -453,9 +449,7 @@ translations: assert!(formatted.contains( " notes.note:\n georgia.fields.field.country:\n Georgia: Georgien\n" )); - assert!(formatted.contains( - " no_change:\n direct:\n - Andorra\n contextual:\n notes.note:\n canada.fields.field.country:\n - Canada\n" - )); + assert!(formatted.contains(" no_change:\n - Andorra\n - Canada\n")); assert!( formatted.find(" direct:\n").unwrap() < formatted.find(" contextual:\n").unwrap(), "direct translations are emitted before contextual translations" diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index f5fcefa..d05e1a2 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -12,7 +12,7 @@ Translation dictionaries separate source-keyed translations from target-only tex | --- | --- | --- | | `direct` | reusable translations of exact non-empty source strings | `source text: target text` | | `contextual` | path-scoped translations for a source string inside a deck context | `context path -> source text: target text` | -| `no_change` | translator-reviewed text that intentionally stays identical to the source | `direct: [source text]`, `contextual: context path -> [source text]` | +| `no_change` | translator-reviewed text that intentionally stays identical to the source | `[source text]` | | `target_additions` | target-language text for fields intentionally blank in the source deck | `stable deck path: target text` | Contextual translations win over direct translations. When multiple contextual scopes match, the longest matching context path wins. No-change entries cover missing translation checks but never modify composed output. @@ -77,16 +77,12 @@ translations: direct: Germany: Deutschland no_change: - direct: - - Andorra - - Canada - contextual: - notes.note: - djibouti.fields.field.capital: - - Djibouti + - Andorra + - Canada + - Djibouti ``` -Use `no_change.direct` for reusable names that should stay identical everywhere. Use `no_change.contextual` when an identical string is only intentional under a specific note, field, or other stable deck context. +Use `no_change` for names or phrases that should stay identical everywhere they appear. If a string usually stays unchanged but needs translation in one context, add it to `no_change` and add a `contextual` translation for the exception. If a string usually translates but stays unchanged in one context, use a contextual translation whose target equals the source. Keep `ignore_paths` for structural paths that translators should not review at all, such as tags, flag/map HTML, or adapter metadata. Use `no_change` for translator-facing text that was reviewed and intentionally left as-is. @@ -175,7 +171,7 @@ To seed translator work after adding English notes or fields, run apply explicit brainbrew translations --manifest brainbrew.yaml --target de-standard --apply ``` -Non-interactive `--apply` preserves the existing scriptable behavior: it inserts deterministic `source: source` translation stubs into `translations.direct` for the missing text fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose one action for all selected rows — mark direct no-change, mark contextual no-change at the suggested safe context, add direct `source: source`, add contextual `source: source`, add `ignore_paths`, skip — or choose to decide per row. It keeps reviewed no-change decisions distinct from real translations and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. +Non-interactive `--apply` preserves the existing scriptable behavior: it inserts deterministic `source: source` translation stubs into `translations.direct` for the missing text fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose one action for all selected rows — mark `no_change`, add direct `source: source`, add contextual `source: source`, add `ignore_paths`, skip — or choose to decide per row. It keeps reviewed no-change decisions distinct from real translations and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. When `require_complete: true`, composition fails if any extracted non-empty translatable string is not translated by `direct`, translated by a matching `contextual` entry, marked by `no_change`, or matched by `ignore_paths`. For release workflows, prefer target-level verification policy in `brainbrew.yaml`: @@ -191,8 +187,8 @@ targets: Translator context views should present extracted strings in the same categories: -- source strings that occur once or can be safely reused are candidates for `direct` or `no_change.direct`; -- repeated source strings should show their stable deck contexts so translators can choose a reusable `direct` translation, `no_change.direct`, `contextual` entries, contextual no-change entries, or both; +- source strings that occur once or can be safely reused are candidates for `direct` or `no_change`; +- repeated source strings should show their stable deck contexts so translators can choose a reusable `direct` translation, reusable `no_change`, contextual entries, or both; - blank source fields should be shown as target-language addition opportunities and written to `target_additions` only when the blank text belongs to the translation overlay. During sync/apply, stale source-key errors mean the translator should refresh against the current source deck before editing the target text. Missing direct/contextual translation errors indicate an untranslated extracted string. Invalid contextual errors indicate either a stale source key or an invalid context path. Invalid target-addition errors indicate the source is no longer blank. diff --git a/documentation/docs/reference/decisions/0008-use-source-variables-and-translation-dictionaries.md b/documentation/docs/reference/decisions/0008-use-source-variables-and-translation-dictionaries.md index c9ca553..13bafdb 100644 --- a/documentation/docs/reference/decisions/0008-use-source-variables-and-translation-dictionaries.md +++ b/documentation/docs/reference/decisions/0008-use-source-variables-and-translation-dictionaries.md @@ -12,7 +12,7 @@ Translation overlays become hard to review when a small phrase change requires r Canonical Deck entities may define source variables, and translation overlays may use translation dictionaries. -Text can reference variables with `${variable.name}`. Variables are resolved from the most specific scope to the broadest scope: card template, note, note type, then deck. Translation dictionaries map direct reusable source strings, contextual source-string translations scoped by stable deck path, explicit no-change decisions for reviewed text that intentionally stays identical, variable values, target-language additions for blank fields, and adapter IDs to translated values. The `direct`, `contextual`, and `no_change` source keys act as expected bases; `target_additions` requires the source field to remain blank. +Text can reference variables with `${variable.name}`. Variables are resolved from the most specific scope to the broadest scope: card template, note, note type, then deck. Translation dictionaries map direct reusable source strings, contextual source-string translations scoped by stable deck path, explicit no-change decisions for reviewed text that intentionally stays identical, variable values, target-language additions for blank fields, and adapter IDs to translated values. The `direct`/`contextual` source keys and `no_change` source entries act as expected bases; `target_additions` requires the source field to remain blank. CrowdAnki and other adapter exports render variables before writing output, so distributable decks contain ordinary adapter-compatible text and HTML. @@ -31,7 +31,7 @@ CrowdAnki and other adapter exports render variables before writing output, so d - Variable scoping adds one more concept to Canonical Deck source. - Translation extraction and coverage checks must be deterministic. -- Ambiguous source strings need `contextual` dictionary entries or contextual no-change entries. +- Ambiguous source strings need `contextual` dictionary entries; no-change remains reusable exact-source coverage. ## Alternatives Considered diff --git a/documentation/docs/reference/yaml.md b/documentation/docs/reference/yaml.md index b76dd5a..bb2819a 100644 --- a/documentation/docs/reference/yaml.md +++ b/documentation/docs/reference/yaml.md @@ -69,11 +69,8 @@ translations: us-georgia.fields.field.region: Georgia: Georgia no_change: - direct: - - Andorra - contextual: - notes.note.djibouti.fields.field.capital: - - Djibouti + - Andorra + - Djibouti target_additions: notes.note.example.fields.field.country-info: Localized blank text. variables: diff --git a/fixtures/ultimate-geography/overlays/languages/cs.yaml b/fixtures/ultimate-geography/overlays/languages/cs.yaml index 6bdde81..131fe21 100644 --- a/fixtures/ultimate-geography/overlays/languages/cs.yaml +++ b/fixtures/ultimate-geography/overlays/languages/cs.yaml @@ -490,208 +490,207 @@ translations: wallis-and-futuna: Overseas territory of France.: 'Zámořské území Francie.' no_change: - direct: - - Abuja - - Accra - - Alofi - - Amsterdam - - Andorra - - Andorra la Vella - - Angola - - Anguilla - - Ankara - - Antananarivo - - Apia - - Argentina - - Aruba - - Asmara - - Astana - - 'Asunción' - - Avarua - - Baku - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Bangui - - Banjul - - Barbados - - Basseterre - - Belfast - - Belize - - Belmopan - - Benin - - Bermuda - - Bern - - Bissau - - 'Bogotá' - - Botswana - - Bougainville - - 'Brasília' - - Bratislava - - Brazzaville - - Bridgetown - - Buenos Aires - - Burkina Faso - - Burundi - - Canberra - - Caracas - - Cardiff - - Castries - - Charlotte Amalie - - Chile - - Ciudad de la Paz - - Conakry - - 'Curaçao' - - Dakar - - Dili - - Dodoma - - Dublin - - Edinburgh - - Egypt - - Eritrea - - Freetown - - Funafuti - - Gabon - - Gaborone - - Georgetown - - Ghana - - Gibraltar - - Gitega - - Grenada - - Guadeloupe - - Guam - - Guatemala - - Guernsey - - Guinea - - Guinea-Bissau - - Guyana - - 'Hagåtña' - - Haiti - - Harare - - Havana - - Honduras - - Honiara - - Jakarta - - Jersey - - Kampala - - Kigali - - Kingston - - Kingstown - - Kinshasa - - Kiribati - - Kosovo - - Kuala Lumpur - - Laos - - Lesotho - - Libreville - - Lilongwe - - Lima - - 'Lomé' - - Luanda - - Lusaka - - Madagascar - - Madeira - - Madrid - - Majuro - - Malawi - - Mali - - Malta - - Managua - - Manila - - Maputo - - Mariehamn - - Maseru - - Mayotte - - Mbabane - - Minsk - - Monrovia - - Montevideo - - Moroni - - Myanmar - - 'N''Djamena' - - Nairobi - - Nassau - - Nauru - - Ngerulmud - - Niamey - - Nicaragua - - Niger - - Niue - - 'Nouméa' - - 'Nukuʻalofa' - - Nuuk - - Oranjestad - - Oslo - - Ottawa - - Ouagadougou - - Palau - - Palikir - - Panama - - Papeete - - Paraguay - - Paramaribo - - Peru - - Podgorica - - Port Louis - - Port Moresby - - Port Vila - - Port of Spain - - Port-au-Prince - - Praia - - Quito - - Rabat - - 'Reykjavík' - - Riga - - Roseau - - Rwanda - - 'Réunion' - - Samoa - - 'San José' - - San Juan - - San Marino - - San Salvador - - Santo Domingo - - Sarajevo - - Senegal - - Sierra Leone - - Skopje - - Somaliland - - Stockholm - - Sucre - - Sumatra - - Suva - - 'São Tomé' - - Tallinn - - Tbilisi - - Tegucigalpa - - Tirana - - Tiraspol - - Togo - - Tonga - - Tunis - - Tuvalu - - 'Tórshavn' - - Uganda - - Uruguay - - Vaduz - - Valletta - - Vanuatu - - Venezuela - - Victoria - - Vientiane - - Vietnam - - Vilnius - - Wales - - Washington, D.C. - - Wellington - - Willemstad - - Windhoek - - Yamoussoukro - - 'Yaoundé' - - Yaren - - Zanzibar - - Zimbabwe + - Abuja + - Accra + - Alofi + - Amsterdam + - Andorra + - Andorra la Vella + - Angola + - Anguilla + - Ankara + - Antananarivo + - Apia + - Argentina + - Aruba + - Asmara + - Astana + - 'Asunción' + - Avarua + - Baku + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangui + - Banjul + - Barbados + - Basseterre + - Belfast + - Belize + - Belmopan + - Benin + - Bermuda + - Bern + - Bissau + - 'Bogotá' + - Botswana + - Bougainville + - 'Brasília' + - Bratislava + - Brazzaville + - Bridgetown + - Buenos Aires + - Burkina Faso + - Burundi + - Canberra + - Caracas + - Cardiff + - Castries + - Charlotte Amalie + - Chile + - Ciudad de la Paz + - Conakry + - 'Curaçao' + - Dakar + - Dili + - Dodoma + - Dublin + - Edinburgh + - Egypt + - Eritrea + - Freetown + - Funafuti + - Gabon + - Gaborone + - Georgetown + - Ghana + - Gibraltar + - Gitega + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-Bissau + - Guyana + - 'Hagåtña' + - Haiti + - Harare + - Havana + - Honduras + - Honiara + - Jakarta + - Jersey + - Kampala + - Kigali + - Kingston + - Kingstown + - Kinshasa + - Kiribati + - Kosovo + - Kuala Lumpur + - Laos + - Lesotho + - Libreville + - Lilongwe + - Lima + - 'Lomé' + - Luanda + - Lusaka + - Madagascar + - Madeira + - Madrid + - Majuro + - Malawi + - Mali + - Malta + - Managua + - Manila + - Maputo + - Mariehamn + - Maseru + - Mayotte + - Mbabane + - Minsk + - Monrovia + - Montevideo + - Moroni + - Myanmar + - 'N''Djamena' + - Nairobi + - Nassau + - Nauru + - Ngerulmud + - Niamey + - Nicaragua + - Niger + - Niue + - 'Nouméa' + - 'Nukuʻalofa' + - Nuuk + - Oranjestad + - Oslo + - Ottawa + - Ouagadougou + - Palau + - Palikir + - Panama + - Papeete + - Paraguay + - Paramaribo + - Peru + - Podgorica + - Port Louis + - Port Moresby + - Port Vila + - Port of Spain + - Port-au-Prince + - Praia + - Quito + - Rabat + - 'Reykjavík' + - Riga + - Roseau + - Rwanda + - 'Réunion' + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Santo Domingo + - Sarajevo + - Senegal + - Sierra Leone + - Skopje + - Somaliland + - Stockholm + - Sucre + - Sumatra + - Suva + - 'São Tomé' + - Tallinn + - Tbilisi + - Tegucigalpa + - Tirana + - Tiraspol + - Togo + - Tonga + - Tunis + - Tuvalu + - 'Tórshavn' + - Uganda + - Uruguay + - Vaduz + - Valletta + - Vanuatu + - Venezuela + - Victoria + - Vientiane + - Vietnam + - Vilnius + - Wales + - Washington, D.C. + - Wellington + - Willemstad + - Windhoek + - Yamoussoukro + - 'Yaoundé' + - Yaren + - Zanzibar + - Zimbabwe target_additions: notes.note.pacific-ocean.fields.field.country-info: 'Známý též jako Pacifik.' notes.note.united-kingdom.fields.field.country-info: 'Známé též jako Velká Británie.' diff --git a/fixtures/ultimate-geography/overlays/languages/da.yaml b/fixtures/ultimate-geography/overlays/languages/da.yaml index 497842f..b02924d 100644 --- a/fixtures/ultimate-geography/overlays/languages/da.yaml +++ b/fixtures/ultimate-geography/overlays/languages/da.yaml @@ -373,308 +373,307 @@ translations: wallis-and-futuna: Overseas territory of France.: 'Fransk oversøisk departement.' no_change: - direct: - - Abu Dhabi - - Abuja - - Accra - - Afghanistan - - Alaska - - Alofi - - Amman - - Amsterdam - - Andorra - - Andorra la Vella - - Angola - - Anguilla - - Ankara - - Antananarivo - - Apia - - Argentina - - Aruba - - Asmara - - Astana - - 'Asunción' - - Avarua - - Bahrain - - Baku - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Bangladesh - - Bangui - - Banjul - - Barbados - - Basseterre - - Beijing - - Beirut - - Belarus - - Belfast - - Belize - - Belmopan - - Benin - - Berlin - - Bermuda - - Bern - - Bhutan - - Bissau - - 'Bogotá' - - Bolivia - - Botswana - - Bougainville - - Bratislava - - Brazzaville - - Bridgetown - - Brunei - - Budapest - - Buenos Aires - - Burkina Faso - - Burundi - - Cairo - - Canada - - Canberra - - Caracas - - Cardiff - - Castries - - Charlotte Amalie - - Chile - - Ciudad de la Paz - - Colombia - - Conakry - - Costa Rica - - Cuba - - 'Curaçao' - - Dakar - - Dhaka - - Dili - - Djibouti - - Dodoma - - Doha - - Dominica - - Dublin - - Ecuador - - Edinburgh - - El Salvador - - England - - Eritrea - - Eswatini - - Fiji - - Finland - - Freetown - - Funafuti - - Gabon - - Gaborone - - Georgetown - - Ghana - - Gibraltar - - Gitega - - Grenada - - Guadeloupe - - Guam - - Guatemala - - Guatemala City - - Guernsey - - Guinea - - Guinea-Bissau - - Guyana - - Haiti - - Hanoi - - Harare - - Hargeisa - - Hawaii - - Helsinki - - Honduras - - Honiara - - Iran - - Islamabad - - Isle of Man - - Israel - - Jamaica - - Japan - - Java - - Jersey - - Jerusalem - - Jordan - - Juba - - Kabul - - Kaliningrad Oblast - - Kampala - - Kenya - - Khartoum - - Kigali - - Kingston - - Kingstown - - Kinshasa - - Kiribati - - Kosovo - - Kuala Lumpur - - Kuwait - - Kyiv - - Laos - - Lesotho - - Liberia - - Libreville - - Liechtenstein - - Lilongwe - - Lima - - Ljubljana - - 'Lomé' - - London - - Luanda - - Lusaka - - Luxembourg - - Madeira - - Madrid - - Majuro - - Malawi - - Malaysia - - Mali - - Malta - - 'Malé' - - Managua - - Manama - - Manila - - Maputo - - Mariehamn - - Martinique - - Maseru - - Mauritius - - Mayotte - - Mbabane - - Mexico - - Mexico City - - Minsk - - Mogadishu - - Moldova - - Monaco - - Monrovia - - Montenegro - - Montevideo - - Moroni - - Mozambique - - Muscat - - Myanmar - - Nairobi - - Namibia - - Nassau - - Nauru - - Naypyidaw - - Nepal - - New Delhi - - New Zealand - - Ngerulmud - - Niamey - - Nicaragua - - Nicosia - - Niger - - Nigeria - - Niue - - Nouakchott - - 'Nouméa' - - Nuuk - - Oman - - Oranjestad - - Oslo - - Ottawa - - Ouagadougou - - Pakistan - - Palau - - Palikir - - Panama - - Panama City - - Papeete - - Paraguay - - Paramaribo - - Paris - - Peru - - Phnom Penh - - Podgorica - - Port Louis - - Port Moresby - - Port Vila - - Port of Spain - - Port-au-Prince - - Portugal - - Praia - - Pretoria, Cape Town, Bloemfontein - - Pristina - - Puerto Rico - - Pyongyang - - Qatar - - Quito - - Rabat - - 'Reykjavík' - - Riga - - Riyadh - - Roseau - - Rwanda - - 'Réunion' - - Saint Lucia - - Samoa - - 'San José' - - San Juan - - San Marino - - San Salvador - - Santiago - - Santo Domingo - - Sarajevo - - Senegal - - Seoul - - Sierra Leone - - Singapore - - Skopje - - Sofia - - Somalia - - Somaliland - - Sri Lanka - - Stockholm - - Sucre - - Sudan - - Sukhumi - - Sumatra - - Suva - - Svalbard - - 'São Tomé' - - Taipei - - Taiwan - - Tallinn - - Tanzania - - Tbilisi - - Tegucigalpa - - Thailand - - Tirana - - Tiraspol - - Togo - - Tokyo - - Tonga - - Tripoli - - Tskhinvali - - Tunis - - Turkmenistan - - Tuvalu - - Uganda - - Ukraine - - Uruguay - - Vaduz - - Valletta - - Vanuatu - - Venezuela - - Victoria - - Vientiane - - Vietnam - - Vilnius - - Wales - - Wellington - - Willemstad - - Windhoek - - Yamoussoukro - - 'Yaoundé' - - Yaren - - Yemen - - Zagreb - - Zambia - - Zanzibar - - Zimbabwe + - Abu Dhabi + - Abuja + - Accra + - Afghanistan + - Alaska + - Alofi + - Amman + - Amsterdam + - Andorra + - Andorra la Vella + - Angola + - Anguilla + - Ankara + - Antananarivo + - Apia + - Argentina + - Aruba + - Asmara + - Astana + - 'Asunción' + - Avarua + - Bahrain + - Baku + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangladesh + - Bangui + - Banjul + - Barbados + - Basseterre + - Beijing + - Beirut + - Belarus + - Belfast + - Belize + - Belmopan + - Benin + - Berlin + - Bermuda + - Bern + - Bhutan + - Bissau + - 'Bogotá' + - Bolivia + - Botswana + - Bougainville + - Bratislava + - Brazzaville + - Bridgetown + - Brunei + - Budapest + - Buenos Aires + - Burkina Faso + - Burundi + - Cairo + - Canada + - Canberra + - Caracas + - Cardiff + - Castries + - Charlotte Amalie + - Chile + - Ciudad de la Paz + - Colombia + - Conakry + - Costa Rica + - Cuba + - 'Curaçao' + - Dakar + - Dhaka + - Dili + - Djibouti + - Dodoma + - Doha + - Dominica + - Dublin + - Ecuador + - Edinburgh + - El Salvador + - England + - Eritrea + - Eswatini + - Fiji + - Finland + - Freetown + - Funafuti + - Gabon + - Gaborone + - Georgetown + - Ghana + - Gibraltar + - Gitega + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guatemala City + - Guernsey + - Guinea + - Guinea-Bissau + - Guyana + - Haiti + - Hanoi + - Harare + - Hargeisa + - Hawaii + - Helsinki + - Honduras + - Honiara + - Iran + - Islamabad + - Isle of Man + - Israel + - Jamaica + - Japan + - Java + - Jersey + - Jerusalem + - Jordan + - Juba + - Kabul + - Kaliningrad Oblast + - Kampala + - Kenya + - Khartoum + - Kigali + - Kingston + - Kingstown + - Kinshasa + - Kiribati + - Kosovo + - Kuala Lumpur + - Kuwait + - Kyiv + - Laos + - Lesotho + - Liberia + - Libreville + - Liechtenstein + - Lilongwe + - Lima + - Ljubljana + - 'Lomé' + - London + - Luanda + - Lusaka + - Luxembourg + - Madeira + - Madrid + - Majuro + - Malawi + - Malaysia + - Mali + - Malta + - 'Malé' + - Managua + - Manama + - Manila + - Maputo + - Mariehamn + - Martinique + - Maseru + - Mauritius + - Mayotte + - Mbabane + - Mexico + - Mexico City + - Minsk + - Mogadishu + - Moldova + - Monaco + - Monrovia + - Montenegro + - Montevideo + - Moroni + - Mozambique + - Muscat + - Myanmar + - Nairobi + - Namibia + - Nassau + - Nauru + - Naypyidaw + - Nepal + - New Delhi + - New Zealand + - Ngerulmud + - Niamey + - Nicaragua + - Nicosia + - Niger + - Nigeria + - Niue + - Nouakchott + - 'Nouméa' + - Nuuk + - Oman + - Oranjestad + - Oslo + - Ottawa + - Ouagadougou + - Pakistan + - Palau + - Palikir + - Panama + - Panama City + - Papeete + - Paraguay + - Paramaribo + - Paris + - Peru + - Phnom Penh + - Podgorica + - Port Louis + - Port Moresby + - Port Vila + - Port of Spain + - Port-au-Prince + - Portugal + - Praia + - Pretoria, Cape Town, Bloemfontein + - Pristina + - Puerto Rico + - Pyongyang + - Qatar + - Quito + - Rabat + - 'Reykjavík' + - Riga + - Riyadh + - Roseau + - Rwanda + - 'Réunion' + - Saint Lucia + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Santiago + - Santo Domingo + - Sarajevo + - Senegal + - Seoul + - Sierra Leone + - Singapore + - Skopje + - Sofia + - Somalia + - Somaliland + - Sri Lanka + - Stockholm + - Sucre + - Sudan + - Sukhumi + - Sumatra + - Suva + - Svalbard + - 'São Tomé' + - Taipei + - Taiwan + - Tallinn + - Tanzania + - Tbilisi + - Tegucigalpa + - Thailand + - Tirana + - Tiraspol + - Togo + - Tokyo + - Tonga + - Tripoli + - Tskhinvali + - Tunis + - Turkmenistan + - Tuvalu + - Uganda + - Ukraine + - Uruguay + - Vaduz + - Valletta + - Vanuatu + - Venezuela + - Victoria + - Vientiane + - Vietnam + - Vilnius + - Wales + - Wellington + - Willemstad + - Windhoek + - Yamoussoukro + - 'Yaoundé' + - Yaren + - Yemen + - Zagreb + - Zambia + - Zanzibar + - Zimbabwe target_additions: notes.note.arctic-ocean.fields.field.country-info: 'Også kendt som Det Arktiske Ocean.' notes.note.balkan-peninsula.fields.field.country-info: 'Også kendt som Balkan.' diff --git a/fixtures/ultimate-geography/overlays/languages/de.yaml b/fixtures/ultimate-geography/overlays/languages/de.yaml index b232a9c..294ca1d 100644 --- a/fixtures/ultimate-geography/overlays/languages/de.yaml +++ b/fixtures/ultimate-geography/overlays/languages/de.yaml @@ -403,273 +403,272 @@ translations: turks-and-caicos-islands: Overseas territory of the United Kingdom.: 'Auch Turks & Caicos. Britisches Überseegebiet.' no_change: - direct: - - Abu Dhabi - - Abuja - - Accra - - Afghanistan - - Alaska - - Alofi - - Amman - - Amsterdam - - Andorra - - Andorra la Vella - - Angola - - Anguilla - - Ankara - - Antananarivo - - Apia - - Aruba - - Asmara - - Astana - - 'Asunción' - - Avarua - - Bahrain - - Baku - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Bangui - - Banjul - - Barbados - - Basseterre - - Beirut - - Belarus - - Belfast - - Belize - - Belmopan - - Benin - - Berlin - - Bermuda - - Bern - - Bhutan - - Bissau - - 'Bogotá' - - Botswana - - Bougainville - - 'Brasília' - - Bratislava - - Brazzaville - - Bridgetown - - Brunei - - Budapest - - Buenos Aires - - Burkina Faso - - Burundi - - Canberra - - Caracas - - Cardiff - - Castries - - Charlotte Amalie - - Chile - - 'Chișinău' - - Ciudad de la Paz - - Conakry - - Costa Rica - - 'Curaçao' - - Dakar - - Dhaka - - Dili - - Dodoma - - Doha - - Dominica - - Dublin - - Ecuador - - Edinburgh - - El Salvador - - England - - Eritrea - - Eswatini - - Freetown - - Funafuti - - Gaborone - - Georgetown - - Ghana - - Gibraltar - - Gitega - - Grenada - - Guadeloupe - - Guam - - Guatemala - - Guernsey - - Guinea - - Guinea-Bissau - - Guyana - - 'Hagåtña' - - Haiti - - Hanoi - - Harare - - Hawaii - - Helsinki - - Honduras - - Honiara - - Hudson Bay - - Iran - - Islamabad - - Isle of Man - - Israel - - Jakarta - - Japan - - Java - - Jersey - - Jerusalem - - Juba - - Kabul - - Kampala - - Kathmandu - - Kigali - - Kingston - - Kingstown - - Kinshasa - - Kiribati - - Kosovo - - Kuala Lumpur - - Kuwait - - Laos - - Lesotho - - Liberia - - Libreville - - Liechtenstein - - Lilongwe - - Lima - - Ljubljana - - 'Lomé' - - London - - Luanda - - Lusaka - - Macau - - Madrid - - Majuro - - Malawi - - Malaysia - - Mali - - Malta - - 'Malé' - - Managua - - Manama - - Manila - - Maputo - - Mariehamn - - Martinique - - Maseru - - Mauritius - - Mayotte - - Mbabane - - Minsk - - Monaco - - Monrovia - - Montenegro - - Montevideo - - Moroni - - Myanmar - - Nairobi - - Namibia - - Nassau - - Nauru - - Naypyidaw - - Nepal - - Ngerulmud - - Niamey - - Nicaragua - - Niger - - Nigeria - - Niue - - Nouakchott - - 'Nouméa' - - 'Nukuʻalofa' - - Nuuk - - Oman - - Oranjestad - - Oslo - - Ottawa - - Ouagadougou - - Pakistan - - Palau - - Palikir - - Panama - - Papeete - - Paraguay - - Paramaribo - - Paris - - Peru - - Phnom Penh - - Podgorica - - Port Louis - - Port Moresby - - Port Vila - - Port of Spain - - Port-au-Prince - - Porto-Novo - - Portugal - - Praia - - Pristina - - Puerto Rico - - Quito - - Rabat - - 'Reykjavík' - - Riga - - Roseau - - Samoa - - 'San José' - - San Juan - - San Marino - - San Salvador - - Sanaa - - Santo Domingo - - Sarajevo - - Senegal - - Seoul - - Sierra Leone - - Sint Maarten - - Skopje - - Sofia - - Somalia - - Somaliland - - South Tarawa - - Sri Lanka - - 'St. George''s' - - Stockholm - - Sucre - - Sudan - - Sumatra - - Suriname - - Suva - - 'São Tomé' - - Taiwan - - Tallinn - - Tegucigalpa - - Thailand - - Thimphu - - Tirana - - Tiraspol - - Togo - - Tonga - - Tunis - - Turkmenistan - - Tuvalu - - 'Tórshavn' - - Uganda - - Ukraine - - Ulaanbaatar - - Uruguay - - Vaduz - - Valletta - - Vanuatu - - Venezuela - - Victoria - - Vientiane - - Vietnam - - Vilnius - - Wales - - Washington, D.C. - - Wellington - - Willemstad - - Windhoek - - Yamoussoukro - - 'Yaoundé' - - Yaren - - Zagreb + - Abu Dhabi + - Abuja + - Accra + - Afghanistan + - Alaska + - Alofi + - Amman + - Amsterdam + - Andorra + - Andorra la Vella + - Angola + - Anguilla + - Ankara + - Antananarivo + - Apia + - Aruba + - Asmara + - Astana + - 'Asunción' + - Avarua + - Bahrain + - Baku + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangui + - Banjul + - Barbados + - Basseterre + - Beirut + - Belarus + - Belfast + - Belize + - Belmopan + - Benin + - Berlin + - Bermuda + - Bern + - Bhutan + - Bissau + - 'Bogotá' + - Botswana + - Bougainville + - 'Brasília' + - Bratislava + - Brazzaville + - Bridgetown + - Brunei + - Budapest + - Buenos Aires + - Burkina Faso + - Burundi + - Canberra + - Caracas + - Cardiff + - Castries + - Charlotte Amalie + - Chile + - 'Chișinău' + - Ciudad de la Paz + - Conakry + - Costa Rica + - 'Curaçao' + - Dakar + - Dhaka + - Dili + - Dodoma + - Doha + - Dominica + - Dublin + - Ecuador + - Edinburgh + - El Salvador + - England + - Eritrea + - Eswatini + - Freetown + - Funafuti + - Gaborone + - Georgetown + - Ghana + - Gibraltar + - Gitega + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-Bissau + - Guyana + - 'Hagåtña' + - Haiti + - Hanoi + - Harare + - Hawaii + - Helsinki + - Honduras + - Honiara + - Hudson Bay + - Iran + - Islamabad + - Isle of Man + - Israel + - Jakarta + - Japan + - Java + - Jersey + - Jerusalem + - Juba + - Kabul + - Kampala + - Kathmandu + - Kigali + - Kingston + - Kingstown + - Kinshasa + - Kiribati + - Kosovo + - Kuala Lumpur + - Kuwait + - Laos + - Lesotho + - Liberia + - Libreville + - Liechtenstein + - Lilongwe + - Lima + - Ljubljana + - 'Lomé' + - London + - Luanda + - Lusaka + - Macau + - Madrid + - Majuro + - Malawi + - Malaysia + - Mali + - Malta + - 'Malé' + - Managua + - Manama + - Manila + - Maputo + - Mariehamn + - Martinique + - Maseru + - Mauritius + - Mayotte + - Mbabane + - Minsk + - Monaco + - Monrovia + - Montenegro + - Montevideo + - Moroni + - Myanmar + - Nairobi + - Namibia + - Nassau + - Nauru + - Naypyidaw + - Nepal + - Ngerulmud + - Niamey + - Nicaragua + - Niger + - Nigeria + - Niue + - Nouakchott + - 'Nouméa' + - 'Nukuʻalofa' + - Nuuk + - Oman + - Oranjestad + - Oslo + - Ottawa + - Ouagadougou + - Pakistan + - Palau + - Palikir + - Panama + - Papeete + - Paraguay + - Paramaribo + - Paris + - Peru + - Phnom Penh + - Podgorica + - Port Louis + - Port Moresby + - Port Vila + - Port of Spain + - Port-au-Prince + - Porto-Novo + - Portugal + - Praia + - Pristina + - Puerto Rico + - Quito + - Rabat + - 'Reykjavík' + - Riga + - Roseau + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Sanaa + - Santo Domingo + - Sarajevo + - Senegal + - Seoul + - Sierra Leone + - Sint Maarten + - Skopje + - Sofia + - Somalia + - Somaliland + - South Tarawa + - Sri Lanka + - 'St. George''s' + - Stockholm + - Sucre + - Sudan + - Sumatra + - Suriname + - Suva + - 'São Tomé' + - Taiwan + - Tallinn + - Tegucigalpa + - Thailand + - Thimphu + - Tirana + - Tiraspol + - Togo + - Tonga + - Tunis + - Turkmenistan + - Tuvalu + - 'Tórshavn' + - Uganda + - Ukraine + - Ulaanbaatar + - Uruguay + - Vaduz + - Valletta + - Vanuatu + - Venezuela + - Victoria + - Vientiane + - Vietnam + - Vilnius + - Wales + - Washington, D.C. + - Wellington + - Willemstad + - Windhoek + - Yamoussoukro + - 'Yaoundé' + - Yaren + - Zagreb target_additions: notes.note.aegean-sea.fields.field.country-info: 'Auch Ägäis.' notes.note.arctic-ocean.fields.field.country-info: 'Auch Nordpolarmeer, nördliches Eismeer, Arktische See oder Arktik.' diff --git a/fixtures/ultimate-geography/overlays/languages/es.yaml b/fixtures/ultimate-geography/overlays/languages/es.yaml index 1e5f74a..127d458 100644 --- a/fixtures/ultimate-geography/overlays/languages/es.yaml +++ b/fixtures/ultimate-geography/overlays/languages/es.yaml @@ -453,201 +453,200 @@ translations: Zimbabwe: Zimbabue 'Åland Islands': 'Åland' no_change: - direct: - - Alaska - - Albania - - Alofi - - Andorra - - Angola - - Ankara - - Antananarivo - - Apia - - Argentina - - Armenia - - Aruba - - Asia - - Asmara - - 'Asunción' - - Australia - - Austria - - Avarua - - Azores - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Bangui - - Banjul - - Barbados - - Basseterre - - Beirut - - Belfast - - 'Bogotá' - - Bolivia - - Bougainville - - Bratislava - - Brazzaville - - Bridgetown - - Budapest - - Buenos Aires - - Bulgaria - - Burkina Faso - - Burundi - - Canberra - - Caracas - - Cardiff - - Castries - - Chad - - Charlotte Amalie - - Chile - - China - - Ciudad de la Paz - - Colombia - - Costa Rica - - Cuba - - Dakar - - Dili - - Dodoma - - Doha - - Dominica - - Ecuador - - El Salvador - - Eritrea - - Estonia - - Freetown - - Funafuti - - Gaborone - - Georgetown - - Georgia - - Ghana - - Gibraltar - - Guam - - Guatemala - - Guernsey - - Guinea - - Guyana - - Harare - - Hargeisa - - Helsinki - - Honduras - - Hong Kong - - Honiara - - India - - Indonesia - - Islamabad - - Israel - - Jamaica - - Java - - Jeju - - Jersey - - Kabul - - Kampala - - Kigali - - Kingston - - Kingstown - - Kiribati - - Kosovo - - Kuala Lumpur - - Kuwait - - Laos - - Liberia - - Libreville - - Liechtenstein - - Lima - - 'Lomé' - - Luanda - - Lusaka - - Madagascar - - Madeira - - Madrid - - Majuro - - Malta - - 'Malé' - - Managua - - Manama - - Manila - - Maputo - - Mariehamn - - Maseru - - Mauritania - - Mayotte - - Mbabane - - Melanesia - - Micronesia - - Minsk - - Mongolia - - Monrovia - - Montenegro - - Montevideo - - Moroni - - Mozambique - - Nairobi - - Namibia - - Nauru - - Nepal - - Ngerulmud - - Niamey - - Nicaragua - - Nicosia - - Nigeria - - Niue - - Nuuk - - Oranjestad - - Oslo - - Ottawa - - Palikir - - Papeete - - Paraguay - - Paramaribo - - Podgorica - - Port Louis - - Port Vila - - Portugal - - Praia - - Pristina - - Puerto Rico - - Quito - - Rabat - - Riga - - Roseau - - Samoa - - 'San José' - - San Juan - - San Marino - - San Salvador - - Santo Domingo - - Sarajevo - - Senegal - - Serbia - - Seychelles - - Somalia - - Sri Lanka - - Sucre - - Sumatra - - Suva - - Svalbard - - Tanzania - - Tegucigalpa - - Tirana - - Togo - - Tonga - - Transnistria - - Tuvalu - - 'Tórshavn' - - Uganda - - Uruguay - - Vaduz - - Vanuatu - - Venezuela - - Victoria - - Vietnam - - Wellington - - Willemstad - - Windhoek - - Yaren - - Yemen - - Zagreb - - Zambia + - Alaska + - Albania + - Alofi + - Andorra + - Angola + - Ankara + - Antananarivo + - Apia + - Argentina + - Armenia + - Aruba + - Asia + - Asmara + - 'Asunción' + - Australia + - Austria + - Avarua + - Azores + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangui + - Banjul + - Barbados + - Basseterre + - Beirut + - Belfast + - 'Bogotá' + - Bolivia + - Bougainville + - Bratislava + - Brazzaville + - Bridgetown + - Budapest + - Buenos Aires + - Bulgaria + - Burkina Faso + - Burundi + - Canberra + - Caracas + - Cardiff + - Castries + - Chad + - Charlotte Amalie + - Chile + - China + - Ciudad de la Paz + - Colombia + - Costa Rica + - Cuba + - Dakar + - Dili + - Dodoma + - Doha + - Dominica + - Ecuador + - El Salvador + - Eritrea + - Estonia + - Freetown + - Funafuti + - Gaborone + - Georgetown + - Georgia + - Ghana + - Gibraltar + - Guam + - Guatemala + - Guernsey + - Guinea + - Guyana + - Harare + - Hargeisa + - Helsinki + - Honduras + - Hong Kong + - Honiara + - India + - Indonesia + - Islamabad + - Israel + - Jamaica + - Java + - Jeju + - Jersey + - Kabul + - Kampala + - Kigali + - Kingston + - Kingstown + - Kiribati + - Kosovo + - Kuala Lumpur + - Kuwait + - Laos + - Liberia + - Libreville + - Liechtenstein + - Lima + - 'Lomé' + - Luanda + - Lusaka + - Madagascar + - Madeira + - Madrid + - Majuro + - Malta + - 'Malé' + - Managua + - Manama + - Manila + - Maputo + - Mariehamn + - Maseru + - Mauritania + - Mayotte + - Mbabane + - Melanesia + - Micronesia + - Minsk + - Mongolia + - Monrovia + - Montenegro + - Montevideo + - Moroni + - Mozambique + - Nairobi + - Namibia + - Nauru + - Nepal + - Ngerulmud + - Niamey + - Nicaragua + - Nicosia + - Nigeria + - Niue + - Nuuk + - Oranjestad + - Oslo + - Ottawa + - Palikir + - Papeete + - Paraguay + - Paramaribo + - Podgorica + - Port Louis + - Port Vila + - Portugal + - Praia + - Pristina + - Puerto Rico + - Quito + - Rabat + - Riga + - Roseau + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Santo Domingo + - Sarajevo + - Senegal + - Serbia + - Seychelles + - Somalia + - Sri Lanka + - Sucre + - Sumatra + - Suva + - Svalbard + - Tanzania + - Tegucigalpa + - Tirana + - Togo + - Tonga + - Transnistria + - Tuvalu + - 'Tórshavn' + - Uganda + - Uruguay + - Vaduz + - Vanuatu + - Venezuela + - Victoria + - Vietnam + - Wellington + - Willemstad + - Windhoek + - Yaren + - Yemen + - Zagreb + - Zambia target_additions: notes.note.celebes-sea.fields.field.country-info: 'También conocido como mar de Sulawesi.' variables: diff --git a/fixtures/ultimate-geography/overlays/languages/fr.yaml b/fixtures/ultimate-geography/overlays/languages/fr.yaml index 1cf048d..7c7f7d0 100644 --- a/fixtures/ultimate-geography/overlays/languages/fr.yaml +++ b/fixtures/ultimate-geography/overlays/languages/fr.yaml @@ -436,232 +436,231 @@ translations: wallis-and-futuna: Overseas territory of France.: 'Collectivité française d''outre-mer.' no_change: - direct: - - Abuja - - Accra - - Afghanistan - - Alaska - - Alofi - - Amman - - Amsterdam - - Angola - - Anguilla - - Ankara - - Antananarivo - - Apia - - Aruba - - Asmara - - Astana - - Avarua - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Bangladesh - - Bangui - - Banjul - - Basseterre - - Belfast - - Belgrade - - Belize - - Belmopan - - Berlin - - Bissau - - Botswana - - Bougainville - - Bratislava - - Brazzaville - - Bridgetown - - Brunei - - Budapest - - Buenos Aires - - Burkina Faso - - Burundi - - Canada - - Canberra - - Caracas - - Cardiff - - Castries - - 'Chișinău' - - Ciudad de la Paz - - Conakry - - Costa Rica - - Cuba - - 'Curaçao' - - Dakar - - Dili - - Djibouti - - Dodoma - - Doha - - Dublin - - Eswatini - - Europe - - France - - Freetown - - Funafuti - - Gabon - - Gaborone - - Georgetown - - Ghana - - Gibraltar - - Gitega - - Guadeloupe - - Guam - - Guatemala - - Guyana - - 'Hagåtña' - - Harare - - Hargeisa - - Helsinki - - Honduras - - Hong Kong - - Honiara - - Iran - - Islamabad - - Jakarta - - Java - - Jeju - - Jersey - - Kampala - - Kazakhstan - - Kenya - - Khartoum - - Kigali - - Kingston - - Kingstown - - Kinshasa - - Kiribati - - Kosovo - - Kuala Lumpur - - Laos - - Lesotho - - Liberia - - Libreville - - Liechtenstein - - Lilongwe - - Lima - - Ljubljana - - 'Lomé' - - Luanda - - Lusaka - - Luxembourg - - Madagascar - - Madrid - - Majuro - - Malawi - - Maldives - - Mali - - 'Malé' - - Managua - - Manama - - Maputo - - Mariehamn - - Martinique - - Maseru - - Mayotte - - Mbabane - - Minsk - - Monaco - - Monrovia - - Montevideo - - Moroni - - Mozambique - - Nairobi - - Nassau - - Nauru - - Naypyidaw - - New Delhi - - Ngerulmud - - Niamey - - Nicaragua - - Niger - - Nigeria - - Niue - - Nouakchott - - 'Nouméa' - - 'Nukuʻalofa' - - Nuuk - - Oman - - Oranjestad - - Oslo - - Ottawa - - Ouagadougou - - Pakistan - - Palestine - - Palikir - - Panama - - Papeete - - Paraguay - - Paramaribo - - Paris - - Philippines - - Phnom Penh - - Podgorica - - Port Moresby - - Port-au-Prince - - Porto-Novo - - Portugal - - Prague - - Praia - - Pristina - - Pyongyang - - Qatar - - Quito - - Rabat - - Riga - - Rome - - Roseau - - Rwanda - - Samoa - - 'San José' - - San Juan - - San Salvador - - Sanaa - - Santiago - - Sarajevo - - Seychelles - - Sierra Leone - - Skopje - - Sofia - - Somaliland - - Sri Lanka - - Stockholm - - Sucre - - Sumatra - - Suriname - - Suva - - Svalbard - - 'São Tomé' - - Taipei - - Tallinn - - Tegucigalpa - - Tirana - - Tiraspol - - Togo - - Tokyo - - Tonga - - Tripoli - - Tskhinvali - - Tunis - - Tuvalu - - 'Tórshavn' - - Ukraine - - Uruguay - - Vaduz - - Vanuatu - - Venezuela - - Victoria - - Vientiane - - Vilnius - - Wellington - - Willemstad - - Windhoek - - Yamoussoukro - - 'Yaoundé' - - Yaren - - Zagreb - - Zanzibar - - Zimbabwe + - Abuja + - Accra + - Afghanistan + - Alaska + - Alofi + - Amman + - Amsterdam + - Angola + - Anguilla + - Ankara + - Antananarivo + - Apia + - Aruba + - Asmara + - Astana + - Avarua + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangladesh + - Bangui + - Banjul + - Basseterre + - Belfast + - Belgrade + - Belize + - Belmopan + - Berlin + - Bissau + - Botswana + - Bougainville + - Bratislava + - Brazzaville + - Bridgetown + - Brunei + - Budapest + - Buenos Aires + - Burkina Faso + - Burundi + - Canada + - Canberra + - Caracas + - Cardiff + - Castries + - 'Chișinău' + - Ciudad de la Paz + - Conakry + - Costa Rica + - Cuba + - 'Curaçao' + - Dakar + - Dili + - Djibouti + - Dodoma + - Doha + - Dublin + - Eswatini + - Europe + - France + - Freetown + - Funafuti + - Gabon + - Gaborone + - Georgetown + - Ghana + - Gibraltar + - Gitega + - Guadeloupe + - Guam + - Guatemala + - Guyana + - 'Hagåtña' + - Harare + - Hargeisa + - Helsinki + - Honduras + - Hong Kong + - Honiara + - Iran + - Islamabad + - Jakarta + - Java + - Jeju + - Jersey + - Kampala + - Kazakhstan + - Kenya + - Khartoum + - Kigali + - Kingston + - Kingstown + - Kinshasa + - Kiribati + - Kosovo + - Kuala Lumpur + - Laos + - Lesotho + - Liberia + - Libreville + - Liechtenstein + - Lilongwe + - Lima + - Ljubljana + - 'Lomé' + - Luanda + - Lusaka + - Luxembourg + - Madagascar + - Madrid + - Majuro + - Malawi + - Maldives + - Mali + - 'Malé' + - Managua + - Manama + - Maputo + - Mariehamn + - Martinique + - Maseru + - Mayotte + - Mbabane + - Minsk + - Monaco + - Monrovia + - Montevideo + - Moroni + - Mozambique + - Nairobi + - Nassau + - Nauru + - Naypyidaw + - New Delhi + - Ngerulmud + - Niamey + - Nicaragua + - Niger + - Nigeria + - Niue + - Nouakchott + - 'Nouméa' + - 'Nukuʻalofa' + - Nuuk + - Oman + - Oranjestad + - Oslo + - Ottawa + - Ouagadougou + - Pakistan + - Palestine + - Palikir + - Panama + - Papeete + - Paraguay + - Paramaribo + - Paris + - Philippines + - Phnom Penh + - Podgorica + - Port Moresby + - Port-au-Prince + - Porto-Novo + - Portugal + - Prague + - Praia + - Pristina + - Pyongyang + - Qatar + - Quito + - Rabat + - Riga + - Rome + - Roseau + - Rwanda + - Samoa + - 'San José' + - San Juan + - San Salvador + - Sanaa + - Santiago + - Sarajevo + - Seychelles + - Sierra Leone + - Skopje + - Sofia + - Somaliland + - Sri Lanka + - Stockholm + - Sucre + - Sumatra + - Suriname + - Suva + - Svalbard + - 'São Tomé' + - Taipei + - Tallinn + - Tegucigalpa + - Tirana + - Tiraspol + - Togo + - Tokyo + - Tonga + - Tripoli + - Tskhinvali + - Tunis + - Tuvalu + - 'Tórshavn' + - Ukraine + - Uruguay + - Vaduz + - Vanuatu + - Venezuela + - Victoria + - Vientiane + - Vilnius + - Wellington + - Willemstad + - Windhoek + - Yamoussoukro + - 'Yaoundé' + - Yaren + - Zagreb + - Zanzibar + - Zimbabwe target_additions: notes.note.celebes-sea.fields.field.country-info: Aussi connu comme la mer de Sulawesi. variables: diff --git a/fixtures/ultimate-geography/overlays/languages/it.yaml b/fixtures/ultimate-geography/overlays/languages/it.yaml index b142c20..eb10cc6 100644 --- a/fixtures/ultimate-geography/overlays/languages/it.yaml +++ b/fixtures/ultimate-geography/overlays/languages/it.yaml @@ -376,286 +376,285 @@ translations: monaco.fields.field.capital: Monaco: Comune di Monaco no_change: - direct: - - Abu Dhabi - - Abuja - - Accra - - Afghanistan - - Africa - - Alaska - - Albania - - Algeria - - Alofi - - Amman - - Amsterdam - - Andorra - - Andorra la Vella - - Angola - - Anguilla - - Ankara - - Antananarivo - - Apia - - Argentina - - Armenia - - Aruba - - Asia - - Asmara - - Astana - - 'Asunción' - - Australia - - Austria - - Avarua - - Baghdad - - Baku - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Bangladesh - - Bangui - - Banjul - - Barbados - - Basseterre - - Beirut - - Belfast - - Belize - - Belmopan - - Benin - - Bermuda - - Bhutan - - Bissau - - Bolivia - - Botswana - - Bougainville - - Bratislava - - Brazzaville - - Bridgetown - - Brunei - - Budapest - - Buenos Aires - - Bulgaria - - Burkina Faso - - Burundi - - Canada - - Canberra - - Caracas - - Cardiff - - Castries - - Charlotte Amalie - - 'Chișinău' - - Ciudad de la Paz - - Colombia - - Conakry - - Corsica - - Costa Rica - - Cuba - - 'Curaçao' - - Dakar - - Dili - - Dodoma - - Doha - - Dominica - - Ecuador - - El Salvador - - Eritrea - - Estonia - - Freetown - - Funafuti - - Gabon - - Gaborone - - Georgetown - - Georgia - - Ghana - - Gitega - - Grenada - - Guam - - Guatemala - - Guernsey - - Guinea - - Guinea-Bissau - - Guyana - - 'Hagåtña' - - Haiti - - Hanoi - - Harare - - Hawaii - - Helsinki - - Honduras - - Hong Kong - - Honiara - - India - - Indonesia - - Iran - - Iraq - - Islamabad - - Jeju - - Jersey - - Kabul - - Kampala - - Kenya - - Kigali - - Kingston - - Kingstown - - Kinshasa - - Kiribati - - Kosovo - - Kuala Lumpur - - Kuwait - - Laos - - Lesotho - - Liberia - - Libreville - - Liechtenstein - - Lilongwe - - Lima - - 'Lomé' - - Luanda - - Lusaka - - Madagascar - - Madrid - - Majuro - - Malawi - - Malaysia - - Mali - - Malta - - 'Malé' - - Managua - - Manama - - Manila - - Maputo - - Mariehamn - - Maseru - - Mauritania - - Mauritius - - Mayotte - - Mbabane - - Melanesia - - Micronesia - - Minsk - - Mongolia - - Monrovia - - Montenegro - - Montevideo - - Moroni - - 'N''Djamena' - - Nairobi - - Namibia - - Nassau - - Nauru - - Naypyidaw - - Nepal - - Ngerulmud - - Niamey - - Nicaragua - - Nicosia - - Niger - - Nigeria - - Niue - - Nouakchott - - 'Nukuʻalofa' - - Nuuk - - Oceania - - Oman - - Oranjestad - - Oslo - - Ottawa - - Ouagadougou - - Pakistan - - Palau - - Palikir - - Panama - - Papeete - - Paraguay - - Paramaribo - - Phnom Penh - - Podgorica - - Port Louis - - Port Moresby - - Port Vila - - Port of Spain - - Port-au-Prince - - Porto-Novo - - Praia - - Pristina - - Pyongyang - - Qatar - - Quito - - Rabat - - 'Reykjavík' - - Riga - - Romania - - Roseau - - Russia - - Saint Lucia - - Samoa - - 'San José' - - San Juan - - San Marino - - San Salvador - - Santo Domingo - - Sarajevo - - Scandinavia - - Senegal - - Serbia - - Seychelles - - Sierra Leone - - Singapore - - Sint Maarten - - Skopje - - Slovenia - - Sofia - - Somalia - - Somaliland - - Sri Lanka - - Sucre - - Sudan - - Sukhumi - - Sumatra - - Suriname - - Suva - - Svalbard - - 'São Tomé' - - Taipei - - Taiwan - - Tallinn - - Tanzania - - Tashkent - - Tbilisi - - Tegucigalpa - - Thimphu - - Tirana - - Tiraspol - - Togo - - Tokyo - - Tonga - - Transnistria - - Tripoli - - Tskhinvali - - Tunisia - - Turkmenistan - - Tuvalu - - 'Tórshavn' - - Uganda - - Uruguay - - Uzbekistan - - Vaduz - - Vanuatu - - Venezuela - - Victoria - - Vienna - - Vientiane - - Vietnam - - Vilnius - - Wellington - - Willemstad - - Windhoek - - Yamoussoukro - - 'Yaoundé' - - Yaren - - Yemen - - Zambia - - Zanzibar - - Zimbabwe + - Abu Dhabi + - Abuja + - Accra + - Afghanistan + - Africa + - Alaska + - Albania + - Algeria + - Alofi + - Amman + - Amsterdam + - Andorra + - Andorra la Vella + - Angola + - Anguilla + - Ankara + - Antananarivo + - Apia + - Argentina + - Armenia + - Aruba + - Asia + - Asmara + - Astana + - 'Asunción' + - Australia + - Austria + - Avarua + - Baghdad + - Baku + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangladesh + - Bangui + - Banjul + - Barbados + - Basseterre + - Beirut + - Belfast + - Belize + - Belmopan + - Benin + - Bermuda + - Bhutan + - Bissau + - Bolivia + - Botswana + - Bougainville + - Bratislava + - Brazzaville + - Bridgetown + - Brunei + - Budapest + - Buenos Aires + - Bulgaria + - Burkina Faso + - Burundi + - Canada + - Canberra + - Caracas + - Cardiff + - Castries + - Charlotte Amalie + - 'Chișinău' + - Ciudad de la Paz + - Colombia + - Conakry + - Corsica + - Costa Rica + - Cuba + - 'Curaçao' + - Dakar + - Dili + - Dodoma + - Doha + - Dominica + - Ecuador + - El Salvador + - Eritrea + - Estonia + - Freetown + - Funafuti + - Gabon + - Gaborone + - Georgetown + - Georgia + - Ghana + - Gitega + - Grenada + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-Bissau + - Guyana + - 'Hagåtña' + - Haiti + - Hanoi + - Harare + - Hawaii + - Helsinki + - Honduras + - Hong Kong + - Honiara + - India + - Indonesia + - Iran + - Iraq + - Islamabad + - Jeju + - Jersey + - Kabul + - Kampala + - Kenya + - Kigali + - Kingston + - Kingstown + - Kinshasa + - Kiribati + - Kosovo + - Kuala Lumpur + - Kuwait + - Laos + - Lesotho + - Liberia + - Libreville + - Liechtenstein + - Lilongwe + - Lima + - 'Lomé' + - Luanda + - Lusaka + - Madagascar + - Madrid + - Majuro + - Malawi + - Malaysia + - Mali + - Malta + - 'Malé' + - Managua + - Manama + - Manila + - Maputo + - Mariehamn + - Maseru + - Mauritania + - Mauritius + - Mayotte + - Mbabane + - Melanesia + - Micronesia + - Minsk + - Mongolia + - Monrovia + - Montenegro + - Montevideo + - Moroni + - 'N''Djamena' + - Nairobi + - Namibia + - Nassau + - Nauru + - Naypyidaw + - Nepal + - Ngerulmud + - Niamey + - Nicaragua + - Nicosia + - Niger + - Nigeria + - Niue + - Nouakchott + - 'Nukuʻalofa' + - Nuuk + - Oceania + - Oman + - Oranjestad + - Oslo + - Ottawa + - Ouagadougou + - Pakistan + - Palau + - Palikir + - Panama + - Papeete + - Paraguay + - Paramaribo + - Phnom Penh + - Podgorica + - Port Louis + - Port Moresby + - Port Vila + - Port of Spain + - Port-au-Prince + - Porto-Novo + - Praia + - Pristina + - Pyongyang + - Qatar + - Quito + - Rabat + - 'Reykjavík' + - Riga + - Romania + - Roseau + - Russia + - Saint Lucia + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Santo Domingo + - Sarajevo + - Scandinavia + - Senegal + - Serbia + - Seychelles + - Sierra Leone + - Singapore + - Sint Maarten + - Skopje + - Slovenia + - Sofia + - Somalia + - Somaliland + - Sri Lanka + - Sucre + - Sudan + - Sukhumi + - Sumatra + - Suriname + - Suva + - Svalbard + - 'São Tomé' + - Taipei + - Taiwan + - Tallinn + - Tanzania + - Tashkent + - Tbilisi + - Tegucigalpa + - Thimphu + - Tirana + - Tiraspol + - Togo + - Tokyo + - Tonga + - Transnistria + - Tripoli + - Tskhinvali + - Tunisia + - Turkmenistan + - Tuvalu + - 'Tórshavn' + - Uganda + - Uruguay + - Uzbekistan + - Vaduz + - Vanuatu + - Venezuela + - Victoria + - Vienna + - Vientiane + - Vietnam + - Vilnius + - Wellington + - Willemstad + - Windhoek + - Yamoussoukro + - 'Yaoundé' + - Yaren + - Yemen + - Zambia + - Zanzibar + - Zimbabwe target_additions: notes.note.arctic-ocean.fields.field.country-info: Conosciuto a livello internazionale come Oceano Artico. variables: diff --git a/fixtures/ultimate-geography/overlays/languages/nb.yaml b/fixtures/ultimate-geography/overlays/languages/nb.yaml index 41a1058..b83fac9 100644 --- a/fixtures/ultimate-geography/overlays/languages/nb.yaml +++ b/fixtures/ultimate-geography/overlays/languages/nb.yaml @@ -372,328 +372,327 @@ translations: wallis-and-futuna: Overseas territory of France.: 'Fransk oversjøisk land.' no_change: - direct: - - Abu Dhabi - - Abuja - - Accra - - Afghanistan - - Alaska - - Albania - - Alofi - - Amman - - Amsterdam - - Andorra - - Andorra la Vella - - Angola - - Anguilla - - Ankara - - Antananarivo - - Apia - - Argentina - - Armenia - - Aruba - - Asia - - Asmara - - Astana - - 'Asunción' - - Australia - - Avarua - - Bahrain - - Baku - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Bangladesh - - Bangui - - Banjul - - Barbados - - Basseterre - - Beijing - - Beirut - - Belarus - - Belfast - - Belize - - Belmopan - - Benin - - Berlin - - Bermuda - - Bern - - Bhutan - - Bissau - - 'Bogotá' - - Bolivia - - Botswana - - Bougainville - - Bratislava - - Brazzaville - - Bridgetown - - Brunei - - Budapest - - Buenos Aires - - Bulgaria - - Burkina Faso - - Burundi - - Canada - - Canberra - - Caracas - - Cardiff - - Castries - - Charlotte Amalie - - Chile - - 'Chișinău' - - Ciudad de la Paz - - Colombia - - Conakry - - Costa Rica - - Cuba - - 'Curaçao' - - Dakar - - Dhaka - - Dili - - Djibouti - - Dodoma - - Doha - - Dominica - - Dublin - - Ecuador - - Edinburgh - - Egypt - - El Salvador - - England - - Eritrea - - Eswatini - - Fiji - - Finland - - Freetown - - Funafuti - - Gabon - - Gaborone - - Georgetown - - Georgia - - Ghana - - Gibraltar - - Gitega - - Grenada - - Guadeloupe - - Guam - - Guatemala - - Guernsey - - Guinea - - Guinea-Bissau - - Guyana - - 'Hagåtña' - - Haiti - - Hanoi - - Harare - - Hargeisa - - Hawaii - - Honduras - - Honiara - - India - - Indonesia - - Iran - - Islamabad - - Israel - - Jakarta - - Jamaica - - Japan - - Java - - Jeju - - Jersey - - Jerusalem - - Jordan - - Juba - - Kabul - - Kampala - - Kenya - - Khartoum - - Kigali - - Kingston - - Kingstown - - Kinshasa - - Kiribati - - Kosovo - - Kuala Lumpur - - Kuwait - - Laos - - Latvia - - Lesotho - - Liberia - - Libreville - - Libya - - Liechtenstein - - Lilongwe - - Lima - - Ljubljana - - 'Lomé' - - London - - Luanda - - Lusaka - - Luxembourg - - Madeira - - Madrid - - Majuro - - Malawi - - Malaysia - - Mali - - Malta - - 'Malé' - - Managua - - Manama - - Manila - - Maputo - - Mariehamn - - Martinique - - Maseru - - Mauritania - - Mauritius - - Mayotte - - Mbabane - - Melanesia - - Mexico - - Minsk - - Mogadishu - - Moldova - - Monaco - - Mongolia - - Monrovia - - Montenegro - - Montevideo - - Moroni - - Muscat - - Myanmar - - Nairobi - - Namibia - - Nassau - - Nauru - - Naypyidaw - - Nepal - - New Delhi - - New Zealand - - Ngerulmud - - Niamey - - Nicaragua - - Niger - - Nigeria - - Niue - - Nouakchott - - 'Nouméa' - - Nuuk - - Oman - - Oranjestad - - Oslo - - Ottawa - - Ouagadougou - - Pakistan - - Palau - - Palikir - - Panama - - Papeete - - Paraguay - - Paramaribo - - Paris - - Peru - - Phnom Penh - - Podgorica - - Polynesia - - Port Louis - - Port Moresby - - Port of Spain - - Port-au-Prince - - Porto-Novo - - Portugal - - Praia - - Pretoria, Cape Town, Bloemfontein - - Pristina - - Puerto Rico - - Pyongyang - - Qatar - - Quito - - Rabat - - 'Reykjavík' - - Riga - - Riyadh - - Romania - - Roseau - - Rwanda - - 'Réunion' - - Saint Lucia - - Samoa - - 'San José' - - San Juan - - San Marino - - San Salvador - - Santo Domingo - - Sarajevo - - Sardinia - - Senegal - - Seoul - - Serbia - - Sierra Leone - - Singapore - - Sint Maarten - - Skopje - - Slovakia - - Slovenia - - Sofia - - Somalia - - Somaliland - - South Tarawa - - Sri Lanka - - 'St. George''s' - - Stockholm - - Sucre - - Sudan - - Sukhumi - - Sumatra - - Suva - - Svalbard - - Syria - - 'São Tomé' - - Taipei - - Taiwan - - Tallinn - - Tanzania - - Tbilisi - - Tegucigalpa - - Thailand - - Tirana - - Tiraspol - - Togo - - Tokyo - - Tonga - - Transnistria - - Tripoli - - Tskhinvali - - Tunis - - Tunisia - - Turkmenistan - - Tuvalu - - 'Tórshavn' - - Uganda - - Ulaanbaatar - - Uruguay - - Vaduz - - Valletta - - Vanuatu - - Venezuela - - Victoria - - Vientiane - - Vietnam - - Vilnius - - Wales - - Washington, D.C. - - Wellington - - Willemstad - - Windhoek - - Yamoussoukro - - 'Yaoundé' - - Yaren - - Zagreb - - Zambia - - Zanzibar - - Zimbabwe + - Abu Dhabi + - Abuja + - Accra + - Afghanistan + - Alaska + - Albania + - Alofi + - Amman + - Amsterdam + - Andorra + - Andorra la Vella + - Angola + - Anguilla + - Ankara + - Antananarivo + - Apia + - Argentina + - Armenia + - Aruba + - Asia + - Asmara + - Astana + - 'Asunción' + - Australia + - Avarua + - Bahrain + - Baku + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangladesh + - Bangui + - Banjul + - Barbados + - Basseterre + - Beijing + - Beirut + - Belarus + - Belfast + - Belize + - Belmopan + - Benin + - Berlin + - Bermuda + - Bern + - Bhutan + - Bissau + - 'Bogotá' + - Bolivia + - Botswana + - Bougainville + - Bratislava + - Brazzaville + - Bridgetown + - Brunei + - Budapest + - Buenos Aires + - Bulgaria + - Burkina Faso + - Burundi + - Canada + - Canberra + - Caracas + - Cardiff + - Castries + - Charlotte Amalie + - Chile + - 'Chișinău' + - Ciudad de la Paz + - Colombia + - Conakry + - Costa Rica + - Cuba + - 'Curaçao' + - Dakar + - Dhaka + - Dili + - Djibouti + - Dodoma + - Doha + - Dominica + - Dublin + - Ecuador + - Edinburgh + - Egypt + - El Salvador + - England + - Eritrea + - Eswatini + - Fiji + - Finland + - Freetown + - Funafuti + - Gabon + - Gaborone + - Georgetown + - Georgia + - Ghana + - Gibraltar + - Gitega + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guinea + - Guinea-Bissau + - Guyana + - 'Hagåtña' + - Haiti + - Hanoi + - Harare + - Hargeisa + - Hawaii + - Honduras + - Honiara + - India + - Indonesia + - Iran + - Islamabad + - Israel + - Jakarta + - Jamaica + - Japan + - Java + - Jeju + - Jersey + - Jerusalem + - Jordan + - Juba + - Kabul + - Kampala + - Kenya + - Khartoum + - Kigali + - Kingston + - Kingstown + - Kinshasa + - Kiribati + - Kosovo + - Kuala Lumpur + - Kuwait + - Laos + - Latvia + - Lesotho + - Liberia + - Libreville + - Libya + - Liechtenstein + - Lilongwe + - Lima + - Ljubljana + - 'Lomé' + - London + - Luanda + - Lusaka + - Luxembourg + - Madeira + - Madrid + - Majuro + - Malawi + - Malaysia + - Mali + - Malta + - 'Malé' + - Managua + - Manama + - Manila + - Maputo + - Mariehamn + - Martinique + - Maseru + - Mauritania + - Mauritius + - Mayotte + - Mbabane + - Melanesia + - Mexico + - Minsk + - Mogadishu + - Moldova + - Monaco + - Mongolia + - Monrovia + - Montenegro + - Montevideo + - Moroni + - Muscat + - Myanmar + - Nairobi + - Namibia + - Nassau + - Nauru + - Naypyidaw + - Nepal + - New Delhi + - New Zealand + - Ngerulmud + - Niamey + - Nicaragua + - Niger + - Nigeria + - Niue + - Nouakchott + - 'Nouméa' + - Nuuk + - Oman + - Oranjestad + - Oslo + - Ottawa + - Ouagadougou + - Pakistan + - Palau + - Palikir + - Panama + - Papeete + - Paraguay + - Paramaribo + - Paris + - Peru + - Phnom Penh + - Podgorica + - Polynesia + - Port Louis + - Port Moresby + - Port of Spain + - Port-au-Prince + - Porto-Novo + - Portugal + - Praia + - Pretoria, Cape Town, Bloemfontein + - Pristina + - Puerto Rico + - Pyongyang + - Qatar + - Quito + - Rabat + - 'Reykjavík' + - Riga + - Riyadh + - Romania + - Roseau + - Rwanda + - 'Réunion' + - Saint Lucia + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Santo Domingo + - Sarajevo + - Sardinia + - Senegal + - Seoul + - Serbia + - Sierra Leone + - Singapore + - Sint Maarten + - Skopje + - Slovakia + - Slovenia + - Sofia + - Somalia + - Somaliland + - South Tarawa + - Sri Lanka + - 'St. George''s' + - Stockholm + - Sucre + - Sudan + - Sukhumi + - Sumatra + - Suva + - Svalbard + - Syria + - 'São Tomé' + - Taipei + - Taiwan + - Tallinn + - Tanzania + - Tbilisi + - Tegucigalpa + - Thailand + - Tirana + - Tiraspol + - Togo + - Tokyo + - Tonga + - Transnistria + - Tripoli + - Tskhinvali + - Tunis + - Tunisia + - Turkmenistan + - Tuvalu + - 'Tórshavn' + - Uganda + - Ulaanbaatar + - Uruguay + - Vaduz + - Valletta + - Vanuatu + - Venezuela + - Victoria + - Vientiane + - Vietnam + - Vilnius + - Wales + - Washington, D.C. + - Wellington + - Willemstad + - Windhoek + - Yamoussoukro + - 'Yaoundé' + - Yaren + - Zagreb + - Zambia + - Zanzibar + - Zimbabwe target_additions: notes.note.arabian-sea.fields.field.country-info: 'Også kalt Arabiahavet.' notes.note.balkan-peninsula.fields.field.country-info: 'Også kalt Balkanhalvøya.' diff --git a/fixtures/ultimate-geography/overlays/languages/nl.yaml b/fixtures/ultimate-geography/overlays/languages/nl.yaml index 5a86f3b..f1a828c 100644 --- a/fixtures/ultimate-geography/overlays/languages/nl.yaml +++ b/fixtures/ultimate-geography/overlays/languages/nl.yaml @@ -376,278 +376,277 @@ translations: Yerevan: Jerevan 'Åland Islands': 'Åland' no_change: - direct: - - Abu Dhabi - - Abuja - - Accra - - Afghanistan - - Alaska - - Algiers - - Alofi - - Amman - - Amsterdam - - Andorra - - Andorra la Vella - - Angola - - Anguilla - - Ankara - - Antananarivo - - Antarctica - - Apia - - Aruba - - Asmara - - Astana - - 'Asunción' - - Avarua - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Bangladesh - - Bangui - - Banjul - - Barbados - - Basseterre - - Belfast - - Belize - - Belmopan - - Benin - - Bermuda - - Bern - - Bhutan - - Bissau - - Bolivia - - Botswana - - Bougainville - - Bratislava - - Brazzaville - - Bridgetown - - Brunei - - Buenos Aires - - Burkina Faso - - Burundi - - Canada - - Canberra - - Caracas - - Cardiff - - Castries - - Charlotte Amalie - - China - - 'Chișinău' - - Ciudad de la Paz - - Colombia - - Conakry - - Corsica - - Costa Rica - - Cuba - - 'Curaçao' - - Cyprus - - Dakar - - Damascus - - Dhaka - - Dili - - Djibouti - - Dodoma - - Doha - - Dominica - - Dublin - - Ecuador - - Edinburgh - - El Salvador - - Eritrea - - Eswatini - - Fiji - - Finland - - Freetown - - Funafuti - - Gabon - - Gaborone - - Georgetown - - Ghana - - Gibraltar - - Gitega - - Grenada - - Guadeloupe - - Guam - - Guatemala - - Guernsey - - Guyana - - 'Hagåtña' - - Hanoi - - Harare - - Hargeisa - - Havana - - Helsinki - - Honduras - - Honiara - - India - - Iran - - Islamabad - - Jakarta - - Jamaica - - Japan - - Java - - Jeju - - Jersey - - Kabul - - Kampala - - Kathmandu - - Kigali - - Kingston - - Kingstown - - Kinshasa - - Kiribati - - Kosovo - - Kuala Lumpur - - Laos - - Lesotho - - Liberia - - Libreville - - Liechtenstein - - Lilongwe - - Lima - - Ljubljana - - 'Lomé' - - Luanda - - Lusaka - - Macau - - Madeira - - Madrid - - Majuro - - Malawi - - Mali - - Malta - - 'Malé' - - Managua - - Manama - - Maputo - - Mariehamn - - Martinique - - Maseru - - Mauritius - - Mayotte - - Mbabane - - Mexico - - Minsk - - Mogadishu - - Monaco - - Monrovia - - Montenegro - - Montevideo - - Moroni - - Mozambique - - Myanmar - - Nairobi - - Nassau - - Nauru - - Naypyidaw - - Nepal - - New Delhi - - Ngerulmud - - Niamey - - Nicaragua - - Nicosia - - Niger - - Nigeria - - Niue - - Nouakchott - - 'Nouméa' - - 'Nukuʻalofa' - - Nuuk - - Oman - - Oranjestad - - Oslo - - Ottawa - - Ouagadougou - - Pakistan - - Palau - - Palikir - - Panama - - Papeete - - Paraguay - - Paramaribo - - Peru - - Phnom Penh - - Podgorica - - Port Louis - - Port Moresby - - Port Vila - - Port of Spain - - Port-au-Prince - - Porto-Novo - - Portugal - - Praia - - Pristina - - Puerto Rico - - Pyongyang - - Qatar - - Quito - - Rabat - - Riga - - Rome - - Roseau - - Rwanda - - 'Réunion' - - Saint Lucia - - Samoa - - 'San José' - - San Juan - - San Marino - - San Salvador - - Sanaa - - Santiago - - Santo Domingo - - Sarajevo - - Senegal - - Sierra Leone - - Singapore - - Skopje - - Sofia - - Somaliland - - Sri Lanka - - Stockholm - - Sucre - - Sumatra - - Suriname - - Suva - - Taipei - - Taiwan - - Tallinn - - Tanzania - - Tbilisi - - Tegucigalpa - - Thailand - - Thimphu - - Tirana - - Tiraspol - - Togo - - Tonga - - Tripoli - - Tunis - - Turkmenistan - - Tuvalu - - 'Tórshavn' - - Ulaanbaatar - - Uruguay - - Vaduz - - Valletta - - Vanuatu - - Venezuela - - Victoria - - Vientiane - - Vietnam - - Vilnius - - Wales - - Wellington - - Willemstad - - Windhoek - - Yamoussoukro - - 'Yaoundé' - - Yaren - - Zagreb - - Zambia - - Zanzibar - - Zimbabwe + - Abu Dhabi + - Abuja + - Accra + - Afghanistan + - Alaska + - Algiers + - Alofi + - Amman + - Amsterdam + - Andorra + - Andorra la Vella + - Angola + - Anguilla + - Ankara + - Antananarivo + - Antarctica + - Apia + - Aruba + - Asmara + - Astana + - 'Asunción' + - Avarua + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangladesh + - Bangui + - Banjul + - Barbados + - Basseterre + - Belfast + - Belize + - Belmopan + - Benin + - Bermuda + - Bern + - Bhutan + - Bissau + - Bolivia + - Botswana + - Bougainville + - Bratislava + - Brazzaville + - Bridgetown + - Brunei + - Buenos Aires + - Burkina Faso + - Burundi + - Canada + - Canberra + - Caracas + - Cardiff + - Castries + - Charlotte Amalie + - China + - 'Chișinău' + - Ciudad de la Paz + - Colombia + - Conakry + - Corsica + - Costa Rica + - Cuba + - 'Curaçao' + - Cyprus + - Dakar + - Damascus + - Dhaka + - Dili + - Djibouti + - Dodoma + - Doha + - Dominica + - Dublin + - Ecuador + - Edinburgh + - El Salvador + - Eritrea + - Eswatini + - Fiji + - Finland + - Freetown + - Funafuti + - Gabon + - Gaborone + - Georgetown + - Ghana + - Gibraltar + - Gitega + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guernsey + - Guyana + - 'Hagåtña' + - Hanoi + - Harare + - Hargeisa + - Havana + - Helsinki + - Honduras + - Honiara + - India + - Iran + - Islamabad + - Jakarta + - Jamaica + - Japan + - Java + - Jeju + - Jersey + - Kabul + - Kampala + - Kathmandu + - Kigali + - Kingston + - Kingstown + - Kinshasa + - Kiribati + - Kosovo + - Kuala Lumpur + - Laos + - Lesotho + - Liberia + - Libreville + - Liechtenstein + - Lilongwe + - Lima + - Ljubljana + - 'Lomé' + - Luanda + - Lusaka + - Macau + - Madeira + - Madrid + - Majuro + - Malawi + - Mali + - Malta + - 'Malé' + - Managua + - Manama + - Maputo + - Mariehamn + - Martinique + - Maseru + - Mauritius + - Mayotte + - Mbabane + - Mexico + - Minsk + - Mogadishu + - Monaco + - Monrovia + - Montenegro + - Montevideo + - Moroni + - Mozambique + - Myanmar + - Nairobi + - Nassau + - Nauru + - Naypyidaw + - Nepal + - New Delhi + - Ngerulmud + - Niamey + - Nicaragua + - Nicosia + - Niger + - Nigeria + - Niue + - Nouakchott + - 'Nouméa' + - 'Nukuʻalofa' + - Nuuk + - Oman + - Oranjestad + - Oslo + - Ottawa + - Ouagadougou + - Pakistan + - Palau + - Palikir + - Panama + - Papeete + - Paraguay + - Paramaribo + - Peru + - Phnom Penh + - Podgorica + - Port Louis + - Port Moresby + - Port Vila + - Port of Spain + - Port-au-Prince + - Porto-Novo + - Portugal + - Praia + - Pristina + - Puerto Rico + - Pyongyang + - Qatar + - Quito + - Rabat + - Riga + - Rome + - Roseau + - Rwanda + - 'Réunion' + - Saint Lucia + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Sanaa + - Santiago + - Santo Domingo + - Sarajevo + - Senegal + - Sierra Leone + - Singapore + - Skopje + - Sofia + - Somaliland + - Sri Lanka + - Stockholm + - Sucre + - Sumatra + - Suriname + - Suva + - Taipei + - Taiwan + - Tallinn + - Tanzania + - Tbilisi + - Tegucigalpa + - Thailand + - Thimphu + - Tirana + - Tiraspol + - Togo + - Tonga + - Tripoli + - Tunis + - Turkmenistan + - Tuvalu + - 'Tórshavn' + - Ulaanbaatar + - Uruguay + - Vaduz + - Valletta + - Vanuatu + - Venezuela + - Victoria + - Vientiane + - Vietnam + - Vilnius + - Wales + - Wellington + - Willemstad + - Windhoek + - Yamoussoukro + - 'Yaoundé' + - Yaren + - Zagreb + - Zambia + - Zanzibar + - Zimbabwe target_additions: notes.note.baltic-sea.fields.field.country-info: Ook wel Baltische Zee. notes.note.belarus.fields.field.country-info: Ook bekend als Belarus. diff --git a/fixtures/ultimate-geography/overlays/languages/pl.yaml b/fixtures/ultimate-geography/overlays/languages/pl.yaml index 1055afb..48405d1 100644 --- a/fixtures/ultimate-geography/overlays/languages/pl.yaml +++ b/fixtures/ultimate-geography/overlays/languages/pl.yaml @@ -453,201 +453,200 @@ translations: Zagreb: Zagrzeb 'Åland Islands': Wyspy Alandzkie no_change: - direct: - - Alaska - - Albania - - Alofi - - Amman - - Amsterdam - - Angola - - Anguilla - - Ankara - - Apia - - Armenia - - Aruba - - Asmara - - Astana - - 'Asunción' - - Australia - - Austria - - Avarua - - Baku - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Barbados - - Basseterre - - Belfast - - Belize - - Belmopan - - Benin - - Berlin - - Bhutan - - Bissau - - Botswana - - Bougainville - - Brazzaville - - Bridgetown - - Brunei - - Buenos Aires - - Burkina Faso - - Burundi - - Canberra - - Caracas - - Cardiff - - Castries - - Charlotte Amalie - - Chile - - Ciudad de la Paz - - 'Curaçao' - - Dakar - - Dhaka - - Dili - - Dodoma - - Doha - - Dublin - - Estonia - - Eswatini - - Freetown - - Funafuti - - Gabon - - Gaborone - - Georgetown - - Ghana - - Gibraltar - - Gitega - - Grenada - - Guam - - Guernsey - - 'Hagåtña' - - Haiti - - Hanoi - - Harare - - Helsinki - - Honduras - - Honiara - - Iran - - Islamabad - - Jersey - - Kabul - - Kampala - - Kigali - - Kingston - - Kingstown - - Kiribati - - Kuala Lumpur - - Laos - - Lesotho - - Liberia - - Libreville - - Liechtenstein - - Lilongwe - - Lima - - 'Lomé' - - Luanda - - Lusaka - - Majuro - - Malawi - - Mali - - Malta - - Managua - - Manama - - Manila - - Maputo - - Maseru - - Mauritius - - Mbabane - - Mongolia - - Monrovia - - Montevideo - - Moroni - - Nairobi - - Namibia - - Nassau - - Nauru - - Naypyidaw - - Nepal - - Ngerulmud - - Niamey - - Niger - - Nigeria - - Niue - - 'Nukuʻalofa' - - Nuuk - - Oceania - - Oman - - Oranjestad - - Oslo - - Ottawa - - Pakistan - - Palau - - Palikir - - Panama - - Papeete - - Paramaribo - - Peru - - Phnom Penh - - Podgorica - - Port Louis - - Port Moresby - - Port Vila - - Port-au-Prince - - Porto-Novo - - Praia - - Quito - - Rabat - - 'Reykjavík' - - Roseau - - Rwanda - - Saint Lucia - - Samoa - - 'San José' - - San Juan - - San Marino - - San Salvador - - Santiago - - Santo Domingo - - Senegal - - Serbia - - Sierra Leone - - Sint Maarten - - Skopje - - Sofia - - Somalia - - Somaliland - - South Tarawa - - Sri Lanka - - Sucre - - Sudan - - Sumatra - - Suva - - Svalbard - - Syria - - 'São Tomé' - - Tallinn - - Tanzania - - Tbilisi - - Tegucigalpa - - Thimphu - - Tirana - - Togo - - Tonga - - Tunis - - Turkmenistan - - Tuvalu - - Uganda - - Uzbekistan - - Vaduz - - Valletta - - Vanuatu - - Victoria - - Wellington - - Willemstad - - Yaren - - Zambia - - Zanzibar - - Zimbabwe + - Alaska + - Albania + - Alofi + - Amman + - Amsterdam + - Angola + - Anguilla + - Ankara + - Apia + - Armenia + - Aruba + - Asmara + - Astana + - 'Asunción' + - Australia + - Austria + - Avarua + - Baku + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Barbados + - Basseterre + - Belfast + - Belize + - Belmopan + - Benin + - Berlin + - Bhutan + - Bissau + - Botswana + - Bougainville + - Brazzaville + - Bridgetown + - Brunei + - Buenos Aires + - Burkina Faso + - Burundi + - Canberra + - Caracas + - Cardiff + - Castries + - Charlotte Amalie + - Chile + - Ciudad de la Paz + - 'Curaçao' + - Dakar + - Dhaka + - Dili + - Dodoma + - Doha + - Dublin + - Estonia + - Eswatini + - Freetown + - Funafuti + - Gabon + - Gaborone + - Georgetown + - Ghana + - Gibraltar + - Gitega + - Grenada + - Guam + - Guernsey + - 'Hagåtña' + - Haiti + - Hanoi + - Harare + - Helsinki + - Honduras + - Honiara + - Iran + - Islamabad + - Jersey + - Kabul + - Kampala + - Kigali + - Kingston + - Kingstown + - Kiribati + - Kuala Lumpur + - Laos + - Lesotho + - Liberia + - Libreville + - Liechtenstein + - Lilongwe + - Lima + - 'Lomé' + - Luanda + - Lusaka + - Majuro + - Malawi + - Mali + - Malta + - Managua + - Manama + - Manila + - Maputo + - Maseru + - Mauritius + - Mbabane + - Mongolia + - Monrovia + - Montevideo + - Moroni + - Nairobi + - Namibia + - Nassau + - Nauru + - Naypyidaw + - Nepal + - Ngerulmud + - Niamey + - Niger + - Nigeria + - Niue + - 'Nukuʻalofa' + - Nuuk + - Oceania + - Oman + - Oranjestad + - Oslo + - Ottawa + - Pakistan + - Palau + - Palikir + - Panama + - Papeete + - Paramaribo + - Peru + - Phnom Penh + - Podgorica + - Port Louis + - Port Moresby + - Port Vila + - Port-au-Prince + - Porto-Novo + - Praia + - Quito + - Rabat + - 'Reykjavík' + - Roseau + - Rwanda + - Saint Lucia + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Santiago + - Santo Domingo + - Senegal + - Serbia + - Sierra Leone + - Sint Maarten + - Skopje + - Sofia + - Somalia + - Somaliland + - South Tarawa + - Sri Lanka + - Sucre + - Sudan + - Sumatra + - Suva + - Svalbard + - Syria + - 'São Tomé' + - Tallinn + - Tanzania + - Tbilisi + - Tegucigalpa + - Thimphu + - Tirana + - Togo + - Tonga + - Tunis + - Turkmenistan + - Tuvalu + - Uganda + - Uzbekistan + - Vaduz + - Valletta + - Vanuatu + - Victoria + - Wellington + - Willemstad + - Yaren + - Zambia + - Zanzibar + - Zimbabwe target_additions: notes.note.arctic-ocean.fields.field.country-info: 'Znany także jako Ocean Północny lub Morze Arktyczne.' notes.note.netherlands.fields.field.country-info: 'Znana także jako Niderlandy.' diff --git a/fixtures/ultimate-geography/overlays/languages/pt.yaml b/fixtures/ultimate-geography/overlays/languages/pt.yaml index f1a3759..cd9e8e1 100644 --- a/fixtures/ultimate-geography/overlays/languages/pt.yaml +++ b/fixtures/ultimate-geography/overlays/languages/pt.yaml @@ -541,130 +541,129 @@ translations: wallis-and-futuna: Overseas territory of France.: 'Território ultramarino da França.' no_change: - direct: - - Abuja - - Alofi - - Andorra - - Angola - - Antananarivo - - Apia - - Argentina - - Aruba - - Asmara - - Astana - - Avarua - - Bali - - Bangui - - Banjul - - Barbados - - Basseterre - - Belize - - Bissau - - 'Bogotá' - - Bougainville - - 'Brasília' - - Bratislava - - Bridgetown - - Brunei - - Buenos Aires - - Burundi - - Cairo - - Caracas - - Castries - - Chile - - China - - Costa Rica - - Cuba - - 'Curaçao' - - Dodoma - - Dominica - - El Salvador - - Fiji - - Freetown - - Funafuti - - Gaborone - - Georgetown - - Gibraltar - - Guam - - Guatemala - - Haiti - - Harare - - Hargeisa - - Havana - - Honduras - - Hong Kong - - Honiara - - Israel - - Jamaica - - Java - - Jeju - - Juba - - Kingston - - Kingstown - - Kosovo - - Kuwait - - Laos - - Liechtenstein - - Lima - - 'Lomé' - - Luanda - - Macau - - Madagascar - - Madeira - - Majuro - - Mali - - Malta - - 'Malé' - - Manama - - Manila - - Maputo - - Maseru - - Montenegro - - Moroni - - Nassau - - Nauru - - Nepal - - Ngerulmud - - Oceania - - Oslo - - Palau - - Paramaribo - - Paris - - Peru - - Podgorica - - Portugal - - Praia - - Pristina - - Quito - - Riga - - Roseau - - Samoa - - Santiago - - Sarajevo - - Senegal - - Sri Lanka - - Sucre - - Sumatra - - Suriname - - Suva - - 'São Tomé' - - Taiwan - - Tegucigalpa - - Timor-Leste - - Tirana - - Togo - - Tonga - - Tuvalu - - 'Tórshavn' - - Uganda - - Vaduz - - Vanuatu - - Venezuela - - Vilnius - - Washington, D.C. - - Wellington - - Willemstad - - Zanzibar + - Abuja + - Alofi + - Andorra + - Angola + - Antananarivo + - Apia + - Argentina + - Aruba + - Asmara + - Astana + - Avarua + - Bali + - Bangui + - Banjul + - Barbados + - Basseterre + - Belize + - Bissau + - 'Bogotá' + - Bougainville + - 'Brasília' + - Bratislava + - Bridgetown + - Brunei + - Buenos Aires + - Burundi + - Cairo + - Caracas + - Castries + - Chile + - China + - Costa Rica + - Cuba + - 'Curaçao' + - Dodoma + - Dominica + - El Salvador + - Fiji + - Freetown + - Funafuti + - Gaborone + - Georgetown + - Gibraltar + - Guam + - Guatemala + - Haiti + - Harare + - Hargeisa + - Havana + - Honduras + - Hong Kong + - Honiara + - Israel + - Jamaica + - Java + - Jeju + - Juba + - Kingston + - Kingstown + - Kosovo + - Kuwait + - Laos + - Liechtenstein + - Lima + - 'Lomé' + - Luanda + - Macau + - Madagascar + - Madeira + - Majuro + - Mali + - Malta + - 'Malé' + - Manama + - Manila + - Maputo + - Maseru + - Montenegro + - Moroni + - Nassau + - Nauru + - Nepal + - Ngerulmud + - Oceania + - Oslo + - Palau + - Paramaribo + - Paris + - Peru + - Podgorica + - Portugal + - Praia + - Pristina + - Quito + - Riga + - Roseau + - Samoa + - Santiago + - Sarajevo + - Senegal + - Sri Lanka + - Sucre + - Sumatra + - Suriname + - Suva + - 'São Tomé' + - Taiwan + - Tegucigalpa + - Timor-Leste + - Tirana + - Togo + - Tonga + - Tuvalu + - 'Tórshavn' + - Uganda + - Vaduz + - Vanuatu + - Venezuela + - Vilnius + - Washington, D.C. + - Wellington + - Willemstad + - Zanzibar target_additions: notes.note.antigua-and-barbuda.fields.field.capital-hint: Estado soberano notes.note.netherlands.fields.field.country-info: Conhecidos informalmente como Holanda. diff --git a/fixtures/ultimate-geography/overlays/languages/sv.yaml b/fixtures/ultimate-geography/overlays/languages/sv.yaml index b9c96bc..3748404 100644 --- a/fixtures/ultimate-geography/overlays/languages/sv.yaml +++ b/fixtures/ultimate-geography/overlays/languages/sv.yaml @@ -394,302 +394,301 @@ translations: wallis-and-futuna: Overseas territory of France.: 'Franskt utomeuropeiskt område.' no_change: - direct: - - Abu Dhabi - - Abuja - - Accra - - Afghanistan - - Alaska - - Alofi - - Amman - - Amsterdam - - Andorra - - Andorra la Vella - - Angola - - Anguilla - - Ankara - - Antananarivo - - Apia - - Argentina - - Aruba - - Asmara - - Astana - - 'Asunción' - - Avarua - - Bahrain - - Baku - - Bali - - Bamako - - Bandar Seri Begawan - - Bangkok - - Bangladesh - - Bangui - - Banjul - - Barbados - - Basseterre - - Beirut - - Belarus - - Belfast - - Belize - - Belmopan - - Benin - - Berlin - - Bermuda - - Bern - - Bhutan - - Bissau - - 'Bogotá' - - Bolivia - - Botswana - - Bougainville - - 'Brasília' - - Bratislava - - Brazzaville - - Bridgetown - - Brunei - - Budapest - - Buenos Aires - - Burkina Faso - - Burundi - - Canberra - - Caracas - - Cardiff - - Castries - - Charlotte Amalie - - Chile - - 'Chișinău' - - Ciudad de la Paz - - Colombia - - Conakry - - Costa Rica - - 'Curaçao' - - Dakar - - Dhaka - - Dili - - Djibouti - - Dodoma - - Doha - - Dominica - - Dublin - - Ecuador - - Edinburgh - - El Salvador - - England - - Eritrea - - Fiji - - Finland - - Freetown - - Funafuti - - Gabon - - Gaborone - - Georgetown - - Ghana - - Gibraltar - - Gitega - - Grenada - - Guadeloupe - - Guam - - Guatemala - - Guatemala City - - Guernsey - - Guinea - - Guinea-Bissau - - Guyana - - 'Hagåtña' - - Haiti - - Hanoi - - Harare - - Hargeisa - - Hawaii - - Honduras - - Honiara - - Hudson Bay - - Iran - - Islamabad - - Isle of Man - - Israel - - Jakarta - - Jamaica - - Japan - - Java - - Jeju - - Jersey - - Jerusalem - - Juba - - Kabul - - Kampala - - Kenya - - Khartoum - - Kigali - - Kingston - - Kingstown - - Kinshasa - - Kiribati - - Kosovo - - Kuala Lumpur - - Kuwait - - Laos - - Lesotho - - Liberia - - Libreville - - Liechtenstein - - Lilongwe - - Lima - - Ljubljana - - 'Lomé' - - London - - Luanda - - Lusaka - - Madeira - - Madrid - - Majuro - - Malawi - - Malaysia - - Mali - - Malta - - 'Malé' - - Managua - - Manama - - Manila - - Maputo - - Mariehamn - - Martinique - - Maseru - - Mauritius - - Mayotte - - Mbabane - - Mexico City - - Minsk - - Mogadishu - - Monaco - - Monrovia - - Montenegro - - Montevideo - - Moroni - - Myanmar - - 'N''Djamena' - - Nairobi - - Namibia - - Nassau - - Nauru - - Naypyidaw - - Nepal - - New Delhi - - Ngerulmud - - Niamey - - Nicaragua - - Nicosia - - Niger - - Nigeria - - Niue - - Nouakchott - - 'Nouméa' - - Nuuk - - Oman - - Oranjestad - - Oslo - - Ottawa - - Ouagadougou - - Pakistan - - Palau - - Palikir - - Panama - - Panama City - - Papeete - - Paraguay - - Paramaribo - - Paris - - Peru - - Phnom Penh - - Podgorica - - Port Louis - - Port Moresby - - Port Vila - - Port of Spain - - Port-au-Prince - - Porto-Novo - - Portugal - - Praia - - Pristina - - Puerto Rico - - Pyongyang - - Qatar - - Quito - - Rabat - - Riga - - Riyadh - - Roseau - - Rwanda - - 'Réunion' - - Saint Lucia - - Saint Martin - - Samoa - - 'San José' - - San Juan - - San Marino - - San Salvador - - Sanaa - - Santo Domingo - - Sarajevo - - Senegal - - Seoul - - Sierra Leone - - Singapore - - Sint Maarten - - Skopje - - Sofia - - Somalia - - Somaliland - - South Tarawa - - Sri Lanka - - Stockholm - - Sucre - - Sudan - - Sumatra - - Suva - - Svalbard - - 'São Tomé' - - Taipei - - Taiwan - - Tallinn - - Tanzania - - Tbilisi - - Tegucigalpa - - Thailand - - Thimphu - - Tirana - - Tiraspol - - Togo - - Tokyo - - Tonga - - Tripoli - - Tunis - - Turkmenistan - - Tuvalu - - 'Tórshavn' - - Uganda - - Uruguay - - Uzbekistan - - Vaduz - - Valletta - - Vanuatu - - Venezuela - - Victoria - - Vientiane - - Vietnam - - Vilnius - - Wales - - Washington, D.C. - - Wellington - - Willemstad - - Windhoek - - Yamoussoukro - - 'Yaoundé' - - Yaren - - Zagreb - - Zambia - - Zanzibar - - Zimbabwe + - Abu Dhabi + - Abuja + - Accra + - Afghanistan + - Alaska + - Alofi + - Amman + - Amsterdam + - Andorra + - Andorra la Vella + - Angola + - Anguilla + - Ankara + - Antananarivo + - Apia + - Argentina + - Aruba + - Asmara + - Astana + - 'Asunción' + - Avarua + - Bahrain + - Baku + - Bali + - Bamako + - Bandar Seri Begawan + - Bangkok + - Bangladesh + - Bangui + - Banjul + - Barbados + - Basseterre + - Beirut + - Belarus + - Belfast + - Belize + - Belmopan + - Benin + - Berlin + - Bermuda + - Bern + - Bhutan + - Bissau + - 'Bogotá' + - Bolivia + - Botswana + - Bougainville + - 'Brasília' + - Bratislava + - Brazzaville + - Bridgetown + - Brunei + - Budapest + - Buenos Aires + - Burkina Faso + - Burundi + - Canberra + - Caracas + - Cardiff + - Castries + - Charlotte Amalie + - Chile + - 'Chișinău' + - Ciudad de la Paz + - Colombia + - Conakry + - Costa Rica + - 'Curaçao' + - Dakar + - Dhaka + - Dili + - Djibouti + - Dodoma + - Doha + - Dominica + - Dublin + - Ecuador + - Edinburgh + - El Salvador + - England + - Eritrea + - Fiji + - Finland + - Freetown + - Funafuti + - Gabon + - Gaborone + - Georgetown + - Ghana + - Gibraltar + - Gitega + - Grenada + - Guadeloupe + - Guam + - Guatemala + - Guatemala City + - Guernsey + - Guinea + - Guinea-Bissau + - Guyana + - 'Hagåtña' + - Haiti + - Hanoi + - Harare + - Hargeisa + - Hawaii + - Honduras + - Honiara + - Hudson Bay + - Iran + - Islamabad + - Isle of Man + - Israel + - Jakarta + - Jamaica + - Japan + - Java + - Jeju + - Jersey + - Jerusalem + - Juba + - Kabul + - Kampala + - Kenya + - Khartoum + - Kigali + - Kingston + - Kingstown + - Kinshasa + - Kiribati + - Kosovo + - Kuala Lumpur + - Kuwait + - Laos + - Lesotho + - Liberia + - Libreville + - Liechtenstein + - Lilongwe + - Lima + - Ljubljana + - 'Lomé' + - London + - Luanda + - Lusaka + - Madeira + - Madrid + - Majuro + - Malawi + - Malaysia + - Mali + - Malta + - 'Malé' + - Managua + - Manama + - Manila + - Maputo + - Mariehamn + - Martinique + - Maseru + - Mauritius + - Mayotte + - Mbabane + - Mexico City + - Minsk + - Mogadishu + - Monaco + - Monrovia + - Montenegro + - Montevideo + - Moroni + - Myanmar + - 'N''Djamena' + - Nairobi + - Namibia + - Nassau + - Nauru + - Naypyidaw + - Nepal + - New Delhi + - Ngerulmud + - Niamey + - Nicaragua + - Nicosia + - Niger + - Nigeria + - Niue + - Nouakchott + - 'Nouméa' + - Nuuk + - Oman + - Oranjestad + - Oslo + - Ottawa + - Ouagadougou + - Pakistan + - Palau + - Palikir + - Panama + - Panama City + - Papeete + - Paraguay + - Paramaribo + - Paris + - Peru + - Phnom Penh + - Podgorica + - Port Louis + - Port Moresby + - Port Vila + - Port of Spain + - Port-au-Prince + - Porto-Novo + - Portugal + - Praia + - Pristina + - Puerto Rico + - Pyongyang + - Qatar + - Quito + - Rabat + - Riga + - Riyadh + - Roseau + - Rwanda + - 'Réunion' + - Saint Lucia + - Saint Martin + - Samoa + - 'San José' + - San Juan + - San Marino + - San Salvador + - Sanaa + - Santo Domingo + - Sarajevo + - Senegal + - Seoul + - Sierra Leone + - Singapore + - Sint Maarten + - Skopje + - Sofia + - Somalia + - Somaliland + - South Tarawa + - Sri Lanka + - Stockholm + - Sucre + - Sudan + - Sumatra + - Suva + - Svalbard + - 'São Tomé' + - Taipei + - Taiwan + - Tallinn + - Tanzania + - Tbilisi + - Tegucigalpa + - Thailand + - Thimphu + - Tirana + - Tiraspol + - Togo + - Tokyo + - Tonga + - Tripoli + - Tunis + - Turkmenistan + - Tuvalu + - 'Tórshavn' + - Uganda + - Uruguay + - Uzbekistan + - Vaduz + - Valletta + - Vanuatu + - Venezuela + - Victoria + - Vientiane + - Vietnam + - Vilnius + - Wales + - Washington, D.C. + - Wellington + - Willemstad + - Windhoek + - Yamoussoukro + - 'Yaoundé' + - Yaren + - Zagreb + - Zambia + - Zanzibar + - Zimbabwe target_additions: notes.note.arabian-sea.fields.field.country-info: 'Även känt som Arabiska sjön.' notes.note.bahrain.fields.field.country-info: Stavas alternativt Bahrein. From 276897803d222b53e1dafab57a911a4b83f206b2 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 17:30:03 +0200 Subject: [PATCH 025/142] add translation coverage summary export --- .../src/commands/translations.rs | 227 +++++++++++++++++- crates/brain-brew-cli/src/help.rs | 2 +- crates/brain-brew-cli/tests/cli.rs | 39 +++ documentation/docs/authoring/translations.md | 3 +- 4 files changed, 264 insertions(+), 7 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 894b9a9..aea957b 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -65,7 +65,15 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { } } - if args.json_output { + if args.summary { + if args.json_output { + print_json_summary(&reports); + } else if reports.is_empty() { + print_no_translation_reports(&args)?; + } else { + print_human_summary(&reports); + } + } else if args.json_output { print_json_reports(&reports, &applied); } else { if reports.is_empty() { @@ -102,6 +110,7 @@ struct TranslationArgs { json_output: bool, interactive: Option, full: bool, + summary: bool, } fn parse_translation_args(args: &[String]) -> Result { @@ -120,6 +129,7 @@ fn parse_translation_args(args: &[String]) -> Result { json_output: false, interactive: None, full: false, + summary: false, }; let mut index = 0; while index < args.len() { @@ -203,6 +213,10 @@ fn parse_translation_args(args: &[String]) -> Result { parsed.full = true; index += 1; } + "--summary" => { + parsed.summary = true; + index += 1; + } "--interactive" => { parsed.interactive = Some(true); index += 1; @@ -220,6 +234,9 @@ fn parse_translation_args(args: &[String]) -> Result { if parsed.interactive == Some(true) && parsed.json_output { return Err("choose --interactive or --json, not both".to_owned()); } + if parsed.summary && parsed.apply { + return Err("choose --summary or --apply, not both".to_owned()); + } Ok(parsed) } @@ -1534,6 +1551,200 @@ fn print_json_reports(reports: &[ScopedTranslationReport], applied: &BTreeMap, + overlay_id: String, + overlay_file: String, + counts: TranslationSummaryCounts, +} + +fn print_json_summary(reports: &[ScopedTranslationReport]) { + let summaries = translation_summary_rows(reports) + .into_iter() + .map(|row| { + json!({ + "language": row.language, + "targets": row.targets.into_iter().collect::>(), + "overlay": row.overlay_id, + "file": row.overlay_file, + "direct_translation": row.counts.direct_translation, + "contextual_override": row.counts.contextual_override, + "no_change": row.counts.no_change, + "target_language_addition": row.counts.target_language_addition, + "variable_translation": row.counts.variable_translation, + "adapter_id_translation": row.counts.adapter_id_translation, + "untranslated_fallback": row.counts.untranslated_fallback, + "missing_text_translation": row.counts.missing_text_translation, + "hidden_untranslated_fallback": row.counts.hidden_untranslated_fallback, + "ignored_source": row.counts.ignored_source, + "stale_invalid": row.counts.stale_invalid, + }) + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "summaries": summaries })) + .expect("translation summary JSON serializes") + ); +} + +fn print_human_summary(reports: &[ScopedTranslationReport]) { + let rows = translation_summary_rows(reports); + println!("{}", color_stdout("Translation coverage summary", "1;36")); + println!( + "language\ttargets\toverlay\tfile\tdirect\tcontextual\tno-change\tadditions\tvariables\tadapter-ids\tmissing-text\thidden-fallbacks\traw-untranslated\tignored\tstale/invalid" + ); + for row in rows { + println!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + row.language, + row.targets.len(), + row.overlay_id, + row.overlay_file, + row.counts.direct_translation, + row.counts.contextual_override, + row.counts.no_change, + row.counts.target_language_addition, + row.counts.variable_translation, + row.counts.adapter_id_translation, + row.counts.missing_text_translation, + row.counts.hidden_untranslated_fallback, + row.counts.untranslated_fallback, + row.counts.ignored_source, + row.counts.stale_invalid, + ); + } +} + +fn translation_summary_rows(reports: &[ScopedTranslationReport]) -> Vec { + let mut grouped = + BTreeMap::<(String, String, String, TranslationSummaryCounts), BTreeSet>::new(); + for report in reports { + let language = inferred_report_language(report); + let counts = translation_summary_counts(&report.report.entries); + grouped + .entry(( + language, + report.overlay_id.clone(), + report.overlay_file.clone(), + counts, + )) + .or_default() + .insert(report.target.clone()); + } + grouped + .into_iter() + .map( + |((language, overlay_id, overlay_file, counts), targets)| TranslationSummaryRow { + language, + targets, + overlay_id, + overlay_file, + counts, + }, + ) + .collect() +} + +fn translation_summary_counts(entries: &[TranslationCoverageEntry]) -> TranslationSummaryCounts { + let counts = category_counts(entries); + let untranslated_fallback = counts.get("untranslated_fallback").copied().unwrap_or(0); + let missing_text_translation = entries + .iter() + .filter(|entry| { + entry.category == TranslationCoverageCategory::UntranslatedFallback + && default_report_includes_untranslated_fallback(entry) + }) + .count(); + TranslationSummaryCounts { + direct_translation: counts.get("direct_translation").copied().unwrap_or(0), + contextual_override: counts.get("contextual_override").copied().unwrap_or(0), + no_change: counts.get("no_change").copied().unwrap_or(0), + target_language_addition: counts.get("target_language_addition").copied().unwrap_or(0), + variable_translation: counts.get("variable_translation").copied().unwrap_or(0), + adapter_id_translation: counts.get("adapter_id_translation").copied().unwrap_or(0), + untranslated_fallback, + missing_text_translation, + hidden_untranslated_fallback: untranslated_fallback + .saturating_sub(missing_text_translation), + ignored_source: counts.get("ignored_source").copied().unwrap_or(0), + stale_invalid: entries + .iter() + .filter(|entry| is_stale_or_invalid(entry.category)) + .count(), + } +} + +fn inferred_report_language(report: &ScopedTranslationReport) -> String { + let stem = Path::new(&report.overlay_file) + .file_stem() + .and_then(|stem| stem.to_str()); + if let Some(language) = language_candidate(stem) { + return language; + } + if let Some(language) = stem + .and_then(|stem| stem.strip_prefix("translation-")) + .and_then(|candidate| language_candidate(Some(candidate))) + { + return language; + } + if let Some((_, suffix)) = report.overlay_id.rsplit_once(".translation.") { + if let Some(language) = language_candidate(Some(suffix)) { + return language; + } + if let Some(language) = suffix + .rsplit_once('.') + .and_then(|(_, last_segment)| language_candidate(Some(last_segment))) + { + return language; + } + } + if let Some(language) = language_candidate(Some(trim_target_language_suffixes(&report.target))) + { + return language; + } + if let Some(language) = stem + .and_then(|stem| stem.rsplit_once(['-', '.'])) + .and_then(|(_, suffix)| language_candidate(Some(suffix))) + { + return language; + } + "unknown".to_owned() +} + +fn trim_target_language_suffixes(target: &str) -> &str { + let mut candidate = target; + loop { + let Some((prefix, suffix)) = candidate.rsplit_once('-') else { + return candidate; + }; + if matches!( + suffix, + "standard" | "extended" | "experimental" | "hardcore" | "release" + ) { + candidate = prefix; + } else { + return candidate; + } + } +} + fn entry_json(entry: &TranslationCoverageEntry) -> serde_json::Value { json!({ "category": entry.category.as_str(), @@ -1810,16 +2021,22 @@ fn inferred_languages(manifest: &FederatedDeckManifest) -> Vec { } fn maybe_insert_language(candidate: Option<&str>, languages: &mut BTreeSet) { - let Some(language) = candidate else { - return; - }; + if let Some(language) = language_candidate(candidate) { + languages.insert(language); + } +} + +fn language_candidate(candidate: Option<&str>) -> Option { + let language = candidate?; if (2..=8).contains(&language.len()) && language .chars() .all(|ch| ch.is_ascii_lowercase() || ch == '-') && language != "translation" { - languages.insert(language.to_owned()); + Some(language.to_owned()) + } else { + None } } diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index e3c302b..7462de1 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -56,7 +56,7 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", ), "translations" | "translate" | "translation" => Some( - "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --apply inserts source->source translation stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (mark no-change, direct/contextual stub, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", + "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply | --summary] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --summary exports compact per-language/per-overlay counts and de-duplicates identical reports across target variants. --apply inserts source->source translation stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (mark no-change, direct/contextual stub, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", ), "verify" => Some( "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 51cb466..4610d7e 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -975,6 +975,45 @@ fn translations_reports_when_selected_target_has_no_dictionary_overlays() { assert!(out.contains("do not use a `translations:` dictionary")); } +#[test] +fn translations_summary_exports_compact_counts_by_language() { + let dir = temp_dir("translations-summary"); + write_translation_workspace(&dir); + + let output = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--all-targets", + "--summary", + "--json", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let json: serde_json::Value = serde_json::from_str(&stdout(&output)).unwrap(); + assert!(json.get("reports").is_none()); + let summaries = json["summaries"].as_array().unwrap(); + assert_eq!(summaries.len(), 1); + let row = &summaries[0]; + assert_eq!(row["language"], "da"); + assert_eq!( + row["targets"], + serde_json::json!(["da-release", "da-standard"]) + ); + assert_eq!(row["overlay"], "overlay.translation.da"); + assert_eq!(row["file"], "da.yaml"); + assert_eq!(row["direct_translation"], 1); + assert_eq!(row["contextual_override"], 1); + assert_eq!(row["no_change"], 0); + assert_eq!(row["target_language_addition"], 1); + assert_eq!(row["variable_translation"], 0); + assert_eq!(row["adapter_id_translation"], 0); + assert_eq!(row["untranslated_fallback"], 2); + assert_eq!(row["missing_text_translation"], 2); + assert_eq!(row["hidden_untranslated_fallback"], 0); + assert_eq!(row["stale_invalid"], 1); +} + #[test] fn translations_default_report_focuses_on_translatable_note_text() { let manifest = workspace_root().join("fixtures/ultimate-geography/brainbrew.yaml"); diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index d05e1a2..f6b5829 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -160,10 +160,11 @@ brainbrew translations --manifest brainbrew.yaml --all-targets --language de brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.berlin brainbrew translations --manifest brainbrew.yaml --target de-standard --path-prefix notes.note.berlin.fields.field.country brainbrew translations --manifest brainbrew.yaml --target de-standard --json +brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json brainbrew translations --manifest brainbrew.yaml --target de-standard --full ``` -Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. +Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Use `--summary` for compact per-language/per-overlay counts; summary mode de-duplicates identical reports across target variants and includes direct translations, contextual overrides, no-change entries, target-language additions, variables, adapter IDs, raw untranslated fallbacks, actionable missing text translations, ignored entries, and stale/invalid keys. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. To seed translator work after adding English notes or fields, run apply explicitly: From 7d0fd923e2dc3f6b3fdfa445c658e5795da94edf Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 17:48:08 +0200 Subject: [PATCH 026/142] align translation summary table output --- .../src/commands/translations.rs | 85 +++++++++++++++---- crates/brain-brew-cli/tests/cli.rs | 39 +++++++++ 2 files changed, 106 insertions(+), 18 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index aea957b..d25b568 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -1607,31 +1607,80 @@ fn print_json_summary(reports: &[ScopedTranslationReport]) { fn print_human_summary(reports: &[ScopedTranslationReport]) { let rows = translation_summary_rows(reports); println!("{}", color_stdout("Translation coverage summary", "1;36")); - println!( - "language\ttargets\toverlay\tfile\tdirect\tcontextual\tno-change\tadditions\tvariables\tadapter-ids\tmissing-text\thidden-fallbacks\traw-untranslated\tignored\tstale/invalid" - ); + let mut table = vec![vec![ + "language".to_owned(), + "targets".to_owned(), + "overlay".to_owned(), + "file".to_owned(), + "direct".to_owned(), + "contextual".to_owned(), + "no-change".to_owned(), + "additions".to_owned(), + "variables".to_owned(), + "adapter-ids".to_owned(), + "missing-text".to_owned(), + "hidden-fallbacks".to_owned(), + "raw-untranslated".to_owned(), + "ignored".to_owned(), + "stale/invalid".to_owned(), + ]]; for row in rows { - println!( - "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + table.push(vec![ row.language, - row.targets.len(), + row.targets.len().to_string(), row.overlay_id, row.overlay_file, - row.counts.direct_translation, - row.counts.contextual_override, - row.counts.no_change, - row.counts.target_language_addition, - row.counts.variable_translation, - row.counts.adapter_id_translation, - row.counts.missing_text_translation, - row.counts.hidden_untranslated_fallback, - row.counts.untranslated_fallback, - row.counts.ignored_source, - row.counts.stale_invalid, - ); + row.counts.direct_translation.to_string(), + row.counts.contextual_override.to_string(), + row.counts.no_change.to_string(), + row.counts.target_language_addition.to_string(), + row.counts.variable_translation.to_string(), + row.counts.adapter_id_translation.to_string(), + row.counts.missing_text_translation.to_string(), + row.counts.hidden_untranslated_fallback.to_string(), + row.counts.untranslated_fallback.to_string(), + row.counts.ignored_source.to_string(), + row.counts.stale_invalid.to_string(), + ]); + } + let right_aligned = [ + false, true, false, false, true, true, true, true, true, true, true, true, true, true, true, + ]; + for line in aligned_table_lines(&table, &right_aligned) { + println!("{line}"); } } +fn aligned_table_lines(rows: &[Vec], right_aligned: &[bool]) -> Vec { + let column_count = rows.iter().map(Vec::len).max().unwrap_or(0); + let mut widths = vec![0; column_count]; + for row in rows { + for (index, cell) in row.iter().enumerate() { + widths[index] = widths[index].max(cell.chars().count()); + } + } + rows.iter() + .map(|row| { + let mut line = String::new(); + for (index, width) in widths.iter().enumerate() { + if index > 0 { + line.push_str(" "); + } + let cell = row.get(index).map(String::as_str).unwrap_or_default(); + let padding = width.saturating_sub(cell.chars().count()); + if right_aligned.get(index).copied().unwrap_or(false) { + line.push_str(&" ".repeat(padding)); + line.push_str(cell); + } else { + line.push_str(cell); + line.push_str(&" ".repeat(padding)); + } + } + line.trim_end().to_owned() + }) + .collect() +} + fn translation_summary_rows(reports: &[ScopedTranslationReport]) -> Vec { let mut grouped = BTreeMap::<(String, String, String, TranslationSummaryCounts), BTreeSet>::new(); diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 4610d7e..48308f1 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -1014,6 +1014,45 @@ fn translations_summary_exports_compact_counts_by_language() { assert_eq!(row["stale_invalid"], 1); } +#[test] +fn translations_summary_human_output_uses_aligned_space_columns() { + let dir = temp_dir("translations-summary-human"); + write_translation_workspace(&dir); + + let output = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--all-targets", + "--summary", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!( + !out.contains('\t'), + "summary should not rely on terminal tab stops:\n{out}" + ); + assert!( + out.contains("language targets"), + "header should use padded columns:\n{out}" + ); + let mut lines = out.lines(); + let _title = lines.next().unwrap(); + let header = lines.next().unwrap(); + let row = lines.next().unwrap(); + assert_eq!( + row.find("overlay.translation.da"), + header.find("overlay"), + "overlay column should start under its header:\n{out}" + ); + assert_eq!( + row.find("da.yaml"), + header.find("file"), + "file column should start under its header:\n{out}" + ); +} + #[test] fn translations_default_report_focuses_on_translatable_note_text() { let manifest = workspace_root().join("fixtures/ultimate-geography/brainbrew.yaml"); From 8bb4dc7f2393b1c21cf21dc5ce7368c454fc5e7c Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 17:54:42 +0200 Subject: [PATCH 027/142] compact translation summary table --- .../src/commands/translations.rs | 55 ++++++++++++++++++- crates/brain-brew-cli/src/help.rs | 2 +- crates/brain-brew-cli/tests/cli.rs | 38 ++++++++----- documentation/docs/authoring/translations.md | 2 +- 4 files changed, 78 insertions(+), 19 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index d25b568..9f9e53f 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -71,7 +71,7 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { } else if reports.is_empty() { print_no_translation_reports(&args)?; } else { - print_human_summary(&reports); + print_human_summary(&reports, args.full); } } else if args.json_output { print_json_reports(&reports, &applied); @@ -1604,9 +1604,60 @@ fn print_json_summary(reports: &[ScopedTranslationReport]) { ); } -fn print_human_summary(reports: &[ScopedTranslationReport]) { +fn print_human_summary(reports: &[ScopedTranslationReport], full: bool) { let rows = translation_summary_rows(reports); println!("{}", color_stdout("Translation coverage summary", "1;36")); + if full { + print_full_human_summary_table(rows); + } else { + print_compact_human_summary_table(rows); + println!("Legend: ctxt=contextual, same=no-change, miss=actionable missing text."); + println!(" hidden/raw=untranslated fallbacks; --summary --full shows overlay/file."); + } +} + +fn print_compact_human_summary_table(rows: Vec) { + let mut table = vec![vec![ + "lang".to_owned(), + "tgt".to_owned(), + "direct".to_owned(), + "ctxt".to_owned(), + "same".to_owned(), + "add".to_owned(), + "vars".to_owned(), + "ids".to_owned(), + "miss".to_owned(), + "hidden".to_owned(), + "raw".to_owned(), + "ign".to_owned(), + "stale".to_owned(), + ]]; + for row in rows { + table.push(vec![ + row.language, + row.targets.len().to_string(), + row.counts.direct_translation.to_string(), + row.counts.contextual_override.to_string(), + row.counts.no_change.to_string(), + row.counts.target_language_addition.to_string(), + row.counts.variable_translation.to_string(), + row.counts.adapter_id_translation.to_string(), + row.counts.missing_text_translation.to_string(), + row.counts.hidden_untranslated_fallback.to_string(), + row.counts.untranslated_fallback.to_string(), + row.counts.ignored_source.to_string(), + row.counts.stale_invalid.to_string(), + ]); + } + let right_aligned = [ + false, true, true, true, true, true, true, true, true, true, true, true, true, + ]; + for line in aligned_table_lines(&table, &right_aligned) { + println!("{line}"); + } +} + +fn print_full_human_summary_table(rows: Vec) { let mut table = vec![vec![ "language".to_owned(), "targets".to_owned(), diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index 7462de1..1052b82 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -56,7 +56,7 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", ), "translations" | "translate" | "translation" => Some( - "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply | --summary] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --summary exports compact per-language/per-overlay counts and de-duplicates identical reports across target variants. --apply inserts source->source translation stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (mark no-change, direct/contextual stub, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", + "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply | --summary] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --summary exports compact per-language/per-overlay counts and de-duplicates identical reports across target variants; human summary output uses narrow aligned columns by default, and --summary --full adds overlay/file columns. --apply inserts source->source translation stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (mark no-change, direct/contextual stub, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", ), "verify" => Some( "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 48308f1..23c646e 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -1034,23 +1034,31 @@ fn translations_summary_human_output_uses_aligned_space_columns() { "summary should not rely on terminal tab stops:\n{out}" ); assert!( - out.contains("language targets"), - "header should use padded columns:\n{out}" + out.contains("lang tgt direct"), + "header should use compact padded columns:\n{out}" ); - let mut lines = out.lines(); - let _title = lines.next().unwrap(); - let header = lines.next().unwrap(); - let row = lines.next().unwrap(); - assert_eq!( - row.find("overlay.translation.da"), - header.find("overlay"), - "overlay column should start under its header:\n{out}" - ); - assert_eq!( - row.find("da.yaml"), - header.find("file"), - "file column should start under its header:\n{out}" + assert!( + !out.contains("overlay.translation.da"), + "default summary should keep wide overlay/file columns out of the table:\n{out}" ); + for line in out.lines().skip(1) { + assert!( + line.chars().count() <= 90, + "default summary line should stay compact, got {} chars:\n{line}\n\n{out}", + line.chars().count() + ); + } + let full = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--all-targets", + "--summary", + "--full", + ]); + assert!(full.status.success(), "stderr: {}", stderr(&full)); + assert!(stdout(&full).contains("overlay.translation.da")); + assert!(stdout(&full).contains("da.yaml")); } #[test] diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index f6b5829..a9cd148 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -164,7 +164,7 @@ brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json brainbrew translations --manifest brainbrew.yaml --target de-standard --full ``` -Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Use `--summary` for compact per-language/per-overlay counts; summary mode de-duplicates identical reports across target variants and includes direct translations, contextual overrides, no-change entries, target-language additions, variables, adapter IDs, raw untranslated fallbacks, actionable missing text translations, ignored entries, and stale/invalid keys. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. +Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Use `--summary` for compact per-language/per-overlay counts; summary mode de-duplicates identical reports across target variants and includes direct translations, contextual overrides, no-change entries, target-language additions, variables, adapter IDs, raw untranslated fallbacks, actionable missing text translations, ignored entries, and stale/invalid keys. Human summary output uses narrow aligned columns by default; add `--summary --full` to include overlay/file columns, or `--summary --json` for complete machine-readable metadata. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. To seed translator work after adding English notes or fields, run apply explicitly: From feb51d425c2e651107dce71419bf40d5103d8977 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 18:29:04 +0200 Subject: [PATCH 028/142] add translator context view --- .../src/commands/translations.rs | 423 +++++++++++++++++- crates/brain-brew-cli/src/help.rs | 2 +- crates/brain-brew-cli/tests/cli.rs | 171 +++++++ crates/brain-brew-core/src/lib.rs | 257 +++++++++++ .../brain-brew-core/tests/overlay_compose.rs | 71 ++- documentation/docs/authoring/translations.md | 7 +- 6 files changed, 917 insertions(+), 14 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 9f9e53f..0ac012d 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -5,8 +5,8 @@ use std::io::{self, IsTerminal, Read, Write}; use std::path::{Path, PathBuf}; use brain_brew_core::{ - CanonicalDeck, Overlay, OverlayKind, TranslationCoverageCategory, TranslationCoverageEntry, - TranslationCoverageReport, + CanonicalDeck, Overlay, OverlayKind, TranslationContextUnit, TranslationContextView, + TranslationCoverageCategory, TranslationCoverageEntry, TranslationCoverageReport, }; use brain_brew_formats::manifest::FederatedDeckManifest; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; @@ -65,7 +65,15 @@ pub(crate) fn run(args: &[String]) -> Result<(), String> { } } - if args.summary { + if args.context { + if args.json_output { + print_json_contexts(&reports); + } else if reports.is_empty() { + print_no_translation_reports(&args)?; + } else { + print_human_contexts(&reports, args.full); + } + } else if args.summary { if args.json_output { print_json_summary(&reports); } else if reports.is_empty() { @@ -111,6 +119,10 @@ struct TranslationArgs { interactive: Option, full: bool, summary: bool, + context: bool, + source: Option, + duplicates: bool, + status: Option, } fn parse_translation_args(args: &[String]) -> Result { @@ -130,6 +142,10 @@ fn parse_translation_args(args: &[String]) -> Result { interactive: None, full: false, summary: false, + context: false, + source: None, + duplicates: false, + status: None, }; let mut index = 0; while index < args.len() { @@ -217,6 +233,29 @@ fn parse_translation_args(args: &[String]) -> Result { parsed.summary = true; index += 1; } + "--context" => { + parsed.context = true; + index += 1; + } + "--source" => { + let Some(source) = args.get(index + 1) else { + return Err("--source requires text to match".to_owned()); + }; + parsed.source = Some(source.clone()); + index += 2; + } + "--duplicates" => { + parsed.duplicates = true; + index += 1; + } + "--status" => { + let Some(status) = args.get(index + 1) else { + return Err("--status requires a status filter".to_owned()); + }; + validate_status_filter(status)?; + parsed.status = Some(status.clone()); + index += 2; + } "--interactive" => { parsed.interactive = Some(true); index += 1; @@ -237,6 +276,9 @@ fn parse_translation_args(args: &[String]) -> Result { if parsed.summary && parsed.apply { return Err("choose --summary or --apply, not both".to_owned()); } + if parsed.summary && parsed.context { + return Err("choose --summary or --context, not both".to_owned()); + } Ok(parsed) } @@ -818,6 +860,7 @@ struct ScopedTranslationReport { overlay_file: String, overlay_path: PathBuf, report: TranslationCoverageReport, + context: TranslationContextView, } fn collect_translation_reports( @@ -840,20 +883,28 @@ fn collect_translation_reports( if overlay_matches_scope(target, planned, overlay, args) && let Some(report) = current.translation_coverage(overlay) { - let entries = report + let full_context = current.translation_context(&report); + let mut entries = report .entries - .into_iter() + .iter() .filter(|entry| entry_matches_scope(entry, args)) + .cloned() .collect::>(); + if args.duplicates { + retain_duplicate_source_entries(&mut entries); + } + let scoped_report = TranslationCoverageReport { + overlay_id: report.overlay_id, + entries, + }; + let context = filter_context_to_report(full_context, &scoped_report); reports.push(ScopedTranslationReport { target: target.clone(), overlay_id: planned.id.clone(), overlay_file: planned.display_file.clone(), overlay_path: planned.file.clone(), - report: TranslationCoverageReport { - overlay_id: report.overlay_id, - entries, - }, + report: scoped_report, + context, }); } current = compose_lenient_translation_overlay(¤t, overlay)?; @@ -1188,7 +1239,40 @@ fn language_matches( .any(|part| part == language || part == format!("{language}.yaml")) } +fn filter_context_to_report( + mut context: TranslationContextView, + report: &TranslationCoverageReport, +) -> TranslationContextView { + context.units.retain(|unit| { + report.entries.iter().any(|entry| { + entry.path == unit.path + && entry.category == unit.category + && entry.source == unit.source + && entry.context.as_deref() == unit.context.as_deref() + }) + }); + context +} + +fn retain_duplicate_source_entries(entries: &mut Vec) { + let mut counts = BTreeMap::::new(); + for entry in entries.iter().filter(|entry| !entry.source.is_empty()) { + *counts.entry(entry.source.clone()).or_insert(0) += 1; + } + entries.retain(|entry| counts.get(&entry.source).copied().unwrap_or(0) > 1); +} + fn entry_matches_scope(entry: &TranslationCoverageEntry, args: &TranslationArgs) -> bool { + if let Some(source) = &args.source + && !entry.source.contains(source) + { + return false; + } + if let Some(status) = &args.status + && !status_matches_filter(entry.category, status) + { + return false; + } if let Some(note) = &args.note { let full = if note.starts_with("note.") { note.clone() @@ -1528,6 +1612,87 @@ fn is_stale_or_invalid(category: TranslationCoverageCategory) -> bool { ) } +fn validate_status_filter(status: &str) -> Result<(), String> { + if status_filter_is_known(status) { + Ok(()) + } else { + Err(format!( + "invalid translation status {status:?}; expected missing, stale, translated, changed, direct, contextual, no-change, addition, variable, adapter-id, ignored, or a coverage category name" + )) + } +} + +fn status_filter_is_known(status: &str) -> bool { + let normalized = normalized_status_filter(status); + matches!( + normalized.as_str(), + "missing" + | "untranslated" + | "stale" + | "invalid" + | "translated" + | "covered" + | "changed" + | "direct" + | "contextual" + | "no_change" + | "addition" + | "variable" + | "adapter_id" + | "ignored" + | "direct_translation" + | "contextual_override" + | "target_language_addition" + | "variable_translation" + | "adapter_id_translation" + | "ignored_source" + | "untranslated_fallback" + | "stale_direct_key" + | "stale_contextual_key" + | "stale_no_change_key" + | "stale_target_addition" + | "stale_variable_key" + | "stale_adapter_id_key" + | "invalid_target_addition" + ) +} + +fn status_matches_filter(category: TranslationCoverageCategory, status: &str) -> bool { + let normalized = normalized_status_filter(status); + match normalized.as_str() { + "missing" | "untranslated" => category == TranslationCoverageCategory::UntranslatedFallback, + "stale" | "invalid" => is_stale_or_invalid(category), + "translated" | "covered" => matches!( + category, + TranslationCoverageCategory::DirectTranslation + | TranslationCoverageCategory::ContextualOverride + | TranslationCoverageCategory::NoChange + | TranslationCoverageCategory::TargetLanguageAddition + | TranslationCoverageCategory::VariableTranslation + | TranslationCoverageCategory::AdapterIdTranslation + ), + "changed" => matches!( + category, + TranslationCoverageCategory::DirectTranslation + | TranslationCoverageCategory::ContextualOverride + | TranslationCoverageCategory::TargetLanguageAddition + | TranslationCoverageCategory::VariableTranslation + ), + "direct" => category == TranslationCoverageCategory::DirectTranslation, + "contextual" => category == TranslationCoverageCategory::ContextualOverride, + "no_change" => category == TranslationCoverageCategory::NoChange, + "addition" => category == TranslationCoverageCategory::TargetLanguageAddition, + "variable" => category == TranslationCoverageCategory::VariableTranslation, + "adapter_id" => category == TranslationCoverageCategory::AdapterIdTranslation, + "ignored" => category == TranslationCoverageCategory::IgnoredSource, + other => category.as_str() == other, + } +} + +fn normalized_status_filter(status: &str) -> String { + status.trim().to_ascii_lowercase().replace(['-', ' '], "_") +} + fn print_json_reports(reports: &[ScopedTranslationReport], applied: &BTreeMap) { let reports_json = reports .iter() @@ -1551,6 +1716,246 @@ fn print_json_reports(reports: &[ScopedTranslationReport], applied: &BTreeMap>(), + }) + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&json!({ "contexts": contexts_json })) + .expect("translation context JSON serializes") + ); +} + +fn context_unit_json(unit: &TranslationContextUnit) -> serde_json::Value { + json!({ + "status": unit.category.as_str(), + "path": unit.path, + "source": unit.source, + "translated": unit.translated, + "context": unit.context, + "note_id": unit.note_id.as_ref().map(|id| id.as_str()), + "note_type_id": unit.note_type_id.as_ref().map(|id| id.as_str()), + "field_id": unit.field_id.as_ref().map(|id| id.as_str()), + "field_name": unit.field_name, + "source_occurrences": unit.source_occurrences, + "note_fields": unit.note_fields.iter().map(|field| json!({ + "field_id": field.field_id.as_str(), + "field_name": field.field_name, + "source": field.source, + "translated": field.translated, + "status": field.category.map(|category| category.as_str()), + })).collect::>(), + "card_templates": unit.card_templates.iter().map(|card| json!({ + "template_id": card.template_id.as_str(), + "template_name": card.template_name, + "sides": card.sides.iter().map(|side| side.as_str()).collect::>(), + })).collect::>(), + }) +} + +fn print_human_contexts(reports: &[ScopedTranslationReport], full: bool) { + for report in reports { + let language = inferred_report_language(report); + let units = report + .context + .units + .iter() + .filter(|unit| full || default_context_includes_unit(unit)) + .collect::>(); + println!( + "{}", + color_stdout( + &format!( + "Translation context for target {} language {} overlay {} ({})", + report.target, language, report.overlay_id, report.overlay_file + ), + "1;36" + ) + ); + if units.is_empty() { + println!(" no context units matched this scope"); + continue; + } + for unit in units { + print_context_unit(unit, &language); + } + } +} + +fn default_context_includes_unit(unit: &TranslationContextUnit) -> bool { + is_stale_or_invalid(unit.category) + || (unit.note_id.is_some() + && unit.field_id.is_some() + && !is_media_or_structural_field_path(&unit.path) + && !looks_like_media_markup(&unit.source)) +} + +fn print_context_unit(unit: &TranslationContextUnit, language: &str) { + println!( + " - {} {}", + color_category(unit.category, display_category_name(unit.category)), + unit.path + ); + if unit.source_occurrences > 1 { + println!( + " duplicate source group: {} occurrence(s) of {}", + unit.source_occurrences, + yaml_scalar(&unit.source) + ); + } + if let Some(note_id) = &unit.note_id { + println!(" note: {note_id}"); + } + if let Some(note_type_id) = &unit.note_type_id { + println!(" note type: {note_type_id}"); + } + if let Some(field_id) = &unit.field_id { + let field_name = unit + .field_name + .as_ref() + .map(|name| format!(" ({name})")) + .unwrap_or_default(); + println!(" field: {field_id}{field_name}"); + } + if !unit.note_fields.is_empty() { + print_note_field_context(unit, language); + } + if let Some(context) = &unit.context { + println!(" dictionary context: {context}"); + } + if !unit.card_templates.is_empty() { + let cards = unit + .card_templates + .iter() + .map(|card| { + let sides = card + .sides + .iter() + .map(|side| side.as_str()) + .collect::>() + .join("+"); + format!("{} [{}]", card.template_id, sides) + }) + .collect::>() + .join(", "); + println!(" cards: {cards}"); + } + for line in side_by_side_lines( + "source/en", + &unit.source, + &format!("target/{language}"), + unit.translated.as_deref().unwrap_or(""), + ) { + println!(" {line}"); + } +} + +fn print_note_field_context(unit: &TranslationContextUnit, language: &str) { + println!(" note fields (source/en | target/{language}):"); + for field in unit.note_fields.iter().filter(|field| { + !field.source.is_empty() + && !looks_like_media_markup(&field.source) + && !is_media_or_structural_field_id(field.field_id.as_str()) + }) { + let marker = if unit.field_id.as_ref() == Some(&field.field_id) { + "*" + } else { + " " + }; + println!( + " {marker} {:<24} {} | {}", + format!("{} ({})", field.field_id, field.field_name), + compact_context_value(&field.source), + compact_context_value(&field.translated) + ); + } +} + +fn is_media_or_structural_field_id(field_id: &str) -> bool { + field_id == "field.flag" || field_id == "field.map" +} + +fn compact_context_value(value: &str) -> String { + let collapsed = value.split_whitespace().collect::>().join(" "); + const LIMIT: usize = 42; + if collapsed.chars().count() <= LIMIT { + collapsed + } else { + let mut truncated = collapsed + .chars() + .take(LIMIT.saturating_sub(1)) + .collect::(); + truncated.push('…'); + truncated + } +} + +fn side_by_side_lines( + left_header: &str, + left: &str, + right_header: &str, + right: &str, +) -> Vec { + const COLUMN_WIDTH: usize = 38; + let mut lines = vec![format!( + "{left_header: Vec { + let mut lines = Vec::new(); + for raw_line in text.lines().flat_map(|line| line.split("\n")) { + let mut current = String::new(); + for word in raw_line.split_whitespace() { + let separator = usize::from(!current.is_empty()); + if current.chars().count() + separator + word.chars().count() > width + && !current.is_empty() + { + lines.push(current); + current = String::new(); + } + if !current.is_empty() { + current.push(' '); + } + current.push_str(word); + } + if !current.is_empty() { + lines.push(current); + } + } + if lines.is_empty() { + lines.push(String::new()); + } + lines +} + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] struct TranslationSummaryCounts { direct_translation: usize, diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index 1052b82..d5a7ca9 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -56,7 +56,7 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "Usage:\n brainbrew targets [--manifest brainbrew.yaml] [--include package/brainbrew.yaml ...] [--package-root packages/] [--json]\n\nExamples:\n brainbrew targets --manifest brainbrew.yaml\n brainbrew targets --manifest brainbrew.yaml --json\n brainbrew targets --package-root ../packages\n", ), "translations" | "translate" | "translation" => Some( - "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--path-prefix ] [--apply | --summary] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --summary exports compact per-language/per-overlay counts and de-duplicates identical reports across target variants; human summary output uses narrow aligned columns by default, and --summary --full adds overlay/file columns. --apply inserts source->source translation stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (mark no-change, direct/contextual stub, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", + "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--source ] [--duplicates] [--status ] [--path-prefix ] [--apply | --summary | --context] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target de-standard --context --source Georgia\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --context shows source and target strings in note/field/card context; combine it with --note, --field, --source, --duplicates, --status, and --language to navigate the translator context view. --summary exports compact per-language/per-overlay counts and de-duplicates identical reports across target variants; human summary output uses narrow aligned columns by default, and --summary --full adds overlay/file columns. --apply inserts source->source translation stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (mark no-change, direct/contextual stub, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", ), "verify" => Some( "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 23c646e..f9b2d38 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -975,6 +975,177 @@ fn translations_reports_when_selected_target_has_no_dictionary_overlays() { assert!(out.contains("do not use a `translations:` dictionary")); } +#[test] +fn translations_context_view_shows_missing_note_field_card_context() { + let dir = temp_dir("translations-context-missing"); + write_translation_workspace(&dir); + + let output = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--context", + "--source", + "Sweden", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("Translation context for target da-standard language da")); + assert!(out.contains("missing_translation notes.note.sweden.fields.field.country")); + assert!(out.contains("note: note.sweden")); + assert!(out.contains("field: field.country (Country)")); + assert!(out.contains("note fields (source/en | target/da):")); + assert!(out.contains("* field.country (Country)")); + assert!(out.contains("cards: template.country-capital [question]")); + assert!(out.contains("source/en")); + assert!(out.contains("target/da")); + assert!(out.contains("Sweden")); +} + +#[test] +fn translations_context_view_shows_ug_repeated_country_info_occurrences() { + let manifest = workspace_root().join("fixtures/ultimate-geography/brainbrew.yaml"); + let output = run([ + "translations", + "--manifest", + manifest.to_str().unwrap(), + "--target", + "es-standard", + "--overlay", + "overlay.translation.es", + "--context", + "--source", + "Island of Indonesia.", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("duplicate source group: 3 occurrence(s)")); + assert!(out.contains("note: note.bali")); + assert!(out.contains("note: note.java")); + assert!(out.contains("note: note.sumatra")); + assert!(out.contains("field: field.country-info (Country info)")); + assert!(out.contains("template.capital-country [answer]")); + assert!(out.contains("template.country-capital [question+answer]")); + assert!(out.contains("Island of Indonesia.")); + assert!(out.contains("Isla de Indonesia.")); +} + +#[test] +fn translations_context_view_wraps_long_ug_flag_similarity_context() { + let manifest = workspace_root().join("fixtures/ultimate-geography/brainbrew.yaml"); + let output = run([ + "translations", + "--manifest", + manifest.to_str().unwrap(), + "--target", + "es-standard", + "--overlay", + "overlay.translation.es", + "--context", + "--source", + "Iceland (blue background", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("note: note.faroe-islands")); + assert!(out.contains("field: field.flag-similarity (Flag similarity)")); + assert!(out.contains("template.flag-country [answer]")); + assert!(out.contains("Iceland (blue background,")); + assert!(out.contains("Noruega (fondo rojo, cruz")); +} + +#[test] +fn translations_context_view_json_exposes_reusable_context_model() { + let manifest = workspace_root().join("fixtures/ultimate-geography/brainbrew.yaml"); + let output = run([ + "translations", + "--manifest", + manifest.to_str().unwrap(), + "--target", + "es-standard", + "--overlay", + "overlay.translation.es", + "--context", + "--source", + "Autonomous community of Spain.", + "--json", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let json: serde_json::Value = serde_json::from_str(&stdout(&output)).unwrap(); + let unit = &json["contexts"][0]["units"][0]; + assert_eq!(unit["status"], "direct_translation"); + assert_eq!(unit["note_id"], "note.canary-islands"); + assert_eq!(unit["field_id"], "field.country-info"); + assert_eq!(unit["field_name"], "Country info"); + assert_eq!(unit["note_fields"][0]["field_id"], "field.country"); + assert_eq!(unit["note_fields"][0]["source"], "Canary Islands"); + assert_eq!(unit["note_fields"][0]["translated"], "Canarias"); + assert_eq!(unit["source"], "Autonomous community of Spain."); + assert_eq!(unit["translated"], "Comunidad autónoma de España."); + assert!(unit["card_templates"].as_array().unwrap().len() >= 4); +} + +#[test] +fn translations_context_apply_uses_existing_translation_apply_path() { + let dir = temp_dir("translations-context-apply"); + write_translation_workspace(&dir); + let overlay_path = dir.join("da.yaml"); + + let output = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--context", + "--source", + "Sweden", + "--apply", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let updated = fs::read_to_string(overlay_path).unwrap(); + assert!(updated.contains(" Sweden: Sweden\n")); +} + +#[test] +fn translations_context_view_can_filter_duplicate_missing_and_language() { + let dir = temp_dir("translations-context-filters"); + write_translation_workspace(&dir); + let deck_path = dir.join("deck.yaml"); + let deck = fs::read_to_string(&deck_path) + .unwrap() + .replace("field.country: Finland", "field.country: Sweden"); + fs::write(&deck_path, deck).unwrap(); + + let output = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--all-targets", + "--language", + "da", + "--context", + "--duplicates", + "--status", + "missing", + "--field", + "field.country", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(out.contains("missing_translation notes.note.sweden.fields.field.country")); + assert!(out.contains("duplicate source group: 2 occurrence(s)")); + assert!(!out.contains("notes.note.finland.fields.field.capital")); +} + #[test] fn translations_summary_exports_compact_counts_by_language() { let dir = temp_dir("translations-summary"); diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs index 40ea45a..5db1862 100644 --- a/crates/brain-brew-core/src/lib.rs +++ b/crates/brain-brew-core/src/lib.rs @@ -196,6 +196,14 @@ impl CanonicalDeck { .map(|translations| translation_coverage_report(self, overlay, translations)) } + /// Build translator-facing note/field/card context for one coverage report. + pub fn translation_context( + &self, + report: &TranslationCoverageReport, + ) -> TranslationContextView { + translation_context_view(self, report) + } + /// Validate strict core invariants that do not require filesystem or format access. pub fn validate(&self) -> Result<(), ValidationReport> { let mut errors = Vec::new(); @@ -708,6 +716,197 @@ impl TranslationCoverageBuilder<'_> { } } +fn translation_context_view( + deck: &CanonicalDeck, + report: &TranslationCoverageReport, +) -> TranslationContextView { + let mut source_counts = BTreeMap::::new(); + for entry in &report.entries { + if !entry.source.is_empty() { + *source_counts.entry(entry.source.clone()).or_insert(0) += 1; + } + } + + let entries_by_path = report + .entries + .iter() + .map(|entry| (entry.path.as_str(), entry)) + .collect::>(); + let units = report + .entries + .iter() + .map(|entry| translation_context_unit(deck, entry, &source_counts, &entries_by_path)) + .collect::>(); + + TranslationContextView { + overlay_id: report.overlay_id.clone(), + units, + } +} + +fn translation_context_unit( + deck: &CanonicalDeck, + entry: &TranslationCoverageEntry, + source_counts: &BTreeMap, + entries_by_path: &BTreeMap<&str, &TranslationCoverageEntry>, +) -> TranslationContextUnit { + let note_id = note_id_from_translation_path(&entry.path); + let note_type_id = note_id + .as_ref() + .and_then(|note_id| { + deck.notes + .get(note_id) + .map(|note| note.note_type_id.clone()) + }) + .or_else(|| note_type_id_from_translation_path(&entry.path)); + let note_type = note_type_id + .as_ref() + .and_then(|note_type_id| deck.note_types.get(note_type_id)); + let field_id = field_id_from_translation_path(&entry.path, note_type); + let note = note_id.as_ref().and_then(|note_id| deck.notes.get(note_id)); + let field_name = note_type + .zip(field_id.as_ref()) + .and_then(|(note_type, field_id)| field_definition_name(note_type, field_id)) + .map(str::to_owned); + let note_fields = note + .zip(note_type) + .zip(note_id.as_ref()) + .map(|((note, note_type), note_id)| { + note_field_contexts(note, note_type, note_id, entries_by_path) + }) + .unwrap_or_default(); + let card_templates = note_type + .zip(field_name.as_deref()) + .map(|(note_type, field_name)| card_contexts_for_field(note_type, field_name)) + .unwrap_or_default(); + + TranslationContextUnit { + category: entry.category, + path: entry.path.clone(), + source: entry.source.clone(), + translated: entry.translated.clone(), + context: entry.context.clone(), + note_id, + note_type_id, + field_id, + field_name, + note_fields, + card_templates, + source_occurrences: source_counts.get(&entry.source).copied().unwrap_or(0), + } +} + +fn note_id_from_translation_path(path: &str) -> Option { + let rest = path.strip_prefix("notes.")?; + let end = [".fields.", ".tags.", ".variables.", ".adapter_ids."] + .into_iter() + .filter_map(|marker| rest.find(marker)) + .min() + .unwrap_or(rest.len()); + StableId::new(rest[..end].to_owned()).ok() +} + +fn note_type_id_from_translation_path(path: &str) -> Option { + let rest = path.strip_prefix("note_types.")?; + let end = [ + ".fields.", + ".card_templates.", + ".variables.", + ".adapter_ids.", + ] + .into_iter() + .filter_map(|marker| rest.find(marker)) + .min() + .unwrap_or(rest.len()); + StableId::new(rest[..end].to_owned()).ok() +} + +fn field_id_from_translation_path(path: &str, note_type: Option<&NoteType>) -> Option { + let rest = path.split_once(".fields.")?.1; + if let Some(note_type) = note_type { + return note_type + .fields + .iter() + .filter(|field| { + rest == field.id.as_str() || rest.starts_with(&format!("{}.", field.id)) + }) + .max_by_key(|field| field.id.as_str().len()) + .map(|field| field.id.clone()); + } + StableId::new(rest.strip_suffix(".name").unwrap_or(rest).to_owned()).ok() +} + +fn field_definition_name<'a>(note_type: &'a NoteType, field_id: &StableId) -> Option<&'a str> { + note_type + .fields + .iter() + .find(|field| &field.id == field_id) + .map(|field| field.name.as_str()) +} + +fn note_field_contexts( + note: &Note, + note_type: &NoteType, + note_id: &StableId, + entries_by_path: &BTreeMap<&str, &TranslationCoverageEntry>, +) -> Vec { + note_type + .fields + .iter() + .map(|field| { + let source = note.fields.get(&field.id).cloned().unwrap_or_default(); + let path = format!("notes.{note_id}.fields.{}", field.id); + let entry = entries_by_path.get(path.as_str()).copied(); + TranslationNoteFieldContext { + field_id: field.id.clone(), + field_name: field.name.clone(), + source: source.clone(), + translated: entry + .and_then(|entry| entry.translated.clone()) + .unwrap_or(source), + category: entry.map(|entry| entry.category), + } + }) + .collect() +} + +fn card_contexts_for_field(note_type: &NoteType, field_name: &str) -> Vec { + note_type + .card_templates + .iter() + .filter_map(|template| { + let mut sides = BTreeSet::new(); + if template_uses_field(&template.question_format, field_name) { + sides.insert(CardTemplateSide::Question); + } + if template_uses_field(&template.answer_format, field_name) { + sides.insert(CardTemplateSide::Answer); + } + if sides.is_empty() { + None + } else { + Some(TranslationCardContext { + template_id: template.id.clone(), + template_name: template.name.clone(), + sides, + }) + } + }) + .collect() +} + +fn template_uses_field(template_text: &str, field_name: &str) -> bool { + [ + format!("{{{{{field_name}}}}}"), + format!("{{{{#{field_name}}}}}"), + format!("{{{{/{field_name}}}}}"), + format!("{{{{^{field_name}}}}}"), + format!("{{{{type:{field_name}}}}}"), + ] + .iter() + .any(|marker| template_text.contains(marker)) +} + fn apply_translation_dictionary( resolved: &mut CanonicalDeck, overlay: &Overlay, @@ -2896,6 +3095,64 @@ pub struct TranslationCoverageEntry { pub context: Option, } +/// Translator-facing contextual view for one translation coverage report. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TranslationContextView { + pub overlay_id: StableId, + pub units: Vec, +} + +/// One translatable unit annotated with note, field, card template, and status context. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TranslationContextUnit { + pub category: TranslationCoverageCategory, + pub path: String, + pub source: String, + pub translated: Option, + pub context: Option, + pub note_id: Option, + pub note_type_id: Option, + pub field_id: Option, + pub field_name: Option, + pub note_fields: Vec, + pub card_templates: Vec, + pub source_occurrences: usize, +} + +/// Sibling note-field context shown around a translatable unit. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TranslationNoteFieldContext { + pub field_id: StableId, + pub field_name: String, + pub source: String, + pub translated: String, + pub category: Option, +} + +/// Card/template context in which a note field can be seen by a translator. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TranslationCardContext { + pub template_id: StableId, + pub template_name: String, + pub sides: BTreeSet, +} + +/// Card side where a translated field appears. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum CardTemplateSide { + Question, + Answer, +} + +impl CardTemplateSide { + pub fn as_str(self) -> &'static str { + match self { + Self::Question => "question", + Self::Answer => "answer", + } + } +} + /// Maintainer-facing classification for translation coverage entries. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum TranslationCoverageCategory { diff --git a/crates/brain-brew-core/tests/overlay_compose.rs b/crates/brain-brew-core/tests/overlay_compose.rs index 6f31747..5ee6506 100644 --- a/crates/brain-brew-core/tests/overlay_compose.rs +++ b/crates/brain-brew-core/tests/overlay_compose.rs @@ -1,8 +1,8 @@ use std::collections::{BTreeMap, BTreeSet}; use brain_brew_core::{ - AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplate, CardTemplateChange, ChangeIntent, - ComposeErrorKind, DeckChange, ExpectedBase, FieldChange, FieldDefinition, + AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplate, CardTemplateChange, CardTemplateSide, + ChangeIntent, ComposeErrorKind, DeckChange, ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, Overlay, OverlayKind, PropertyChange, StableId, TagChange, TranslationCoverageCategory, TranslationDictionary, @@ -400,6 +400,73 @@ fn translation_dictionary_no_change_counts_as_reviewed_without_modifying_output( })); } +#[test] +fn translation_context_identifies_note_field_card_and_duplicate_status() { + let mut base = ug_style_deck(); + base.notes.insert(sid("note.sweden"), sweden_note()); + let overlay = Overlay { + id: sid("overlay.translation.da"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::from([("Europe".to_owned(), "Europa".to_owned())]), + contextual: BTreeMap::new(), + no_change: BTreeSet::new(), + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::from([ + "deck.*".to_owned(), + "note_types.*".to_owned(), + "notes.*.fields.field.flag".to_owned(), + ]), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = base + .translation_coverage(&overlay) + .expect("translation overlay has coverage"); + let context = base.translation_context(&report); + let finland = context + .units + .iter() + .find(|unit| unit.path == "notes.note.finland.fields.field.country") + .expect("Finland field has context"); + + assert_eq!( + finland.category, + TranslationCoverageCategory::UntranslatedFallback + ); + assert_eq!(finland.note_id.as_ref().unwrap().as_str(), "note.finland"); + assert_eq!( + finland.note_type_id.as_ref().unwrap().as_str(), + "note-type.country" + ); + assert_eq!(finland.field_id.as_ref().unwrap().as_str(), "field.country"); + assert_eq!(finland.field_name.as_deref(), Some("Country")); + assert_eq!(finland.note_fields.len(), 3); + assert_eq!(finland.note_fields[0].field_id.as_str(), "field.country"); + assert_eq!(finland.note_fields[0].source, "Finland"); + assert_eq!(finland.note_fields[0].translated, "Finland"); + assert_eq!(finland.source_occurrences, 1); + assert!(finland.card_templates.iter().any(|card| { + card.template_id.as_str() == "template.country-to-capital" + && card.sides.contains(&CardTemplateSide::Question) + })); + + let europe_units = context + .units + .iter() + .filter(|unit| unit.source == "Europe") + .collect::>(); + assert_eq!(europe_units.len(), 2); + assert!(europe_units.iter().all(|unit| unit.source_occurrences == 2)); +} + #[test] fn translation_dictionary_reports_stale_no_change_entries() { let base = ug_style_deck(); diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index a9cd148..a979ab7 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -159,12 +159,15 @@ brainbrew translations --manifest brainbrew.yaml --target de-standard brainbrew translations --manifest brainbrew.yaml --all-targets --language de brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.berlin brainbrew translations --manifest brainbrew.yaml --target de-standard --path-prefix notes.note.berlin.fields.field.country +brainbrew translations --manifest brainbrew.yaml --target de-standard --context --source Georgia +brainbrew translations --manifest brainbrew.yaml --target de-standard --context --duplicates +brainbrew translations --manifest brainbrew.yaml --target de-standard --context --status missing brainbrew translations --manifest brainbrew.yaml --target de-standard --json brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json brainbrew translations --manifest brainbrew.yaml --target de-standard --full ``` -Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Use `--summary` for compact per-language/per-overlay counts; summary mode de-duplicates identical reports across target variants and includes direct translations, contextual overrides, no-change entries, target-language additions, variables, adapter IDs, raw untranslated fallbacks, actionable missing text translations, ignored entries, and stale/invalid keys. Human summary output uses narrow aligned columns by default; add `--summary --full` to include overlay/file columns, or `--summary --json` for complete machine-readable metadata. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. +Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Use `--context` for the first translator-in-context terminal view: it shows source English and target text together with translation status, note id, field id/name, note type, and card templates where the field appears. Filter or navigate the context view with `--language`, `--note`, `--field`, `--source`, `--duplicates`, `--status missing|stale|translated|direct|contextual|no-change`, and `--path-prefix`. Repeated source strings are shown as duplicate source groups so translators can see whether a reusable `direct`/`no_change` entry is enough or a path-specific `contextual` override is needed. YAML remains the canonical storage format; the terminal context view is the first ergonomic translator interface over that canonical data, and `--context --json` exposes the same note/field/card context model for later Iced or web UIs. Use `--summary` for compact per-language/per-overlay counts; summary mode de-duplicates identical reports across target variants and includes direct translations, contextual overrides, no-change entries, target-language additions, variables, adapter IDs, raw untranslated fallbacks, actionable missing text translations, ignored entries, and stale/invalid keys. Human summary output uses narrow aligned columns by default; add `--summary --full` to include overlay/file columns, or `--summary --json` for complete machine-readable metadata. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. To seed translator work after adding English notes or fields, run apply explicitly: @@ -172,7 +175,7 @@ To seed translator work after adding English notes or fields, run apply explicit brainbrew translations --manifest brainbrew.yaml --target de-standard --apply ``` -Non-interactive `--apply` preserves the existing scriptable behavior: it inserts deterministic `source: source` translation stubs into `translations.direct` for the missing text fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose one action for all selected rows — mark `no_change`, add direct `source: source`, add contextual `source: source`, add `ignore_paths`, skip — or choose to decide per row. It keeps reviewed no-change decisions distinct from real translations and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. +Non-interactive `--apply` preserves the existing scriptable behavior: it inserts deterministic `source: source` translation stubs into `translations.direct` for the missing text fallbacks in scope. Interactive apply is selective: toggle rows with Space, confirm with Enter, then choose one action for all selected rows — mark `no_change`, add direct `source: source`, add contextual `source: source`, add `ignore_paths`, skip — or choose to decide per row. Context viewing does not introduce a second mutation path: combine context filters with `--apply --interactive` when you want to apply changes, and Brain Brew routes them through the same translation sync/apply machinery. It keeps reviewed no-change decisions distinct from real translations and does not invent target-language additions for blank source fields. Existing comments and layout are preserved where practical; run `brainbrew fmt overlays/languages/de.yaml` when you want fully canonical formatting. When `require_complete: true`, composition fails if any extracted non-empty translatable string is not translated by `direct`, translated by a matching `contextual` entry, marked by `no_change`, or matched by `ignore_paths`. For release workflows, prefer target-level verification policy in `brainbrew.yaml`: From a923b2ae9c5d74e8081bc926c34e562a56ffdeb3 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 19:20:52 +0200 Subject: [PATCH 029/142] document non-Nix install path --- .github/workflows/package-smoke.yml | 19 ++++++++++++ README.md | 6 ++-- devenv.nix | 7 +++++ documentation/docs/getting-started/install.md | 16 ++++++++-- documentation/docs/reference/releasing.md | 11 +++++-- scripts/release_smoke.sh | 30 +++++++++++++++++++ 6 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/package-smoke.yml create mode 100755 scripts/release_smoke.sh diff --git a/.github/workflows/package-smoke.yml b/.github/workflows/package-smoke.yml new file mode 100644 index 0000000..bc29fe6 --- /dev/null +++ b/.github/workflows/package-smoke.yml @@ -0,0 +1,19 @@ +name: Package Smoke + +on: + pull_request: + push: + +jobs: + cargo-install-smoke: + name: cargo install smoke + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - name: Install packaged CLI without Nix + run: | + cargo install --path crates/brain-brew-cli --locked --root "$RUNNER_TEMP/brainbrew-install" + echo "$RUNNER_TEMP/brainbrew-install/bin" >> "$GITHUB_PATH" + - name: Smoke test installed CLI + run: scripts/release_smoke.sh brainbrew diff --git a/README.md b/README.md index 506d83f..e232fac 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ See the dedicated documentation site in [`documentation/`](documentation/) for m ## Install the CLI +For normal deck users and contributors, install a released `brainbrew` binary. You do not need Nix to edit or verify a Federated Deck workspace, and downstream projects such as Ultimate Geography can link to the release tag they recommend. + The easiest install path is the GitHub Release installer: ```bash @@ -54,13 +56,13 @@ Homebrew users can install from the tap once the preview release is published: brew install jeprecated/tap/brainbrew ``` -Rust users can install directly from the release tag: +Rust users can install directly from the pinned release tag: ```bash cargo install --git https://github.com/jeprecated/brain-brew --tag v1.0.0-alpha.1 brainbrew --locked ``` -Nix remains available as an optional build/install path: +Nix remains available as an optional reproducible build/install path for contributors and CI: ```bash nix run . -- --help diff --git a/devenv.nix b/devenv.nix index 8ab99b0..d20d64b 100644 --- a/devenv.nix +++ b/devenv.nix @@ -36,6 +36,13 @@ in scripts."dist:plan".exec = '' dist manifest --tag v1.0.0-alpha.1 --artifacts=all --no-local-paths --output-format=json ''; + scripts."release:smoke".exec = '' + set -euo pipefail + install_root="$(mktemp -d)" + trap 'rm -rf "$install_root"' EXIT + cargo install --path crates/brain-brew-cli --locked --root "$install_root" + "$PWD/scripts/release_smoke.sh" "$install_root/bin/brainbrew" + ''; scripts.ci.exec = '' set -euo pipefail diff --git a/documentation/docs/getting-started/install.md b/documentation/docs/getting-started/install.md index be6ca92..cf896fe 100644 --- a/documentation/docs/getting-started/install.md +++ b/documentation/docs/getting-started/install.md @@ -6,7 +6,17 @@ title: Install the CLI Brain Brew is a Rust CLI named `brainbrew`. -The primary install path is a prebuilt GitHub Release binary. You do **not** need Rust or Nix to test a released Brain Brew build. +The primary install path is a prebuilt GitHub Release binary. You do **not** need Rust or Nix to test a released Brain Brew build. Deck projects such as Ultimate Geography can link to this page and recommend a specific Brain Brew release tag instead of asking contributors to run Brain Brew through a Nix flake. + +## Recommended version for deck contributors + +Use the version recommended by the deck repository you are contributing to. For the current Brain Brew preview, pin examples and contributor docs to `v1.0.0-alpha.1` and verify the installed binary with: + +```bash +brainbrew --version +``` + +If a deck needs a newer Brain Brew behavior, update its docs to name the new Brain Brew tag or release URL. Nix users can still pin a flake revision for reproducible CI, but normal deck editing should start from the release installer, Homebrew, or Cargo install commands below. ## GitHub Release installer @@ -41,13 +51,15 @@ The Homebrew formula is generated from the same release artifacts as the shell i ## Cargo install -If you already have Rust installed: +If you already have Rust installed, install the same release tag directly from GitHub: ```bash cargo install --git https://github.com/jeprecated/brain-brew --tag v1.0.0-alpha.1 brainbrew --locked brainbrew --version ``` +This is a normal non-Nix install path, but the prebuilt release installer is preferred for users who do not already have Rust. + For unreleased branch testing: ```bash diff --git a/documentation/docs/reference/releasing.md b/documentation/docs/reference/releasing.md index 080ce52..ba6b76c 100644 --- a/documentation/docs/reference/releasing.md +++ b/documentation/docs/reference/releasing.md @@ -16,6 +16,12 @@ Brain Brew releases are built with [`cargo-dist`](https://opensource.axo.dev/car The generated workflow can publish the Homebrew formula to `jeprecated/homebrew-tap`. Add a repository secret named `HOMEBREW_TAP_TOKEN` to `jeprecated/brain-brew` with permission to push to `jeprecated/homebrew-tap`. +## Versioning and pinning + +Brain Brew uses the Cargo workspace version as the release version. Release tags are `v`, for example `v1.0.0-alpha.1`, and the Cargo workspace version must match the tag version before the tag is pushed. + +Deck projects that depend on Brain Brew should pin or recommend a release tag in their contributor docs. For example, Ultimate Geography can link to the install page and say that contributors should install Brain Brew `v1.0.0-alpha.1` until the project intentionally moves to a newer Brain Brew release. CI workflows that prefer Nix can instead pin a flake revision, but that should be documented as the reproducible CI path rather than the normal user install path. + ## Local release checks Before creating a tag: @@ -23,9 +29,10 @@ Before creating a tag: ```bash devenv shell ci devenv shell dist:plan > /tmp/brainbrew-dist-manifest.json +devenv shell release:smoke ``` -`dist:plan` verifies that `cargo-dist` can see the release package and expected artifacts for `v1.0.0-alpha.1`. +`dist:plan` verifies that `cargo-dist` can see the release package and expected artifacts for `v1.0.0-alpha.1`. `release:smoke` installs the CLI with `cargo install --path crates/brain-brew-cli --locked` into a temporary root and runs the installed binary through `validate`, `compose`, `export crowdanki`, and `verify` against the fast UG-style fixture. If you change `dist-workspace.toml`, regenerate the workflow: @@ -43,7 +50,7 @@ Using Jujutsu to create the tag locally: jj tag set v1.0.0-alpha.1 -r rust-brainbrew ``` -Push the tag with your Git/Jujutsu setup. The GitHub release workflow runs when the tag reaches GitHub and creates the release assets. +Push the tag with your Git/Jujutsu setup. The GitHub release workflow runs when the tag reaches GitHub and creates the release assets. The separate Package Smoke workflow also runs on pushes and pull requests to verify that the non-Nix Cargo install path produces a working `brainbrew` binary for `validate`, `compose`, `export`, and `verify`. ## Reviewer install commands diff --git a/scripts/release_smoke.sh b/scripts/release_smoke.sh new file mode 100755 index 0000000..5f72fba --- /dev/null +++ b/scripts/release_smoke.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +brainbrew_bin="${1:-brainbrew}" +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +manifest="$repo_root/fixtures/ug-style/brainbrew.yaml" +out_dir="$(mktemp -d)" +trap 'rm -rf "$out_dir"' EXIT + +"$brainbrew_bin" --version + +"$brainbrew_bin" validate \ + --manifest "$manifest" \ + --target full-demo + +"$brainbrew_bin" compose \ + --manifest "$manifest" \ + --target full-demo \ + --out "$out_dir/full-demo.yaml" +test -s "$out_dir/full-demo.yaml" + +"$brainbrew_bin" export crowdanki \ + --manifest "$manifest" \ + --target full-demo \ + --out "$out_dir/crowdanki" +test -s "$out_dir/crowdanki/deck.json" + +"$brainbrew_bin" verify \ + --manifest "$manifest" \ + --all-targets From 7ee471ff8e93cf009c07925b817e25f0a50e1efa Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 20:05:26 +0200 Subject: [PATCH 030/142] prepare crates.io publishing --- .github/workflows/package-smoke.yml | 2 + CHANGELOG.md | 1 + Cargo.toml | 4 +- README.md | 8 ++- crates/brain-brew-cli/Cargo.toml | 1 - crates/brain-brew-core/Cargo.toml | 3 +- crates/brain-brew-formats/Cargo.toml | 3 +- devenv.nix | 1 + documentation/docs/getting-started/install.md | 11 ++- documentation/docs/reference/releasing.md | 36 +++++++++- scripts/check_cratesio_metadata.py | 72 +++++++++++++++++++ 11 files changed, 133 insertions(+), 9 deletions(-) create mode 100755 scripts/check_cratesio_metadata.py diff --git a/.github/workflows/package-smoke.yml b/.github/workflows/package-smoke.yml index bc29fe6..1c50c05 100644 --- a/.github/workflows/package-smoke.yml +++ b/.github/workflows/package-smoke.yml @@ -11,6 +11,8 @@ jobs: steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable + - name: Check crates.io package metadata + run: scripts/check_cratesio_metadata.py - name: Install packaged CLI without Nix run: | cargo install --path crates/brain-brew-cli --locked --root "$RUNNER_TEMP/brainbrew-install" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d25890..33d4d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,4 @@ Initial Rust-based Brain Brew preview release. - Adds media reference validation and release-oracle comparison support for parity reviews. - Includes Ultimate Geography-style fixtures used to validate translations, variants, and Hardcore Geography extension overlays. - Ships prebuilt release archives, shell and PowerShell installers, and a Homebrew formula via `cargo-dist`. +- Supports publishing `brain-brew-core`, `brain-brew-formats`, and `brainbrew` to crates.io so Rust users can install with `cargo install brainbrew --version 1.0.0-alpha.1 --locked` after the release is published. diff --git a/Cargo.toml b/Cargo.toml index fba5209..413c617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ authors = ["Brain Brew contributors"] repository = "https://github.com/jeprecated/brain-brew" [workspace.dependencies] -brain-brew-core = { path = "crates/brain-brew-core" } -brain-brew-formats = { path = "crates/brain-brew-formats" } +brain-brew-core = { path = "crates/brain-brew-core", version = "=1.0.0-alpha.1" } +brain-brew-formats = { path = "crates/brain-brew-formats", version = "=1.0.0-alpha.1" } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/README.md b/README.md index e232fac..6beae8d 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,13 @@ Homebrew users can install from the tap once the preview release is published: brew install jeprecated/tap/brainbrew ``` -Rust users can install directly from the pinned release tag: +Rust users can install from crates.io once the preview crate is published: + +```bash +cargo install brainbrew --version 1.0.0-alpha.1 --locked +``` + +Before the crates.io package is published, Rust users can install directly from the pinned release tag: ```bash cargo install --git https://github.com/jeprecated/brain-brew --tag v1.0.0-alpha.1 brainbrew --locked diff --git a/crates/brain-brew-cli/Cargo.toml b/crates/brain-brew-cli/Cargo.toml index 0cf9f0a..5693dc7 100644 --- a/crates/brain-brew-cli/Cargo.toml +++ b/crates/brain-brew-cli/Cargo.toml @@ -6,7 +6,6 @@ rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true -publish = false description = "Rust-based Brain Brew CLI for local-first Anki-compatible deck federation" homepage = "https://github.com/jeprecated/brain-brew" diff --git a/crates/brain-brew-core/Cargo.toml b/crates/brain-brew-core/Cargo.toml index f031610..478b151 100644 --- a/crates/brain-brew-core/Cargo.toml +++ b/crates/brain-brew-core/Cargo.toml @@ -6,7 +6,8 @@ rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true -publish = false +description = "Pure domain model for Brain Brew local-first Anki-compatible deck federation" +homepage = "https://github.com/jeprecated/brain-brew" [lints] workspace = true diff --git a/crates/brain-brew-formats/Cargo.toml b/crates/brain-brew-formats/Cargo.toml index 24264f0..930a348 100644 --- a/crates/brain-brew-formats/Cargo.toml +++ b/crates/brain-brew-formats/Cargo.toml @@ -6,7 +6,8 @@ rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true -publish = false +description = "YAML and CrowdAnki codecs for Brain Brew local-first deck federation" +homepage = "https://github.com/jeprecated/brain-brew" [dependencies] brain-brew-core.workspace = true diff --git a/devenv.nix b/devenv.nix index d20d64b..daa2037 100644 --- a/devenv.nix +++ b/devenv.nix @@ -36,6 +36,7 @@ in scripts."dist:plan".exec = '' dist manifest --tag v1.0.0-alpha.1 --artifacts=all --no-local-paths --output-format=json ''; + scripts."crates:metadata-check".exec = "scripts/check_cratesio_metadata.py"; scripts."release:smoke".exec = '' set -euo pipefail install_root="$(mktemp -d)" diff --git a/documentation/docs/getting-started/install.md b/documentation/docs/getting-started/install.md index cf896fe..2e2e357 100644 --- a/documentation/docs/getting-started/install.md +++ b/documentation/docs/getting-started/install.md @@ -51,14 +51,21 @@ The Homebrew formula is generated from the same release artifacts as the shell i ## Cargo install -If you already have Rust installed, install the same release tag directly from GitHub: +If you already have Rust installed, install the pinned release from crates.io after the preview crate is published: + +```bash +cargo install brainbrew --version 1.0.0-alpha.1 --locked +brainbrew --version +``` + +Before the crates.io package is published, or if you need to test the exact GitHub tag, install directly from the same release tag: ```bash cargo install --git https://github.com/jeprecated/brain-brew --tag v1.0.0-alpha.1 brainbrew --locked brainbrew --version ``` -This is a normal non-Nix install path, but the prebuilt release installer is preferred for users who do not already have Rust. +Both commands are normal non-Nix install paths, but the prebuilt release installer is preferred for users who do not already have Rust. For unreleased branch testing: diff --git a/documentation/docs/reference/releasing.md b/documentation/docs/reference/releasing.md index ba6b76c..2a3181e 100644 --- a/documentation/docs/reference/releasing.md +++ b/documentation/docs/reference/releasing.md @@ -28,11 +28,15 @@ Before creating a tag: ```bash devenv shell ci +devenv shell crates:metadata-check devenv shell dist:plan > /tmp/brainbrew-dist-manifest.json devenv shell release:smoke +cargo publish -p brain-brew-core --dry-run ``` -`dist:plan` verifies that `cargo-dist` can see the release package and expected artifacts for `v1.0.0-alpha.1`. `release:smoke` installs the CLI with `cargo install --path crates/brain-brew-cli --locked` into a temporary root and runs the installed binary through `validate`, `compose`, `export crowdanki`, and `verify` against the fast UG-style fixture. +`crates:metadata-check` verifies that the workspace packages are publishable and that internal dependencies carry exact version requirements for crates.io. `dist:plan` verifies that `cargo-dist` can see the release package and expected artifacts for `v1.0.0-alpha.1`. `release:smoke` installs the CLI with `cargo install --path crates/brain-brew-cli --locked` into a temporary root and runs the installed binary through `validate`, `compose`, `export crowdanki`, and `verify` against the fast UG-style fixture. + +Only `brain-brew-core` can be fully dry-run before anything is published because the dependent crates resolve their exact internal dependencies from crates.io during `cargo publish --dry-run`. After `brain-brew-core` is published and visible in the crates.io index, dry-run and publish `brain-brew-formats`; after that is visible, dry-run and publish `brainbrew`. If you change `dist-workspace.toml`, regenerate the workflow: @@ -52,6 +56,29 @@ jj tag set v1.0.0-alpha.1 -r rust-brainbrew Push the tag with your Git/Jujutsu setup. The GitHub release workflow runs when the tag reaches GitHub and creates the release assets. The separate Package Smoke workflow also runs on pushes and pull requests to verify that the non-Nix Cargo install path produces a working `brainbrew` binary for `validate`, `compose`, `export`, and `verify`. +## Publish crates.io packages + +Log in to crates.io once with `cargo login`, then publish in dependency order. Crates.io versions are immutable, so double-check the workspace version, README install snippets, and changelog first. + +```bash +cargo publish -p brain-brew-core --dry-run +cargo publish -p brain-brew-core +# wait for the crates.io index to show brain-brew-core v1.0.0-alpha.1 + +cargo publish -p brain-brew-formats --dry-run +cargo publish -p brain-brew-formats +# wait for the crates.io index to show brain-brew-formats v1.0.0-alpha.1 + +cargo publish -p brainbrew --dry-run +cargo publish -p brainbrew +``` + +After `brainbrew` is published, Rust users can install without Nix and without a Git checkout: + +```bash +cargo install brainbrew --version 1.0.0-alpha.1 --locked +``` + ## Reviewer install commands After the workflow completes, reviewers can install without Rust or Nix: @@ -63,6 +90,13 @@ curl --proto '=https' --tlsv1.2 -LsSf \ brainbrew --version ``` +Rust users can run: + +```bash +cargo install brainbrew --version 1.0.0-alpha.1 --locked +brainbrew --version +``` + Homebrew users can run: ```bash diff --git a/scripts/check_cratesio_metadata.py b/scripts/check_cratesio_metadata.py new file mode 100755 index 0000000..37126ab --- /dev/null +++ b/scripts/check_cratesio_metadata.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Check workspace package metadata needed for crates.io publishing.""" + +from __future__ import annotations + +import json +import subprocess +import sys + +EXPECTED_PACKAGES = {"brain-brew-core", "brain-brew-formats", "brainbrew"} +INTERNAL_DEPS = {"brain-brew-core", "brain-brew-formats"} + + +def main() -> int: + metadata = json.loads( + subprocess.check_output( + ["cargo", "metadata", "--no-deps", "--format-version", "1"], text=True + ) + ) + workspace_packages = { + package["name"]: package + for package in metadata["packages"] + if package["id"] in metadata["workspace_members"] + } + errors: list[str] = [] + + missing = EXPECTED_PACKAGES - workspace_packages.keys() + if missing: + errors.append(f"missing workspace packages: {', '.join(sorted(missing))}") + + workspace_version = metadata["workspace_default_members"] and next( + package["version"] for package in workspace_packages.values() + ) + + for name in sorted(EXPECTED_PACKAGES & workspace_packages.keys()): + package = workspace_packages[name] + if package.get("publish") is not None: + errors.append(f"{name}: package.publish must allow crates.io publishing") + for field in ["description", "license", "repository"]: + if not package.get(field): + errors.append(f"{name}: missing package.{field}") + if package["version"] != workspace_version: + errors.append( + f"{name}: version {package['version']} does not match workspace version {workspace_version}" + ) + + for dep in package["dependencies"]: + if dep["name"] not in INTERNAL_DEPS: + continue + expected_req = f"={workspace_version}" + if dep["req"] != expected_req: + errors.append( + f"{name}: dependency {dep['name']} must use exact req {expected_req}, got {dep['req']}" + ) + if not dep.get("path"): + errors.append( + f"{name}: dependency {dep['name']} should keep a local path for workspace development" + ) + + if errors: + for error in errors: + print(f"crates.io metadata check failed: {error}", file=sys.stderr) + return 1 + + print( + f"crates.io metadata ok for {', '.join(sorted(EXPECTED_PACKAGES))} at {workspace_version}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From c8e3f5970bb4ba430028a1782a0ddcb21f087745 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 22:25:09 +0200 Subject: [PATCH 031/142] add crates release task commands --- .sd/aggregates.yaml | 66 ++++++++++++ .sd/tasks.yaml | 23 ++++ .sd/workspace.yaml | 8 ++ devenv.nix | 7 ++ documentation/docs/reference/releasing.md | 39 +++++-- scripts/publish_crates.sh | 124 ++++++++++++++++++++++ 6 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 .sd/aggregates.yaml create mode 100644 .sd/tasks.yaml create mode 100644 .sd/workspace.yaml create mode 100755 scripts/publish_crates.sh diff --git a/.sd/aggregates.yaml b/.sd/aggregates.yaml new file mode 100644 index 0000000..891479f --- /dev/null +++ b/.sd/aggregates.yaml @@ -0,0 +1,66 @@ +version: 1 + +tasks: + release/crates/all: + desc: Publish brain-brew-core, brain-brew-formats, then brainbrew to crates.io + danger: true + defaultMode: dry-run + modes: + dry-run: + danger: false + values: + releaseMode: dry-run + confirmArgs: "" + publish: + values: + releaseMode: publish + confirmArgs: " --yes" + run: scripts/publish_crates.sh {{releaseMode}} all{{confirmArgs}} + + release/crates/core: + desc: Publish brain-brew-core to crates.io + danger: true + defaultMode: dry-run + modes: + dry-run: + danger: false + values: + releaseMode: dry-run + confirmArgs: "" + publish: + values: + releaseMode: publish + confirmArgs: " --yes" + run: scripts/publish_crates.sh {{releaseMode}} core{{confirmArgs}} + + release/crates/formats: + desc: Publish brain-brew-formats to crates.io after brain-brew-core is indexed + danger: true + defaultMode: dry-run + modes: + dry-run: + danger: false + values: + releaseMode: dry-run + confirmArgs: "" + publish: + values: + releaseMode: publish + confirmArgs: " --yes" + run: scripts/publish_crates.sh {{releaseMode}} formats{{confirmArgs}} + + release/crates/cli: + desc: Publish brainbrew to crates.io after brain-brew-formats is indexed + danger: true + defaultMode: dry-run + modes: + dry-run: + danger: false + values: + releaseMode: dry-run + confirmArgs: "" + publish: + values: + releaseMode: publish + confirmArgs: " --yes" + run: scripts/publish_crates.sh {{releaseMode}} cli{{confirmArgs}} diff --git a/.sd/tasks.yaml b/.sd/tasks.yaml new file mode 100644 index 0000000..7626a8e --- /dev/null +++ b/.sd/tasks.yaml @@ -0,0 +1,23 @@ +version: 1 + +project: + id: brain-brew + name: Brain Brew + aliases: + - brainbrew + workArea: Deck federation + visibility: public-code / CLI workspace + dir: . + +tasks: + check: + desc: Run the standard Brain Brew validation gate + run: devenv shell ci + +passthrough: + cargo: + desc: Run cargo in the Brain Brew workspace + run: cargo "$@" + devenv: + desc: Run devenv in the Brain Brew workspace + run: devenv "$@" diff --git a/.sd/workspace.yaml b/.sd/workspace.yaml new file mode 100644 index 0000000..15fb746 --- /dev/null +++ b/.sd/workspace.yaml @@ -0,0 +1,8 @@ +version: 1 + +workspace: + id: brain-brew + name: Brain Brew + +discover: + - .sd/tasks.yaml diff --git a/devenv.nix b/devenv.nix index daa2037..8be5afd 100644 --- a/devenv.nix +++ b/devenv.nix @@ -37,6 +37,13 @@ in dist manifest --tag v1.0.0-alpha.1 --artifacts=all --no-local-paths --output-format=json ''; scripts."crates:metadata-check".exec = "scripts/check_cratesio_metadata.py"; + scripts."crates:publish-dry-run".exec = '' + scripts/publish_crates.sh dry-run "''${1:-all}" + ''; + scripts."crates:publish".exec = '' + scripts/publish_crates.sh publish "''${1:-all}" --yes + ''; + scripts."release:crates".exec = "scripts/publish_crates.sh dry-run all"; scripts."release:smoke".exec = '' set -euo pipefail install_root="$(mktemp -d)" diff --git a/documentation/docs/reference/releasing.md b/documentation/docs/reference/releasing.md index 2a3181e..7d823cc 100644 --- a/documentation/docs/reference/releasing.md +++ b/documentation/docs/reference/releasing.md @@ -31,12 +31,14 @@ devenv shell ci devenv shell crates:metadata-check devenv shell dist:plan > /tmp/brainbrew-dist-manifest.json devenv shell release:smoke -cargo publish -p brain-brew-core --dry-run +devenv shell release:crates +# or, if you use the sd task dispatcher: +sd release crates all ``` -`crates:metadata-check` verifies that the workspace packages are publishable and that internal dependencies carry exact version requirements for crates.io. `dist:plan` verifies that `cargo-dist` can see the release package and expected artifacts for `v1.0.0-alpha.1`. `release:smoke` installs the CLI with `cargo install --path crates/brain-brew-cli --locked` into a temporary root and runs the installed binary through `validate`, `compose`, `export crowdanki`, and `verify` against the fast UG-style fixture. +`crates:metadata-check` verifies that the workspace packages are publishable and that internal dependencies carry exact version requirements for crates.io. `dist:plan` verifies that `cargo-dist` can see the release package and expected artifacts for `v1.0.0-alpha.1`. `release:smoke` installs the CLI with `cargo install --path crates/brain-brew-cli --locked` into a temporary root and runs the installed binary through `validate`, `compose`, `export crowdanki`, and `verify` against the fast UG-style fixture. `release:crates` and `sd release crates all` default to dry-run mode. -Only `brain-brew-core` can be fully dry-run before anything is published because the dependent crates resolve their exact internal dependencies from crates.io during `cargo publish --dry-run`. After `brain-brew-core` is published and visible in the crates.io index, dry-run and publish `brain-brew-formats`; after that is visible, dry-run and publish `brainbrew`. +Only `brain-brew-core` can be fully dry-run before anything is published because the dependent crates resolve their exact internal dependencies from crates.io during `cargo publish --dry-run`. The all-crates dry-run reports dependent crates as skipped until earlier crates are visible in the crates.io index. After `brain-brew-core` is published and visible, dry-run and publish `brain-brew-formats`; after that is visible, dry-run and publish `brainbrew`. If you change `dist-workspace.toml`, regenerate the workflow: @@ -60,19 +62,38 @@ Push the tag with your Git/Jujutsu setup. The GitHub release workflow runs when Log in to crates.io once with `cargo login`, then publish in dependency order. Crates.io versions are immutable, so double-check the workspace version, README install snippets, and changelog first. +With Devenv scripts: + +```bash +devenv shell crates:publish-dry-run core +devenv shell crates:publish core +# wait for the crates.io index to show brain-brew-core v1.0.0-alpha.1 + +devenv shell crates:publish-dry-run formats +devenv shell crates:publish formats +# wait for the crates.io index to show brain-brew-formats v1.0.0-alpha.1 + +devenv shell crates:publish-dry-run cli +devenv shell crates:publish cli +``` + +With the `sd` task dispatcher, dry-run is the default and publish mode requires `--yes`: + ```bash -cargo publish -p brain-brew-core --dry-run -cargo publish -p brain-brew-core +sd release crates core +sd release crates core --mode publish --yes # wait for the crates.io index to show brain-brew-core v1.0.0-alpha.1 -cargo publish -p brain-brew-formats --dry-run -cargo publish -p brain-brew-formats +sd release crates formats +sd release crates formats --mode publish --yes # wait for the crates.io index to show brain-brew-formats v1.0.0-alpha.1 -cargo publish -p brainbrew --dry-run -cargo publish -p brainbrew +sd release crates cli +sd release crates cli --mode publish --yes ``` +The same commands are backed by `scripts/publish_crates.sh`; use it directly if neither Devenv nor `sd` is available. + After `brainbrew` is published, Rust users can install without Nix and without a Git checkout: ```bash diff --git a/scripts/publish_crates.sh b/scripts/publish_crates.sh new file mode 100755 index 0000000..13a94f2 --- /dev/null +++ b/scripts/publish_crates.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat >&2 <<'USAGE' +Usage: + scripts/publish_crates.sh dry-run + scripts/publish_crates.sh publish --yes + +Dry-run mode is safe and is the default used by the sd release task. +Publish mode uploads immutable crates.io versions and requires --yes. +USAGE +} + +mode="${1:-}" +target="${2:-}" +shift $(( $# >= 2 ? 2 : $# )) +confirm="${1:-}" + +case "$mode" in + dry-run|publish) ;; + *) usage; exit 2 ;; +esac + +case "$target" in + all|core|formats|cli) ;; + *) usage; exit 2 ;; +esac + +if [[ "$mode" == "publish" && "$confirm" != "--yes" ]]; then + echo "Refusing to publish without explicit --yes." >&2 + usage + exit 2 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')" + +declare -A package_by_target=( + [core]="brain-brew-core" + [formats]="brain-brew-formats" + [cli]="brainbrew" +) + +declare -A crate_by_target=( + [core]="brain-brew-core" + [formats]="brain-brew-formats" + [cli]="brainbrew" +) + +publish_one() { + local target_name="$1" + local package="${package_by_target[$target_name]}" + if [[ "$mode" == "dry-run" ]]; then + cargo publish -p "$package" --dry-run + else + cargo publish -p "$package" + fi +} + +crate_is_indexed() { + local crate="$1" + cargo search "$crate" --limit 5 2>/dev/null | grep -Eq "^${crate} = \"${version}\"" +} + +wait_for_crate() { + local target_name="$1" + local crate="${crate_by_target[$target_name]}" + echo "Waiting for ${crate} ${version} to appear in the crates.io index..." + for _ in $(seq 1 30); do + if crate_is_indexed "$crate"; then + echo "${crate} ${version} is visible in the crates.io index." + return 0 + fi + sleep 10 + done + echo "Timed out waiting for ${crate} ${version} in the crates.io index." >&2 + echo "Wait a little longer, then continue with the next package manually." >&2 + return 1 +} + +try_dependent_dry_run() { + local target_name="$1" + local package="${package_by_target[$target_name]}" + local log + log="$(mktemp)" + if cargo publish -p "$package" --dry-run >"$log" 2>&1; then + cat "$log" + rm -f "$log" + return 0 + fi + if grep -q "no matching package named" "$log"; then + echo "Skipping ${package} dry-run for now: exact internal dependency is not visible in crates.io yet." + echo "After publishing earlier crates and waiting for the index, run:" + echo " sd release crates ${target_name}" + rm -f "$log" + return 0 + fi + cat "$log" >&2 + rm -f "$log" + return 1 +} + +case "$target" in + core|formats|cli) + publish_one "$target" + ;; + all) + scripts/check_cratesio_metadata.py + if [[ "$mode" == "dry-run" ]]; then + publish_one core + try_dependent_dry_run formats + try_dependent_dry_run cli + else + publish_one core + wait_for_crate core + publish_one formats + wait_for_crate formats + publish_one cli + fi + ;; +esac From 08d1a8ca5d9337eba3cc64b43ce0e9897eab18ef Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 22:39:11 +0200 Subject: [PATCH 032/142] update install docs for published crate --- CHANGELOG.md | 2 +- README.md | 19 +++++++------ documentation/docs/getting-started/install.md | 28 +++++++++++-------- documentation/docs/reference/releasing.md | 2 +- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d4d8f..3e9d372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,4 +9,4 @@ Initial Rust-based Brain Brew preview release. - Adds media reference validation and release-oracle comparison support for parity reviews. - Includes Ultimate Geography-style fixtures used to validate translations, variants, and Hardcore Geography extension overlays. - Ships prebuilt release archives, shell and PowerShell installers, and a Homebrew formula via `cargo-dist`. -- Supports publishing `brain-brew-core`, `brain-brew-formats`, and `brainbrew` to crates.io so Rust users can install with `cargo install brainbrew --version 1.0.0-alpha.1 --locked` after the release is published. +- Publishes `brain-brew-core`, `brain-brew-formats`, and `brainbrew` to crates.io so Rust users can install with `cargo install brainbrew --version 1.0.0-alpha.1 --locked`. diff --git a/README.md b/README.md index 6beae8d..b55acd3 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,16 @@ See the dedicated documentation site in [`documentation/`](documentation/) for m ## Install the CLI -For normal deck users and contributors, install a released `brainbrew` binary. You do not need Nix to edit or verify a Federated Deck workspace, and downstream projects such as Ultimate Geography can link to the release tag they recommend. +For normal deck users and contributors, install the released `brainbrew` CLI from crates.io or a GitHub Release. You do not need Nix to edit or verify a Federated Deck workspace, and downstream projects such as Ultimate Geography can link to the release version they recommend. -The easiest install path is the GitHub Release installer: +Rust users can install the published crate: + +```bash +cargo install brainbrew --version 1.0.0-alpha.1 --locked +brainbrew --version +``` + +The easiest no-Rust install path is the GitHub Release installer: ```bash curl --proto '=https' --tlsv1.2 -LsSf \ @@ -56,13 +63,7 @@ Homebrew users can install from the tap once the preview release is published: brew install jeprecated/tap/brainbrew ``` -Rust users can install from crates.io once the preview crate is published: - -```bash -cargo install brainbrew --version 1.0.0-alpha.1 --locked -``` - -Before the crates.io package is published, Rust users can install directly from the pinned release tag: +To test the exact GitHub tag instead of the crates.io package, install directly from the pinned release tag: ```bash cargo install --git https://github.com/jeprecated/brain-brew --tag v1.0.0-alpha.1 brainbrew --locked diff --git a/documentation/docs/getting-started/install.md b/documentation/docs/getting-started/install.md index 2e2e357..2e2b18b 100644 --- a/documentation/docs/getting-started/install.md +++ b/documentation/docs/getting-started/install.md @@ -6,7 +6,7 @@ title: Install the CLI Brain Brew is a Rust CLI named `brainbrew`. -The primary install path is a prebuilt GitHub Release binary. You do **not** need Rust or Nix to test a released Brain Brew build. Deck projects such as Ultimate Geography can link to this page and recommend a specific Brain Brew release tag instead of asking contributors to run Brain Brew through a Nix flake. +The normal install paths are the published crates.io package and prebuilt GitHub Release binaries. You do **not** need Nix to test a released Brain Brew build. Deck projects such as Ultimate Geography can link to this page and recommend a specific Brain Brew version instead of asking contributors to run Brain Brew through a Nix flake. ## Recommended version for deck contributors @@ -16,7 +16,18 @@ Use the version recommended by the deck repository you are contributing to. For brainbrew --version ``` -If a deck needs a newer Brain Brew behavior, update its docs to name the new Brain Brew tag or release URL. Nix users can still pin a flake revision for reproducible CI, but normal deck editing should start from the release installer, Homebrew, or Cargo install commands below. +If a deck needs a newer Brain Brew behavior, update its docs to name the new Brain Brew version, tag, or release URL. Nix users can still pin a flake revision for reproducible CI, but normal deck editing should start from the Cargo, release installer, or Homebrew commands below. + +## Cargo install + +If you already have Rust installed, install the published crate from crates.io: + +```bash +cargo install brainbrew --version 1.0.0-alpha.1 --locked +brainbrew --version +``` + +Use the explicit `--version` while Brain Brew is in preview so deck docs and CI can pin the expected behavior. ## GitHub Release installer @@ -49,23 +60,16 @@ brainbrew --version The Homebrew formula is generated from the same release artifacts as the shell installer. -## Cargo install - -If you already have Rust installed, install the pinned release from crates.io after the preview crate is published: - -```bash -cargo install brainbrew --version 1.0.0-alpha.1 --locked -brainbrew --version -``` +## Install from a Git tag -Before the crates.io package is published, or if you need to test the exact GitHub tag, install directly from the same release tag: +If you need to test the exact GitHub tag instead of the crates.io package, install directly from the same release tag: ```bash cargo install --git https://github.com/jeprecated/brain-brew --tag v1.0.0-alpha.1 brainbrew --locked brainbrew --version ``` -Both commands are normal non-Nix install paths, but the prebuilt release installer is preferred for users who do not already have Rust. +This is also a normal non-Nix install path, but `cargo install brainbrew --version ...` is preferred for Rust users now that the crate is published. For unreleased branch testing: diff --git a/documentation/docs/reference/releasing.md b/documentation/docs/reference/releasing.md index 7d823cc..dc171a5 100644 --- a/documentation/docs/reference/releasing.md +++ b/documentation/docs/reference/releasing.md @@ -94,7 +94,7 @@ sd release crates cli --mode publish --yes The same commands are backed by `scripts/publish_crates.sh`; use it directly if neither Devenv nor `sd` is available. -After `brainbrew` is published, Rust users can install without Nix and without a Git checkout: +The current preview crate is published, so Rust users can install without Nix and without a Git checkout: ```bash cargo install brainbrew --version 1.0.0-alpha.1 --locked From 83a26a9428b1ccdc1bedcff3218d1cdd4c8c81fa Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 23:02:30 +0200 Subject: [PATCH 033/142] strengthen semantic regression coverage --- .../tests/canonical_yaml.rs | 21 ++++++- .../tests/ultimate_geography_fixture.rs | 56 +++++++++++++++++++ documentation/docs/authoring/verify-export.md | 33 ++++++++++- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/crates/brain-brew-formats/tests/canonical_yaml.rs b/crates/brain-brew-formats/tests/canonical_yaml.rs index bdf1664..15ac2f3 100644 --- a/crates/brain-brew-formats/tests/canonical_yaml.rs +++ b/crates/brain-brew-formats/tests/canonical_yaml.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use brain_brew_core::{ AdapterIds, CanonicalDeck, CardTemplate, FieldDefinition, MediaReference, Note, NoteType, - StableId, + SemanticChangeKind, StableId, }; use brain_brew_formats::{canonical_yaml, crowdanki}; @@ -25,7 +25,7 @@ fn parses_emitted_yaml_back_to_semantically_equal_deck() { } #[test] -fn formatter_canonicalizes_valid_yaml_bytes() { +fn semantic_diff_ignores_yaml_ordering_and_reports_content_changes() { let messy_yaml = r#"deck: description: A geography deck fixture. id: deck.ultimate-geography @@ -71,6 +71,23 @@ tombstones: [] let formatted = canonical_yaml::format_str(messy_yaml).expect("valid yaml formats"); assert_eq!(formatted, EXPECTED_CANONICAL_YAML); + + let canonical = canonical_yaml::from_str(EXPECTED_CANONICAL_YAML).expect("canonical parses"); + let reordered = canonical_yaml::from_str(messy_yaml).expect("reordered yaml parses"); + assert!( + canonical.semantic_diff(&reordered).is_empty(), + "semantic comparison should ignore YAML key ordering and formatting noise" + ); + + let changed_yaml = messy_yaml.replace("field.capital: Helsinki", "field.capital: Helsingfors"); + let changed = canonical_yaml::from_str(&changed_yaml).expect("changed yaml parses"); + let diff = canonical.semantic_diff(&changed); + assert_eq!(diff.changes.len(), 1); + let change = &diff.changes[0]; + assert_eq!(change.kind, SemanticChangeKind::Modified); + assert_eq!(change.path, "notes.note.finland.fields.field.capital"); + assert_eq!(change.before.as_deref(), Some("Helsinki")); + assert_eq!(change.after.as_deref(), Some("Helsingfors")); } #[test] diff --git a/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs index 2eed847..4c21b52 100644 --- a/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs +++ b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs @@ -6,6 +6,7 @@ use brain_brew_formats::core::{ AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplateChange, ChangeIntent, DeckChange, ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, MediaChange, MediaReference, Note, NoteChange, NoteTypeChange, Overlay, OverlayKind, PropertyChange, StableId, TagChange, + TranslationDictionary, }; use brain_brew_formats::{canonical_yaml, crowdanki, manifest, media, source_includes}; @@ -1032,6 +1033,61 @@ fn ug_regression_note_changes_flow_to_crowdanki_for_every_target() { }); } +#[test] +fn ug_regression_translation_overlay_changes_flow_to_resolved_and_crowdanki_output() { + let root = fixture_root(); + let manifest = read_manifest(&root); + let target = "en-standard"; + let baseline_deck = compose_target(&root, &manifest, target); + let baseline_json = exported_json(&baseline_deck); + let note_id = sid("note.finland"); + let field_id = sid("field.capital"); + let current_value = baseline_deck.notes[¬e_id].fields[&field_id].clone(); + let translated_value = "Regression translated capital".to_owned(); + let expected_export_path = + note_field_json_path(&baseline_deck, "note.finland", "field.capital"); + + let mut overlay = empty_overlay("overlay.regression.translation.en-standard"); + overlay.kind = OverlayKind::Translation; + overlay.translations = Some(TranslationDictionary { + direct: BTreeMap::new(), + contextual: BTreeMap::from([( + "notes.note.finland".to_owned(), + BTreeMap::from([(current_value, translated_value.clone())]), + )]), + no_change: BTreeSet::new(), + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: false, + ignore_paths: BTreeSet::new(), + }); + + let changed_deck = compose_target_with_extra_overlay(&root, &manifest, target, Some(overlay)); + let resolved_diff = baseline_deck.semantic_diff(&changed_deck); + assert_eq!(resolved_diff.changes.len(), 1); + assert_eq!( + resolved_diff.changes[0].path, + "notes.note.finland.fields.field.capital" + ); + assert_eq!(resolved_diff.changes[0].before.as_deref(), Some("Helsinki")); + assert_eq!( + resolved_diff.changes[0].after.as_deref(), + Some(translated_value.as_str()) + ); + + let changed_json = exported_json(&changed_deck); + assert_eq!( + json_diff_paths(&baseline_json, &changed_json), + BTreeSet::from([expected_export_path.clone()]), + "translation overlay should change exactly one CrowdAnki JSON path" + ); + assert_eq!( + changed_json.pointer(&expected_export_path), + Some(&serde_json::json!(translated_value)) + ); +} + #[test] fn ug_regression_media_changes_flow_to_crowdanki_for_every_target() { assert_all_targets_export_exact_diffs("media addition", |target, _deck, baseline_json| { diff --git a/documentation/docs/authoring/verify-export.md b/documentation/docs/authoring/verify-export.md index 4563e2d..3bb3207 100644 --- a/documentation/docs/authoring/verify-export.md +++ b/documentation/docs/authoring/verify-export.md @@ -27,7 +27,9 @@ Verification checks: 5. dependency expansion; 6. target composition; 7. Canonical Deck validation; -8. configured CrowdAnki golden checks. +8. translation coverage policy, when configured or passed with `--translation-coverage`; +9. media references and SHA-256 hashes, when `--media-root` is passed; +10. configured CrowdAnki golden checks. ## Verify media @@ -75,11 +77,36 @@ When `--out` is omitted, Brain Brew uses `exports.crowdanki.out` when configured brainbrew export crowdanki --manifest brainbrew.yaml --target de-standard ``` +## Regression guarantees + +Brain Brew's regression checks are semantic rather than raw text comparisons where ordering is not meaningful: + +- Canonical Deck semantic diffs report stable entity paths such as `notes.note.finland.fields.field.capital` and ignore YAML key ordering or formatting noise. +- Composition/export regression tests exercise one deliberate change at a time: representative note fields, template HTML, CSS/styling, deck descriptions, media references, translation overlays, variants, and extension/field-fill overlays. +- Translation coverage tests distinguish stale source keys, strict-mode missing translations, path-specific overrides, reviewed no-change text, and target-language additions. +- CrowdAnki export tests compare parsed JSON paths so a one-parameter source edit must affect exactly the expected exported location. + +For deck workspaces, run the same gate before review: + +```bash +brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/ +brainbrew translations --manifest brainbrew.yaml --all-targets --overlay overlays/languages --summary +``` + +Use `brainbrew diff` or `brainbrew explain` when reviewing a change interactively; both report semantic paths instead of line-oriented YAML noise. + ## Golden checks -When `golden` is configured, `verify` compares generated CrowdAnki JSON against the golden as parsed JSON. +When `golden` is configured, `verify` compares generated CrowdAnki JSON against the golden as parsed JSON. This makes snapshots hard to update accidentally: changing generated output without updating the golden fails `verify`. + +To update a golden intentionally, export the target to its configured golden directory, review the semantic and JSON differences, then commit the golden with the source change: + +```bash +brainbrew export crowdanki --manifest brainbrew.yaml --target de-standard --out goldens/de-standard +brainbrew verify --manifest brainbrew.yaml --target de-standard --media-root media/ +``` -Use `golden_allowlist` only after reviewing concrete differences: +Use `golden_allowlist` only after reviewing concrete differences and keep it as narrow as possible: ```yaml golden_allowlist: From e07724bfbf2be72340cb941f33ff255b3abfbf00 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Mon, 22 Jun 2026 23:24:39 +0200 Subject: [PATCH 034/142] support structured translatable messages --- .../src/commands/translations.rs | 43 ++ crates/brain-brew-cli/tests/cli.rs | 164 ++++++ crates/brain-brew-core/src/lib.rs | 474 +++++++++++++++++- .../tests/canonical_deck_validation.rs | 1 + .../brain-brew-core/tests/overlay_compose.rs | 258 +++++++++- crates/brain-brew-core/tests/semantic_diff.rs | 2 + .../brain-brew-formats/src/canonical_yaml.rs | 138 ++++- crates/brain-brew-formats/src/crowdanki.rs | 1 + .../brain-brew-formats/src/source_includes.rs | 3 + .../tests/canonical_yaml.rs | 72 +++ crates/brain-brew-formats/tests/crowdanki.rs | 1 + .../tests/media_references.rs | 1 + .../tests/ultimate_geography_fixture.rs | 1 + documentation/docs/authoring/translations.md | 51 ++ documentation/docs/reference/yaml.md | 18 + 15 files changed, 1208 insertions(+), 20 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 0ac012d..6feaaf8 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -7,6 +7,7 @@ use std::path::{Path, PathBuf}; use brain_brew_core::{ CanonicalDeck, Overlay, OverlayKind, TranslationContextUnit, TranslationContextView, TranslationCoverageCategory, TranslationCoverageEntry, TranslationCoverageReport, + TranslationMessageContext, }; use brain_brew_formats::manifest::FederatedDeckManifest; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; @@ -1755,6 +1756,19 @@ fn context_unit_json(unit: &TranslationContextUnit) -> serde_json::Value { "translated": field.translated, "status": field.category.map(|category| category.as_str()), })).collect::>(), + "message": unit.message.as_ref().map(|message| json!({ + "source": message.source, + "translated": message.translated, + "components": message.components.iter().map(|component| json!({ + "index": component.index, + "kind": component.kind.as_str(), + "path": component.path, + "source": component.source, + "translated": component.translated, + "reference": component.reference, + "status": component.category.map(|category| category.as_str()), + })).collect::>(), + })), "card_templates": unit.card_templates.iter().map(|card| json!({ "template_id": card.template_id.as_str(), "template_name": card.template_name, @@ -1830,6 +1844,9 @@ fn print_context_unit(unit: &TranslationContextUnit, language: &str) { if !unit.note_fields.is_empty() { print_note_field_context(unit, language); } + if let Some(message) = &unit.message { + print_message_context(message, language); + } if let Some(context) = &unit.context { println!(" dictionary context: {context}"); } @@ -1881,6 +1898,32 @@ fn print_note_field_context(unit: &TranslationContextUnit, language: &str) { } } +fn print_message_context(message: &TranslationMessageContext, language: &str) { + println!(" structured message (source/en | target/{language}):"); + println!( + " resolved {} | {}", + compact_context_value(&message.source), + compact_context_value(&message.translated) + ); + for component in &message.components { + let label = match component.reference.as_deref() { + Some(reference) => format!( + "[{}] {} {}", + component.index, + component.kind.as_str(), + reference + ), + None => format!("[{}] {}", component.index, component.kind.as_str()), + }; + println!( + " {:<32} {} | {}", + label, + compact_context_value(&component.source), + compact_context_value(&component.translated) + ); + } +} + fn is_media_or_structural_field_id(field_id: &str) -> bool { field_id == "field.flag" || field_id == "field.map" } diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index f9b2d38..f069115 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -1091,6 +1091,55 @@ fn translations_context_view_json_exposes_reusable_context_model() { assert!(unit["card_templates"].as_array().unwrap().len() >= 4); } +#[test] +fn translations_context_view_json_shows_structured_message_components() { + let dir = temp_dir("translations-context-structured-message"); + write_structured_message_translation_workspace(&dir); + + let output = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "nb-standard", + "--context", + "--source", + "blue background", + "--json", + ]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let json: serde_json::Value = serde_json::from_str(&stdout(&output)).unwrap(); + let unit = &json["contexts"][0]["units"][0]; + assert_eq!( + unit["path"], + "notes.note.finland.fields.field.flag-similarity.message.2" + ); + assert_eq!(unit["status"], "contextual_override"); + assert_eq!( + unit["message"]["source"], + "Iceland (blue background with a white cross), Norway (red background with a blue cross)" + ); + assert_eq!( + unit["message"]["translated"], + "Island (blå bakgrunn med hvitt kors), Norge (rød bakgrunn med blått kors)" + ); + let components = unit["message"]["components"].as_array().unwrap(); + assert_eq!(components[0]["kind"], "field_ref"); + assert_eq!( + components[0]["reference"], + "notes.note.iceland.fields.field.country" + ); + assert_eq!(components[0]["source"], "Iceland"); + assert_eq!(components[0]["translated"], "Island"); + assert_eq!(components[2]["kind"], "text"); + assert_eq!( + components[2]["source"], + "blue background with a white cross" + ); + assert_eq!(components[2]["translated"], "blå bakgrunn med hvitt kors"); +} + #[test] fn translations_context_apply_uses_existing_translation_apply_path() { let dir = temp_dir("translations-context-apply"); @@ -2454,6 +2503,121 @@ targets: .unwrap(); } +fn write_structured_message_translation_workspace(dir: &Path) { + fs::write( + dir.join("deck.yaml"), + r#"deck: + id: deck.structured-message + name: Structured Message Fixture + description: Structured message translation fixture. + adapter_ids: {} +note_types: + note-type.country: + name: Country + field_order: + - field.country + - field.capital + - field.flag + - field.flag-similarity + fields: + field.country: + name: Country + field.capital: + name: Capital + field.flag: + name: Flag + field.flag-similarity: + name: Flag similarity + card_template_order: + - template.flag-country + card_templates: + template.flag-country: + name: Flag - Country + question_format: '{{Flag}}' + answer_format: '{{Flag similarity}}' + adapter_ids: {} + styling: '' + adapter_ids: {} +notes: + note.finland: + note_type_id: note-type.country + fields: + field.capital: Helsinki + field.country: Finland + field.flag: '' + field.flag-similarity: + message: + - ref: notes.note.iceland.fields.field.country + - literal: ' (' + - text: blue background with a white cross + - literal: '), ' + - ref: notes.note.norway.fields.field.country + - literal: ' (' + - text: red background with a blue cross + - literal: ')' + tags: [] + adapter_ids: {} + note.iceland: + note_type_id: note-type.country + fields: + field.capital: Reykjavik + field.country: Iceland + field.flag: '' + field.flag-similarity: '' + tags: [] + adapter_ids: {} + note.norway: + note_type_id: note-type.country + fields: + field.capital: Oslo + field.country: Norway + field.flag: '' + field.flag-similarity: '' + tags: [] + adapter_ids: {} +media: {} +tombstones: [] +"#, + ) + .unwrap(); + fs::write( + dir.join("nb.yaml"), + r#"id: overlay.translation.nb +kind: translation +translations: + ignore_paths: + - deck.* + - note_types.* + - notes.*.fields.field.country + - notes.*.fields.field.capital + - notes.*.fields.field.flag + - notes.*.tags.* + direct: + Iceland: Island + Norway: Norge + red background with a blue cross: rød bakgrunn med blått kors + contextual: + notes.note.finland: + blue background with a white cross: blå bakgrunn med hvitt kors +"#, + ) + .unwrap(); + fs::write( + dir.join("brainbrew.yaml"), + r#"base: deck.yaml +overlays: + overlay.translation.nb: + file: nb.yaml + kind: translation +targets: + nb-standard: + overlays: + - overlay.translation.nb +"#, + ) + .unwrap(); +} + fn write_include_workspace(dir: &Path) { fs::create_dir_all(dir.join("content")).unwrap(); fs::create_dir_all(dir.join("templates")).unwrap(); diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs index 5db1862..033adcf 100644 --- a/crates/brain-brew-core/src/lib.rs +++ b/crates/brain-brew-core/src/lib.rs @@ -127,8 +127,11 @@ impl CanonicalDeck { let mut errors = Vec::new(); let mut changed_paths = BTreeMap::::new(); + resolve_structured_messages_with_compose_errors(&mut resolved, &mut errors); + for overlay in overlays { apply_overlay(&mut resolved, overlay, &mut changed_paths, &mut errors); + resolve_structured_messages_with_compose_errors(&mut resolved, &mut errors); } if !errors.is_empty() { @@ -188,6 +191,18 @@ impl CanonicalDeck { render_deck_variables(self) } + /// Resolve structured message fields into their final scalar field strings. + pub fn resolve_structured_messages(&self) -> Result { + let mut resolved = self.clone(); + let mut errors = Vec::new(); + resolve_structured_messages_with_validation_errors(&mut resolved, &mut errors); + if errors.is_empty() { + Ok(resolved) + } else { + Err(ValidationReport { errors }) + } + } + /// Report translation coverage for one translation overlay without modifying this deck. pub fn translation_coverage(&self, overlay: &Overlay) -> Option { overlay @@ -279,6 +294,20 @@ impl CanonicalDeck { } } + for (field_id, message) in ¬e.field_messages { + if !expected_field_ids.contains(field_id) { + errors.push(ValidationError::new( + ValidationErrorKind::UnknownNoteField, + format!("notes.{id}.fields.{field_id}.message"), + format!( + "structured message field {field_id} is not defined by note type {}", + note.note_type_id + ), + )); + } + validate_message_references(self, id, field_id, message, &mut errors); + } + for field_id in expected_field_ids { if !note.fields.contains_key(&field_id) { errors.push(ValidationError::new( @@ -384,12 +413,123 @@ fn fill_added_field_blanks( } } +fn validate_message_references( + deck: &CanonicalDeck, + note_id: &StableId, + field_id: &StableId, + message: &StructuredMessage, + errors: &mut Vec, +) { + for (index, component) in message.components.iter().enumerate() { + let MessageComponent::FieldRef(reference) = component else { + continue; + }; + if field_value_at_path(deck, reference).is_none() { + errors.push(ValidationError::new( + ValidationErrorKind::InvalidMessageReference, + message_component_path(note_id, field_id, index), + format!("structured message field reference {reference:?} does not resolve to a note field"), + )); + } + } +} + +fn resolve_structured_messages_with_validation_errors( + deck: &mut CanonicalDeck, + errors: &mut Vec, +) { + let snapshot = deck.clone(); + let mut resolved_fields = Vec::<(StableId, StableId, String)>::new(); + for (note_id, note) in &snapshot.notes { + for (field_id, message) in ¬e.field_messages { + match render_structured_message(&snapshot, message) { + Ok(value) => resolved_fields.push((note_id.clone(), field_id.clone(), value)), + Err(reference) => errors.push(ValidationError::new( + ValidationErrorKind::InvalidMessageReference, + format!("notes.{note_id}.fields.{field_id}.message"), + format!("structured message field reference {reference:?} does not resolve to a note field"), + )), + } + } + } + for (note_id, field_id, value) in resolved_fields { + if let Some(note) = deck.notes.get_mut(¬e_id) { + note.fields.insert(field_id, value); + } + } +} + +fn resolve_structured_messages_with_compose_errors( + deck: &mut CanonicalDeck, + errors: &mut Vec, +) { + let snapshot = deck.clone(); + let mut resolved_fields = Vec::<(StableId, StableId, String)>::new(); + for (note_id, note) in &snapshot.notes { + for (field_id, message) in ¬e.field_messages { + match render_structured_message(&snapshot, message) { + Ok(value) => resolved_fields.push((note_id.clone(), field_id.clone(), value)), + Err(reference) => errors.push(ComposeError::new( + ComposeErrorKind::ValidationFailed, + format!("notes.{note_id}.fields.{field_id}.message"), + format!("structured message field reference {reference:?} does not resolve to a note field"), + )), + } + } + } + for (note_id, field_id, value) in resolved_fields { + if let Some(note) = deck.notes.get_mut(¬e_id) { + note.fields.insert(field_id, value); + } + } +} + +fn render_structured_message( + deck: &CanonicalDeck, + message: &StructuredMessage, +) -> Result { + let mut rendered = String::new(); + for component in &message.components { + match component { + MessageComponent::Literal(value) | MessageComponent::Text(value) => { + rendered.push_str(value); + } + MessageComponent::FieldRef(reference) => { + let Some(value) = field_value_at_path(deck, reference) else { + return Err(reference.clone()); + }; + rendered.push_str(value); + } + } + } + Ok(rendered) +} + +fn field_value_at_path<'a>(deck: &'a CanonicalDeck, path: &str) -> Option<&'a str> { + let (note_id, field_id) = note_field_path_parts(path)?; + deck.notes + .get(¬e_id) + .and_then(|note| note.fields.get(&field_id)) + .map(String::as_str) +} + +fn note_field_path_parts(path: &str) -> Option<(StableId, StableId)> { + let rest = path.strip_prefix("notes.")?; + let (note_id, field_id) = rest.split_once(".fields.")?; + Some((StableId::new(note_id).ok()?, StableId::new(field_id).ok()?)) +} + +fn message_component_path(note_id: &StableId, field_id: &StableId, index: usize) -> String { + format!("notes.{note_id}.fields.{field_id}.message.{index}") +} + fn translation_coverage_report( deck: &CanonicalDeck, overlay: &Overlay, translations: &TranslationDictionary, ) -> TranslationCoverageReport { let mut builder = TranslationCoverageBuilder { + deck, translations, entries: Vec::new(), seen_sources: BTreeSet::new(), @@ -456,7 +596,12 @@ fn translation_coverage_report( for (note_id, note) in &deck.notes { builder.record_variables(¬e.variables, &format!("notes.{note_id}.variables")); for (field_id, value) in ¬e.fields { - builder.record_string(value, format!("notes.{note_id}.fields.{field_id}"), None); + let path = format!("notes.{note_id}.fields.{field_id}"); + if let Some(message) = note.field_messages.get(field_id) { + builder.record_message(value, path, note_id, field_id, message); + } else { + builder.record_string(value, path, None); + } } builder.record_tags(¬e.tags, &format!("notes.{note_id}.tags")); builder.record_adapter_ids(¬e.adapter_ids, &format!("notes.{note_id}.adapter_ids")); @@ -466,6 +611,7 @@ fn translation_coverage_report( } struct TranslationCoverageBuilder<'a> { + deck: &'a CanonicalDeck, translations: &'a TranslationDictionary, entries: Vec, seen_sources: BTreeSet, @@ -484,6 +630,81 @@ impl TranslationCoverageBuilder<'_> { } } + fn record_message( + &mut self, + resolved_value: &str, + path: String, + note_id: &StableId, + field_id: &StableId, + message: &StructuredMessage, + ) { + if self.translations.target_additions.contains_key(&path) + || self.has_explicit_string_entry(resolved_value, &path, None) + { + self.record_string(resolved_value, path, None); + return; + } + + if resolved_value.is_empty() { + return; + } + + if is_ignored_translation_path(self.translations, &path) { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::IgnoredSource, + path, + source: resolved_value.to_owned(), + translated: None, + context: None, + }); + return; + } + + for (index, component) in message.components.iter().enumerate() { + let component_path = message_component_path(note_id, field_id, index); + match component { + MessageComponent::Literal(_) => {} + MessageComponent::Text(value) => { + self.record_string(value, component_path, None); + } + MessageComponent::FieldRef(reference) => { + if let Some(value) = field_value_at_path(self.deck, reference) { + self.record_string(value, component_path, None); + } + } + } + } + } + + fn has_explicit_string_entry( + &self, + value: &str, + path: &str, + variable_key: Option<&str>, + ) -> bool { + if value.is_empty() { + return false; + } + if let Some(variable_key) = variable_key + && self + .translations + .variables + .get(variable_key) + .is_some_and(|replacements| replacements.contains_key(value)) + { + return true; + } + self.translations.direct.contains_key(value) + || self.translations.no_change.contains(value) + || self + .translations + .contextual + .iter() + .any(|(context_path, replacements)| { + context_matches_path(context_path, path) && replacements.contains_key(value) + }) + } + fn record_string(&mut self, value: &str, path: String, variable_key: Option<&str>) { if let Some(addition) = self.translations.target_additions.get(&path) { self.seen_target_additions.insert(path.clone()); @@ -775,6 +996,11 @@ fn translation_context_unit( note_field_contexts(note, note_type, note_id, entries_by_path) }) .unwrap_or_default(); + let message = note.zip(note_id.as_ref()).zip(field_id.as_ref()).and_then( + |((note, note_id), field_id)| { + message_context(deck, note, note_id, field_id, entries_by_path) + }, + ); let card_templates = note_type .zip(field_name.as_deref()) .map(|(note_type, field_name)| card_contexts_for_field(note_type, field_name)) @@ -791,6 +1017,7 @@ fn translation_context_unit( field_id, field_name, note_fields, + message, card_templates, source_occurrences: source_counts.get(&entry.source).copied().unwrap_or(0), } @@ -870,6 +1097,61 @@ fn note_field_contexts( .collect() } +fn message_context( + deck: &CanonicalDeck, + note: &Note, + note_id: &StableId, + field_id: &StableId, + entries_by_path: &BTreeMap<&str, &TranslationCoverageEntry>, +) -> Option { + let message = note.field_messages.get(field_id)?; + let resolved_source = note.fields.get(field_id).cloned().unwrap_or_default(); + let mut components = Vec::new(); + for (index, component) in message.components.iter().enumerate() { + let path = message_component_path(note_id, field_id, index); + let entry = entries_by_path.get(path.as_str()).copied(); + let (kind, source, reference) = match component { + MessageComponent::Literal(value) => { + (MessageComponentKind::Literal, value.clone(), None) + } + MessageComponent::Text(value) => (MessageComponentKind::Text, value.clone(), None), + MessageComponent::FieldRef(reference) => ( + MessageComponentKind::FieldRef, + field_value_at_path(deck, reference) + .unwrap_or_default() + .to_owned(), + Some(reference.clone()), + ), + }; + let translated = entry + .and_then(|entry| entry.translated.clone()) + .unwrap_or_else(|| source.clone()); + components.push(TranslationMessageComponentContext { + index, + kind, + path, + source, + translated, + reference, + category: entry.map(|entry| entry.category), + }); + } + let translated = entries_by_path + .get(format!("notes.{note_id}.fields.{field_id}").as_str()) + .and_then(|entry| entry.translated.clone()) + .unwrap_or_else(|| { + components + .iter() + .map(|component| component.translated.as_str()) + .collect::() + }); + Some(TranslationMessageContext { + source: resolved_source, + translated, + components, + }) +} + fn card_contexts_for_field(note_type: &NoteType, field_name: &str) -> Vec { note_type .card_templates @@ -914,6 +1196,7 @@ fn apply_translation_dictionary( changed_paths: &mut BTreeMap, errors: &mut Vec, ) { + let source_deck = resolved.clone(); let mut seen_direct = BTreeSet::new(); let mut seen_contextual = BTreeSet::new(); let mut seen_target_additions = BTreeSet::new(); @@ -992,8 +1275,32 @@ fn apply_translation_dictionary( for (note_id, note) in &mut resolved.notes { context.translate_variables(&mut note.variables, &format!("notes.{note_id}.variables")); - for (field_id, value) in &mut note.fields { - context.translate_string(value, format!("notes.{note_id}.fields.{field_id}"), None); + let field_ids = note.fields.keys().cloned().collect::>(); + for field_id in field_ids { + let path = format!("notes.{note_id}.fields.{field_id}"); + if note.field_messages.contains_key(&field_id) { + let source = note.fields.get(&field_id).cloned().unwrap_or_default(); + let full_override = { + let message = note + .field_messages + .get_mut(&field_id) + .expect("field message existence checked"); + context.translate_message_field( + &source_deck, + &source, + path.clone(), + note_id, + &field_id, + message, + ) + }; + if let Some(translated) = full_override { + note.field_messages.remove(&field_id); + note.fields.insert(field_id, translated); + } + } else if let Some(value) = note.fields.get_mut(&field_id) { + context.translate_string(value, path, None); + } } context.translate_tags(&mut note.tags, &format!("notes.{note_id}.tags")); context.translate_adapter_ids( @@ -1113,6 +1420,76 @@ impl TranslationApplyContext<'_, '_> { } } + fn translate_message_field( + &mut self, + source_deck: &CanonicalDeck, + resolved_source: &str, + path: String, + note_id: &StableId, + field_id: &StableId, + message: &mut StructuredMessage, + ) -> Option { + if self.translations.target_additions.contains_key(&path) + || self.has_explicit_string_entry(resolved_source, &path, None) + { + let mut translated = resolved_source.to_owned(); + self.translate_string(&mut translated, path, None); + return Some(translated); + } + + if resolved_source.is_empty() || is_ignored_translation_path(self.translations, &path) { + return None; + } + + for (index, component) in message.components.iter_mut().enumerate() { + let component_path = message_component_path(note_id, field_id, index); + match component { + MessageComponent::Literal(_) => {} + MessageComponent::Text(value) => { + self.translate_string(value, component_path, None); + } + MessageComponent::FieldRef(reference) => { + let Some(source) = field_value_at_path(source_deck, reference) else { + continue; + }; + let mut translated = source.to_owned(); + self.translate_string(&mut translated, component_path, None); + *component = MessageComponent::Literal(translated); + } + } + } + None + } + + fn has_explicit_string_entry( + &self, + value: &str, + path: &str, + variable_key: Option<&str>, + ) -> bool { + if value.is_empty() { + return false; + } + if let Some(variable_key) = variable_key + && self + .translations + .variables + .get(variable_key) + .is_some_and(|replacements| replacements.contains_key(value)) + { + return true; + } + self.translations.direct.contains_key(value) + || self.translations.no_change.contains(value) + || self + .translations + .contextual + .iter() + .any(|(context_path, replacements)| { + context_matches_path(context_path, path) && replacements.contains_key(value) + }) + } + fn translate_string(&mut self, value: &mut String, path: String, variable_key: Option<&str>) { if let Some(addition) = self.translations.target_additions.get(&path) { self.seen_target_additions.insert(path.clone()); @@ -2400,6 +2777,7 @@ fn apply_field_change( } ChangeIntent::Remove => { note.fields.remove(field_id); + note.field_messages.remove(field_id); } ChangeIntent::Add | ChangeIntent::Merge @@ -2414,6 +2792,7 @@ fn apply_field_change( return; }; note.fields.insert(field_id.clone(), value.clone()); + note.field_messages.remove(field_id); } } } @@ -2693,15 +3072,46 @@ fn render_deck_variables(deck: &CanonicalDeck) -> Result], + errors: &mut Vec, +) { + for (index, component) in message.components.iter_mut().enumerate() { + match component { + MessageComponent::Literal(value) | MessageComponent::Text(value) => { + render_string_with_variables(value, &format!("{path}.{index}"), scopes, errors); + } + MessageComponent::FieldRef(_) => {} + } + } +} + fn render_string_with_variables( value: &mut String, path: &str, @@ -3115,10 +3525,31 @@ pub struct TranslationContextUnit { pub field_id: Option, pub field_name: Option, pub note_fields: Vec, + pub message: Option, pub card_templates: Vec, pub source_occurrences: usize, } +/// Structured-message context for a note field shown around a translatable unit. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TranslationMessageContext { + pub source: String, + pub translated: String, + pub components: Vec, +} + +/// One component of a structured message shown in translator context. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TranslationMessageComponentContext { + pub index: usize, + pub kind: MessageComponentKind, + pub path: String, + pub source: String, + pub translated: String, + pub reference: Option, + pub category: Option, +} + /// Sibling note-field context shown around a translatable unit. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TranslationNoteFieldContext { @@ -3368,10 +3799,46 @@ pub struct Note { pub note_type_id: StableId, pub variables: BTreeMap, pub fields: BTreeMap, + pub field_messages: BTreeMap, pub tags: BTreeSet, pub adapter_ids: AdapterIds, } +/// A field value assembled from reusable references, translatable fragments, and literal glue. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StructuredMessage { + pub components: Vec, +} + +/// One component of a structured message field value. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum MessageComponent { + /// Non-translatable glue such as punctuation, spaces, or markup. + Literal(String), + /// A translatable fragment extracted independently for translation coverage. + Text(String), + /// A reference to another note field path, such as `notes.note.iceland.fields.field.country`. + FieldRef(String), +} + +/// Stable component kind for reports and translator context UIs. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MessageComponentKind { + Literal, + Text, + FieldRef, +} + +impl MessageComponentKind { + pub fn as_str(self) -> &'static str { + match self { + Self::Literal => "literal", + Self::Text => "text", + Self::FieldRef => "field_ref", + } + } +} + /// A reference to an external media asset. #[derive(Clone, Debug, Eq, PartialEq)] pub struct MediaReference { @@ -3582,6 +4049,7 @@ pub enum ValidationErrorKind { MismatchedEntityId, DuplicateFieldDefinition, DuplicateCardTemplate, + InvalidMessageReference, } #[cfg(test)] diff --git a/crates/brain-brew-core/tests/canonical_deck_validation.rs b/crates/brain-brew-core/tests/canonical_deck_validation.rs index 3fc6b91..283e717 100644 --- a/crates/brain-brew-core/tests/canonical_deck_validation.rs +++ b/crates/brain-brew-core/tests/canonical_deck_validation.rs @@ -163,6 +163,7 @@ fn ug_style_deck() -> CanonicalDeck { (sid("field.capital"), "Helsinki".to_owned()), (sid("field.flag"), "".to_owned()), ]), + field_messages: BTreeMap::new(), tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), adapter_ids: note_adapter_ids, }; diff --git a/crates/brain-brew-core/tests/overlay_compose.rs b/crates/brain-brew-core/tests/overlay_compose.rs index 5ee6506..ad2c108 100644 --- a/crates/brain-brew-core/tests/overlay_compose.rs +++ b/crates/brain-brew-core/tests/overlay_compose.rs @@ -3,9 +3,9 @@ use std::collections::{BTreeMap, BTreeSet}; use brain_brew_core::{ AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplate, CardTemplateChange, CardTemplateSide, ChangeIntent, ComposeErrorKind, DeckChange, ExpectedBase, FieldChange, FieldDefinition, - FieldDefinitionChange, MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, - Overlay, OverlayKind, PropertyChange, StableId, TagChange, TranslationCoverageCategory, - TranslationDictionary, + FieldDefinitionChange, MediaChange, MediaReference, MessageComponent, Note, NoteChange, + NoteType, NoteTypeChange, Overlay, OverlayKind, PropertyChange, StableId, StructuredMessage, + TagChange, TranslationCoverageCategory, TranslationDictionary, }; #[test] @@ -105,6 +105,7 @@ fn extension_overlay_can_add_a_note_type_and_notes_using_it() { (sid("field.region"), "Europe".to_owned()), (sid("field.map"), "".to_owned()), ]), + field_messages: BTreeMap::new(), tags: BTreeSet::from(["UG::Europe".to_owned()]), adapter_ids: AdapterIds::new(), }), @@ -771,6 +772,188 @@ fn translation_coverage_reports_path_specific_overrides_and_additions() { })); } +#[test] +fn translation_dictionary_resolves_structured_message_components() { + let base = ug_style_deck_with_flag_similarity_message(); + let overlay = Overlay { + id: sid("overlay.translation.nb"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::from([ + ("Iceland".to_owned(), "Island".to_owned()), + ("Norway".to_owned(), "Norge".to_owned()), + ( + "red background with a blue cross".to_owned(), + "rød bakgrunn med blått kors".to_owned(), + ), + ]), + contextual: BTreeMap::from([( + "notes.note.finland".to_owned(), + BTreeMap::from([( + "blue background with a white cross".to_owned(), + "blå bakgrunn med hvitt kors".to_owned(), + )]), + )]), + no_change: BTreeSet::new(), + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: true, + ignore_paths: BTreeSet::from([ + "deck.*".to_owned(), + "note_types.*".to_owned(), + "notes.*.fields.field.country".to_owned(), + "notes.*.fields.field.capital".to_owned(), + "notes.*.fields.field.flag".to_owned(), + "notes.*.tags.*".to_owned(), + ]), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = base + .translation_coverage(&overlay) + .expect("translation overlay has coverage"); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::DirectTranslation + && entry.path == "notes.note.finland.fields.field.flag-similarity.message.0" + && entry.source == "Iceland" + && entry.translated.as_deref() == Some("Island") + })); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::ContextualOverride + && entry.path == "notes.note.finland.fields.field.flag-similarity.message.2" + && entry.source == "blue background with a white cross" + && entry.translated.as_deref() == Some("blå bakgrunn med hvitt kors") + })); + + let resolved = base.compose(&[overlay]).expect("translations compose"); + + assert_eq!( + resolved.notes[&sid("note.finland")].fields[&sid("field.flag-similarity")], + "Island (blå bakgrunn med hvitt kors), Norge (rød bakgrunn med blått kors)" + ); +} + +#[test] +fn translation_dictionary_can_override_full_structured_message_when_needed() { + let base = ug_style_deck_with_flag_similarity_message(); + let full_source = + "Iceland (blue background with a white cross), Norway (red background with a blue cross)"; + let overlay = Overlay { + id: sid("overlay.translation.nb"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::new(), + contextual: BTreeMap::from([( + "notes.note.finland".to_owned(), + BTreeMap::from([( + full_source.to_owned(), + "Særskilt full oversettelse".to_owned(), + )]), + )]), + no_change: BTreeSet::new(), + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: true, + ignore_paths: BTreeSet::from([ + "deck.*".to_owned(), + "note_types.*".to_owned(), + "notes.*.fields.field.country".to_owned(), + "notes.*.fields.field.capital".to_owned(), + "notes.*.fields.field.flag".to_owned(), + "notes.*.tags.*".to_owned(), + ]), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let resolved = base.compose(&[overlay]).expect("full override composes"); + + assert_eq!( + resolved.notes[&sid("note.finland")].fields[&sid("field.flag-similarity")], + "Særskilt full oversettelse" + ); + assert!( + !resolved.notes[&sid("note.finland")] + .field_messages + .contains_key(&sid("field.flag-similarity")), + "full resolved-text overrides replace the structured source message" + ); +} + +#[test] +fn translation_dictionary_reports_structured_message_missing_and_stale_components() { + let base = ug_style_deck_with_flag_similarity_message(); + let overlay = Overlay { + id: sid("overlay.translation.nb"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::from([ + ("Iceland".to_owned(), "Island".to_owned()), + ("Norway".to_owned(), "Norge".to_owned()), + ( + "stale qualifier".to_owned(), + "foreldet kvalifikator".to_owned(), + ), + ]), + contextual: BTreeMap::new(), + no_change: BTreeSet::new(), + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: true, + ignore_paths: BTreeSet::from([ + "deck.*".to_owned(), + "note_types.*".to_owned(), + "notes.*.fields.field.country".to_owned(), + "notes.*.fields.field.capital".to_owned(), + "notes.*.fields.field.flag".to_owned(), + "notes.*.tags.*".to_owned(), + ]), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = base + .translation_coverage(&overlay) + .expect("translation overlay has coverage"); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::UntranslatedFallback + && entry.path == "notes.note.finland.fields.field.flag-similarity.message.2" + && entry.source == "blue background with a white cross" + })); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::StaleDirectKey + && entry.path == "translations.direct.stale qualifier" + })); + + let report = base + .compose(&[overlay]) + .expect_err("strict structured message components fail"); + assert!(report.errors.iter().any(|error| { + error.kind == ComposeErrorKind::MissingTranslation + && error.path == "notes.note.finland.fields.field.flag-similarity.message.2" + && error + .message + .contains("missing direct or contextual translation") + })); + assert!(report.errors.iter().any(|error| { + error.kind == ComposeErrorKind::StaleTranslationEntry + && error.path == "translations.direct.stale qualifier" + })); +} + #[test] fn extension_overlay_can_add_a_note_type_field_and_backfill_note_values() { let base = ug_style_deck(); @@ -1306,6 +1489,7 @@ fn ug_style_deck() -> CanonicalDeck { (sid("field.capital"), "Helsinki".to_owned()), (sid("field.flag"), "".to_owned()), ]), + field_messages: BTreeMap::new(), tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), adapter_ids: note_adapter_ids, }; @@ -1333,6 +1517,73 @@ fn ug_style_deck() -> CanonicalDeck { } } +fn ug_style_deck_with_flag_similarity_message() -> CanonicalDeck { + let mut deck = ug_style_deck(); + deck.note_types + .get_mut(&sid("note-type.country")) + .unwrap() + .fields + .push(FieldDefinition { + id: sid("field.flag-similarity"), + name: "Flag similarity".to_owned(), + }); + deck.notes.insert( + sid("note.iceland"), + Note { + id: sid("note.iceland"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Iceland".to_owned()), + (sid("field.capital"), "Reykjavik".to_owned()), + (sid("field.flag"), "".to_owned()), + (sid("field.flag-similarity"), String::new()), + ]), + field_messages: BTreeMap::new(), + tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), + adapter_ids: AdapterIds::new(), + }, + ); + deck.notes.insert( + sid("note.norway"), + Note { + id: sid("note.norway"), + note_type_id: sid("note-type.country"), + variables: BTreeMap::new(), + fields: BTreeMap::from([ + (sid("field.country"), "Norway".to_owned()), + (sid("field.capital"), "Oslo".to_owned()), + (sid("field.flag"), "".to_owned()), + (sid("field.flag-similarity"), String::new()), + ]), + field_messages: BTreeMap::new(), + tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), + adapter_ids: AdapterIds::new(), + }, + ); + let finland = deck.notes.get_mut(&sid("note.finland")).unwrap(); + finland + .fields + .insert(sid("field.flag-similarity"), String::new()); + finland.field_messages.insert( + sid("field.flag-similarity"), + StructuredMessage { + components: vec![ + MessageComponent::FieldRef("notes.note.iceland.fields.field.country".to_owned()), + MessageComponent::Literal(" (".to_owned()), + MessageComponent::Text("blue background with a white cross".to_owned()), + MessageComponent::Literal("), ".to_owned()), + MessageComponent::FieldRef("notes.note.norway.fields.field.country".to_owned()), + MessageComponent::Literal(" (".to_owned()), + MessageComponent::Text("red background with a blue cross".to_owned()), + MessageComponent::Literal(")".to_owned()), + ], + }, + ); + deck.resolve_structured_messages() + .expect("structured message resolves") +} + fn sweden_note() -> Note { Note { id: sid("note.sweden"), @@ -1343,6 +1594,7 @@ fn sweden_note() -> Note { (sid("field.capital"), "Stockholm".to_owned()), (sid("field.flag"), "".to_owned()), ]), + field_messages: BTreeMap::new(), tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), adapter_ids: AdapterIds::new(), } diff --git a/crates/brain-brew-core/tests/semantic_diff.rs b/crates/brain-brew-core/tests/semantic_diff.rs index e47b9fc..bc90ea5 100644 --- a/crates/brain-brew-core/tests/semantic_diff.rs +++ b/crates/brain-brew-core/tests/semantic_diff.rs @@ -103,6 +103,7 @@ fn ug_style_deck() -> CanonicalDeck { (sid("field.capital"), "Helsinki".to_owned()), (sid("field.flag"), "".to_owned()), ]), + field_messages: BTreeMap::new(), tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), adapter_ids: AdapterIds::new(), }; @@ -137,6 +138,7 @@ fn sweden_note() -> Note { (sid("field.capital"), "Stockholm".to_owned()), (sid("field.flag"), "".to_owned()), ]), + field_messages: BTreeMap::new(), tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), adapter_ids: AdapterIds::new(), } diff --git a/crates/brain-brew-formats/src/canonical_yaml.rs b/crates/brain-brew-formats/src/canonical_yaml.rs index 0ea69e2..b0ce07b 100644 --- a/crates/brain-brew-formats/src/canonical_yaml.rs +++ b/crates/brain-brew-formats/src/canonical_yaml.rs @@ -4,8 +4,9 @@ use std::fmt::{self, Write as _}; use brain_brew_core::{ AdapterIdChange, AdapterIds, CanonicalDeck, CardTemplate, CardTemplateChange, ChangeIntent, DeckChange, ExpectedBase, FieldChange, FieldDefinition, FieldDefinitionChange, InvalidStableId, - MediaChange, MediaReference, Note, NoteChange, NoteType, NoteTypeChange, Overlay, OverlayKind, - PropertyChange, StableId, TagChange, TranslationDictionary, ValidationReport, + MediaChange, MediaReference, MessageComponent, Note, NoteChange, NoteType, NoteTypeChange, + Overlay, OverlayKind, PropertyChange, StableId, StructuredMessage, TagChange, + TranslationDictionary, ValidationReport, }; use serde::Deserialize; @@ -109,8 +110,12 @@ pub fn to_string(deck: &CanonicalDeck) -> Result { write_variables(&mut out, " ", ¬e.variables); writeln!(out, " fields:").expect("writing to a string cannot fail"); for (field_id, value) in ¬e.fields { - writeln!(out, " {field_id}: {}", yaml_scalar(value)) - .expect("writing to a string cannot fail"); + if let Some(message) = note.field_messages.get(field_id) { + write_structured_message_field(&mut out, " ", field_id, message); + } else { + writeln!(out, " {field_id}: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } } writeln!(out, " tags:").expect("writing to a string cannot fail"); for tag in ¬e.tags { @@ -825,8 +830,12 @@ fn write_note_payload(out: &mut String, indent: &str, note: &Note) { write_variables(out, indent, ¬e.variables); writeln!(out, "{indent}fields:").expect("writing to a string cannot fail"); for (field_id, value) in ¬e.fields { - writeln!(out, "{indent} {field_id}: {}", yaml_scalar(value)) - .expect("writing to a string cannot fail"); + if let Some(message) = note.field_messages.get(field_id) { + write_structured_message_field(out, &format!("{indent} "), field_id, message); + } else { + writeln!(out, "{indent} {field_id}: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } } writeln!(out, "{indent}tags:").expect("writing to a string cannot fail"); for tag in ¬e.tags { @@ -835,6 +844,32 @@ fn write_note_payload(out: &mut String, indent: &str, note: &Note) { write_adapter_ids(out, indent, ¬e.adapter_ids); } +fn write_structured_message_field( + out: &mut String, + indent: &str, + field_id: &StableId, + message: &StructuredMessage, +) { + writeln!(out, "{indent}{field_id}:").expect("writing to a string cannot fail"); + writeln!(out, "{indent} message:").expect("writing to a string cannot fail"); + for component in &message.components { + match component { + MessageComponent::Literal(value) => { + writeln!(out, "{indent} - literal: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + MessageComponent::Text(value) => { + writeln!(out, "{indent} - text: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + MessageComponent::FieldRef(reference) => { + writeln!(out, "{indent} - ref: {}", yaml_scalar(reference)) + .expect("writing to a string cannot fail"); + } + } + } +} + fn write_card_template_payload(out: &mut String, indent: &str, template: &CardTemplate) { writeln!(out, "{indent}name: {}", yaml_scalar(&template.name)) .expect("writing to a string cannot fail"); @@ -1732,7 +1767,7 @@ struct CanonicalDeckYaml { impl CanonicalDeckYaml { fn into_deck(self) -> Result { - Ok(CanonicalDeck { + let deck = CanonicalDeck { id: sid(&self.deck.id)?, name: self.deck.name, description: self.deck.description, @@ -1767,7 +1802,9 @@ impl CanonicalDeckYaml { .map(|id| sid(&id)) .collect::, _>>()?, adapter_ids: adapter_ids_from_map(self.deck.adapter_ids), - }) + }; + deck.resolve_structured_messages() + .map_err(CanonicalYamlError::Validation) } } @@ -1858,7 +1895,7 @@ struct NoteYaml { note_type_id: String, #[serde(default)] variables: BTreeMap, - fields: BTreeMap, + fields: BTreeMap, #[serde(default)] tags: BTreeSet, #[serde(default)] @@ -1867,21 +1904,94 @@ struct NoteYaml { impl NoteYaml { fn into_note(self, id: StableId) -> Result { + let mut fields = BTreeMap::new(); + let mut field_messages = BTreeMap::new(); + for (field_id, value) in self.fields { + let field_id = sid(&field_id)?; + match value { + FieldValueYaml::Scalar(value) => { + fields.insert(field_id, value); + } + FieldValueYaml::Message(message) => { + fields.insert(field_id.clone(), String::new()); + field_messages.insert(field_id, message.into_structured_message()); + } + } + } Ok(Note { id, note_type_id: sid(&self.note_type_id)?, variables: self.variables, - fields: self - .fields - .into_iter() - .map(|(id, value)| Ok((sid(&id)?, value))) - .collect::>()?, + fields, + field_messages, tags: self.tags, adapter_ids: adapter_ids_from_map(self.adapter_ids), }) } } +#[derive(Deserialize)] +#[serde(untagged)] +enum FieldValueYaml { + Scalar(String), + Message(FieldMessageYaml), +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct FieldMessageYaml { + message: Vec, +} + +impl FieldMessageYaml { + fn into_structured_message(self) -> StructuredMessage { + StructuredMessage { + components: self + .message + .into_iter() + .map(MessageComponentYaml::into_component) + .collect(), + } + } +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum MessageComponentYaml { + Literal(LiteralComponentYaml), + Text(TextComponentYaml), + Reference(ReferenceComponentYaml), +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct LiteralComponentYaml { + literal: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct TextComponentYaml { + text: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ReferenceComponentYaml { + #[serde(rename = "ref")] + reference: String, +} + +impl MessageComponentYaml { + fn into_component(self) -> MessageComponent { + match self { + Self::Literal(component) => MessageComponent::Literal(component.literal), + Self::Text(component) => MessageComponent::Text(component.text), + Self::Reference(component) => MessageComponent::FieldRef(component.reference), + } + } +} + #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct MediaYaml { diff --git a/crates/brain-brew-formats/src/crowdanki.rs b/crates/brain-brew-formats/src/crowdanki.rs index ab337dc..6e90b9c 100644 --- a/crates/brain-brew-formats/src/crowdanki.rs +++ b/crates/brain-brew-formats/src/crowdanki.rs @@ -1074,6 +1074,7 @@ impl CrowdAnkiNoteJson { note_type_id, variables: BTreeMap::new(), fields, + field_messages: BTreeMap::new(), tags: self.tags.into_iter().collect(), adapter_ids, }, diff --git a/crates/brain-brew-formats/src/source_includes.rs b/crates/brain-brew-formats/src/source_includes.rs index 814bc6e..e308053 100644 --- a/crates/brain-brew-formats/src/source_includes.rs +++ b/crates/brain-brew-formats/src/source_includes.rs @@ -373,6 +373,9 @@ fn is_scalar_content_path(path: &[String]) -> bool { ) { return true; } + if matches!(last, "text" | "literal") && path.iter().any(|segment| segment == "message") { + return true; + } if path.iter().any(|segment| segment == "variables") && last != "variables" { return true; } diff --git a/crates/brain-brew-formats/tests/canonical_yaml.rs b/crates/brain-brew-formats/tests/canonical_yaml.rs index 15ac2f3..d18acba 100644 --- a/crates/brain-brew-formats/tests/canonical_yaml.rs +++ b/crates/brain-brew-formats/tests/canonical_yaml.rs @@ -90,6 +90,77 @@ tombstones: [] assert_eq!(change.after.as_deref(), Some("Helsingfors")); } +#[test] +fn parses_formats_and_resolves_structured_message_fields() { + let yaml = r#"deck: + id: deck.structured-message + name: Structured Message Fixture + description: Structured message translation fixture. + adapter_ids: {} +note_types: + note-type.country: + name: Country + field_order: + - field.country + - field.flag-similarity + fields: + field.country: + name: Country + field.flag-similarity: + name: Flag similarity + card_template_order: + - template.flag-country + card_templates: + template.flag-country: + name: Flag - Country + question_format: '{{Country}}' + answer_format: '{{Flag similarity}}' + adapter_ids: {} + styling: '' + adapter_ids: {} +notes: + note.finland: + note_type_id: note-type.country + fields: + field.country: Finland + field.flag-similarity: + message: + - ref: notes.note.iceland.fields.field.country + - literal: ' (' + - text: blue background with a white cross + - literal: ')' + tags: [] + adapter_ids: {} + note.iceland: + note_type_id: note-type.country + fields: + field.country: Iceland + field.flag-similarity: '' + tags: [] + adapter_ids: {} +media: {} +tombstones: [] +"#; + + let deck = canonical_yaml::from_str(yaml).expect("structured message yaml parses"); + + assert_eq!( + deck.notes[&sid("note.finland")].fields[&sid("field.flag-similarity")], + "Iceland (blue background with a white cross)" + ); + assert_eq!( + deck.notes[&sid("note.finland")].field_messages[&sid("field.flag-similarity")] + .components + .len(), + 4 + ); + + let formatted = canonical_yaml::to_string(&deck).expect("structured message emits"); + assert!(formatted.contains("field.flag-similarity:\n message:")); + assert!(formatted.contains(" - ref: notes.note.iceland.fields.field.country")); + assert!(formatted.contains(" - text: blue background with a white cross")); +} + #[test] fn translation_dictionary_overlay_translates_variables_fields_and_adapter_ids() { let deck = canonical_yaml::from_str( @@ -391,6 +462,7 @@ fn ug_style_deck() -> CanonicalDeck { (sid("field.capital"), "Helsinki".to_owned()), (sid("field.flag"), "".to_owned()), ]), + field_messages: BTreeMap::new(), tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), adapter_ids: note_adapter_ids, }; diff --git a/crates/brain-brew-formats/tests/crowdanki.rs b/crates/brain-brew-formats/tests/crowdanki.rs index a10058f..ab421e5 100644 --- a/crates/brain-brew-formats/tests/crowdanki.rs +++ b/crates/brain-brew-formats/tests/crowdanki.rs @@ -404,6 +404,7 @@ fn ug_style_deck() -> CanonicalDeck { (sid("field.capital"), "Helsinki".to_owned()), (sid("field.flag"), "".to_owned()), ]), + field_messages: BTreeMap::new(), tags: BTreeSet::from(["Europe".to_owned(), "Nordic".to_owned()]), adapter_ids: note_adapter_ids, }; diff --git a/crates/brain-brew-formats/tests/media_references.rs b/crates/brain-brew-formats/tests/media_references.rs index 26d99b8..bfb06fb 100644 --- a/crates/brain-brew-formats/tests/media_references.rs +++ b/crates/brain-brew-formats/tests/media_references.rs @@ -175,6 +175,7 @@ fn media_deck() -> CanonicalDeck { (sid("field.country"), "Finland".to_owned()), (sid("field.flag"), "".to_owned()), ]), + field_messages: BTreeMap::new(), tags: BTreeSet::new(), adapter_ids: AdapterIds::new(), }; diff --git a/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs index 4c21b52..03883e2 100644 --- a/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs +++ b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs @@ -1006,6 +1006,7 @@ fn ug_regression_note_changes_flow_to_crowdanki_for_every_target() { note_type_id: note_type.id.clone(), variables: BTreeMap::new(), fields, + field_messages: BTreeMap::new(), tags: BTreeSet::from(["ZZZ::Regression".to_owned()]), adapter_ids, }; diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index a979ab7..63c7c5f 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -86,6 +86,57 @@ Use `no_change` for names or phrases that should stay identical everywhere they Keep `ignore_paths` for structural paths that translators should not review at all, such as tags, flag/map HTML, or adapter metadata. Use `no_change` for translator-facing text that was reviewed and intentionally left as-is. +## Structured messages for composite fields + +Use a structured field message when a field value is genuinely assembled from reusable translated pieces, such as a list of country names plus small qualifier fragments. Do not use it for every ordinary sentence; plain scalar strings are easier to read and translate when the whole sentence is the natural translation unit. + +Base source: + +```yaml +notes: + note.finland: + fields: + field.flag-similarity: + message: + - ref: notes.note.iceland.fields.field.country + - literal: ' (' + - text: blue background with a white cross + - literal: '), ' + - ref: notes.note.norway.fields.field.country + - literal: ' (' + - text: red background with a blue cross + - literal: ')' +``` + +Components mean: + +- `ref` points at another note field and can reuse that field's existing translation, such as a country-name entry in `translations.direct`; +- `text` is an editable translatable fragment and appears in translation coverage at a component path such as `notes.note.finland.fields.field.flag-similarity.message.2`; +- `literal` is non-translatable glue for punctuation, spaces, or markup. + +Translation overlay: + +```yaml +translations: + direct: + Iceland: Island + Norway: Norge + red background with a blue cross: rød bakgrunn med blått kors + contextual: + notes.note.finland: + blue background with a white cross: blå bakgrunn med hvitt kors +``` + +The resolved field exported to Anki is still a plain string: + +```text +Island (blå bakgrunn med hvitt kors), Norge (rød bakgrunn med blått kors) +``` + +Strict coverage reports missing and stale entries for each `text` or `ref` component instead of requiring one long key for the whole composite field. The translator context view shows the resolved message plus its components so translators can edit the reusable country names and qualifier fragments separately. If a target language needs a special whole-field wording, add a contextual translation for the full resolved source string at the field or note context; that full override replaces the component-composed output for that target. + +Coordinate with deck maintainers before migrating existing large fields: structured messages are best for repeated, composite source text where component reuse clearly reduces duplication. + ## Target-language additions for blank source fields Use `translations.target_additions` only when blank localized text genuinely belongs to the translation overlay. This is valid when the source deck intentionally has no English text for the field but a target language should supply text. diff --git a/documentation/docs/reference/yaml.md b/documentation/docs/reference/yaml.md index bb2819a..df9ba45 100644 --- a/documentation/docs/reference/yaml.md +++ b/documentation/docs/reference/yaml.md @@ -53,6 +53,24 @@ kind: patch # corrections or adjustments kind: personal # learner/local source content ``` +## Structured field messages + +Note fields are usually scalar strings. For genuinely composite text that should reuse existing translations, a field may use a structured `message` instead: + +```yaml +notes: + note.finland: + fields: + field.flag-similarity: + message: + - ref: notes.note.iceland.fields.field.country + - literal: ' (' + - text: blue background with a white cross + - literal: ')' +``` + +`ref` resolves another note field before export, `text` is extracted for translation coverage, and `literal` is non-translatable glue. Adapter exports receive a plain resolved string. + ## Translation dictionary ```yaml From 7fa52ece1a45296311e82ecbc398f6f6b37cfef9 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Tue, 23 Jun 2026 07:31:36 +0200 Subject: [PATCH 035/142] migrate UG flag similarity messages --- crates/brain-brew-cli/src/overlay_draft.rs | 1 + crates/brain-brew-core/src/lib.rs | 9 +- .../brain-brew-core/tests/overlay_compose.rs | 2 + .../brain-brew-formats/src/canonical_yaml.rs | 67 +++- .../tests/ultimate_geography_fixture.rs | 23 ++ fixtures/ultimate-geography/deck.yaml | 356 +++++++++++++++--- .../overlays/extensions/hardcore.yaml | 7 +- .../extensions/hardcore/translations/de.yaml | 3 +- .../extensions/hardcore/translations/es.yaml | 3 +- .../extensions/hardcore/translations/fr.yaml | 3 +- .../extensions/hardcore/translations/nb.yaml | 3 +- 11 files changed, 414 insertions(+), 63 deletions(-) diff --git a/crates/brain-brew-cli/src/overlay_draft.rs b/crates/brain-brew-cli/src/overlay_draft.rs index 53d6fc3..65db320 100644 --- a/crates/brain-brew-cli/src/overlay_draft.rs +++ b/crates/brain-brew-cli/src/overlay_draft.rs @@ -126,6 +126,7 @@ fn draft_note_changes(left: &CanonicalDeck, right: &CanonicalDeck, overlay: &mut FieldChange { intent: ChangeIntent::Replace, value: Some(right_value.clone()), + message: None, expected_base: Some(ExpectedBase::Value(left_value.clone())), }, ); diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs index 033adcf..468e56f 100644 --- a/crates/brain-brew-core/src/lib.rs +++ b/crates/brain-brew-core/src/lib.rs @@ -2783,11 +2783,17 @@ fn apply_field_change( | ChangeIntent::Merge | ChangeIntent::Replace | ChangeIntent::Override => { + if let Some(message) = &change.message { + note.fields.insert(field_id.clone(), String::new()); + note.field_messages + .insert(field_id.clone(), message.clone()); + return; + } let Some(value) = &change.value else { errors.push(ComposeError::new( ComposeErrorKind::MissingOverlayPayload, path, - format!("field change for {field_id} must include a value"), + format!("field change for {field_id} must include a value or message"), )); return; }; @@ -3759,6 +3765,7 @@ pub struct MediaChange { pub struct FieldChange { pub intent: ChangeIntent, pub value: Option, + pub message: Option, pub expected_base: Option, } diff --git a/crates/brain-brew-core/tests/overlay_compose.rs b/crates/brain-brew-core/tests/overlay_compose.rs index ad2c108..9b3a795 100644 --- a/crates/brain-brew-core/tests/overlay_compose.rs +++ b/crates/brain-brew-core/tests/overlay_compose.rs @@ -997,6 +997,7 @@ fn extension_overlay_can_add_a_note_type_field_and_backfill_note_values() { FieldChange { intent: ChangeIntent::Add, value: Some("5.6 million".to_owned()), + message: None, expected_base: None, }, )]), @@ -1431,6 +1432,7 @@ fn overlay_replacing_capital( FieldChange { intent, value: Some(value.to_owned()), + message: None, expected_base, }, )]), diff --git a/crates/brain-brew-formats/src/canonical_yaml.rs b/crates/brain-brew-formats/src/canonical_yaml.rs index b0ce07b..a8711aa 100644 --- a/crates/brain-brew-formats/src/canonical_yaml.rs +++ b/crates/brain-brew-formats/src/canonical_yaml.rs @@ -339,6 +339,12 @@ struct FieldAdditionsForFormat { values: BTreeMap>, } +#[derive(Clone)] +enum FieldFillForFormat { + Scalar(String), + Message(StructuredMessage), +} + fn split_field_additions_for_format( overlay: &Overlay, ) -> ( @@ -432,10 +438,10 @@ fn split_field_additions_for_format( fn split_field_fills_for_format( mut note_changes: BTreeMap, ) -> ( - BTreeMap>, + BTreeMap>, BTreeMap, ) { - let mut field_fills = BTreeMap::>::new(); + let mut field_fills = BTreeMap::>::new(); for (note_id, change) in ¬e_changes { if change.intent != ChangeIntent::Merge { @@ -446,7 +452,7 @@ fn split_field_fills_for_format( field_fills .entry(note_id.clone()) .or_default() - .insert(field_id.clone(), value.to_owned()); + .insert(field_id.clone(), value); } } } @@ -464,11 +470,18 @@ fn split_field_fills_for_format( (field_fills, note_changes) } -fn field_fill_value(change: &FieldChange) -> Option<&str> { +fn field_fill_value(change: &FieldChange) -> Option { if change.intent == ChangeIntent::Replace && matches!(change.expected_base, Some(ExpectedBase::Value(ref value)) if value.is_empty()) { - change.value.as_deref() + if let Some(message) = &change.message { + Some(FieldFillForFormat::Message(message.clone())) + } else { + change + .value + .as_ref() + .map(|value| FieldFillForFormat::Scalar(value.clone())) + } } else { None } @@ -523,14 +536,21 @@ fn write_field_additions( fn write_field_fills( out: &mut String, - field_fills: &BTreeMap>, + field_fills: &BTreeMap>, ) { writeln!(out, "field_fills:").expect("writing to a string cannot fail"); for (note_id, fields) in field_fills { writeln!(out, " {note_id}:").expect("writing to a string cannot fail"); for (field_id, value) in fields { - writeln!(out, " {field_id}: {}", yaml_scalar(value)) - .expect("writing to a string cannot fail"); + match value { + FieldFillForFormat::Scalar(value) => { + writeln!(out, " {field_id}: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + FieldFillForFormat::Message(message) => { + write_structured_message_field(out, " ", field_id, message); + } + } } } } @@ -767,6 +787,10 @@ fn write_field_change(out: &mut String, indent: &str, field_id: &StableId, chang if let Some(value) = &change.value { write_multiline_or_scalar(out, &format!("{indent} "), "value", value); } + if let Some(message) = &change.message { + writeln!(out, "{indent} message:").expect("writing to a string cannot fail"); + write_message_components(out, &format!("{indent} "), message); + } if let Some(expected_base) = &change.expected_base { write_expected_base(out, &format!("{indent} "), expected_base); } @@ -852,18 +876,22 @@ fn write_structured_message_field( ) { writeln!(out, "{indent}{field_id}:").expect("writing to a string cannot fail"); writeln!(out, "{indent} message:").expect("writing to a string cannot fail"); + write_message_components(out, &format!("{indent} "), message); +} + +fn write_message_components(out: &mut String, indent: &str, message: &StructuredMessage) { for component in &message.components { match component { MessageComponent::Literal(value) => { - writeln!(out, "{indent} - literal: {}", yaml_scalar(value)) + writeln!(out, "{indent}- literal: {}", yaml_scalar(value)) .expect("writing to a string cannot fail"); } MessageComponent::Text(value) => { - writeln!(out, "{indent} - text: {}", yaml_scalar(value)) + writeln!(out, "{indent}- text: {}", yaml_scalar(value)) .expect("writing to a string cannot fail"); } MessageComponent::FieldRef(reference) => { - writeln!(out, "{indent} - ref: {}", yaml_scalar(reference)) + writeln!(out, "{indent}- ref: {}", yaml_scalar(reference)) .expect("writing to a string cannot fail"); } } @@ -1009,7 +1037,7 @@ struct OverlayYaml { #[serde(default)] field_additions: BTreeMap, #[serde(default)] - field_fills: BTreeMap>, + field_fills: BTreeMap>, #[serde(default)] notes: BTreeMap, #[serde(default)] @@ -1152,6 +1180,7 @@ impl FieldAdditionsYaml { FieldChange { intent: ChangeIntent::Add, value: Some(value), + message: None, expected_base: None, }, ); @@ -1163,7 +1192,7 @@ impl FieldAdditionsYaml { } fn apply_field_fills( - field_fills: BTreeMap>, + field_fills: BTreeMap>, note_changes: &mut BTreeMap, ) -> Result<(), CanonicalYamlError> { for (note_id, fields) in field_fills { @@ -1188,11 +1217,16 @@ fn apply_field_fills( "field_fills.{note_id}.{field_id} conflicts with another field change" ))); } + let (value, message) = match value { + FieldValueYaml::Scalar(value) => (Some(value), None), + FieldValueYaml::Message(message) => (None, Some(message.into_structured_message())), + }; note_change.fields.insert( field_id, FieldChange { intent: ChangeIntent::Replace, - value: Some(value), + value, + message, expected_base: Some(ExpectedBase::Value(String::new())), }, ); @@ -1650,6 +1684,8 @@ struct FieldChangeYaml { #[serde(default)] value: Option, #[serde(default)] + message: Option>, + #[serde(default)] expected_base: Option, } @@ -1658,6 +1694,9 @@ impl FieldChangeYaml { Ok(FieldChange { intent: parse_change_intent(&self.intent)?, value: self.value, + message: self + .message + .map(|message| FieldMessageYaml { message }.into_structured_message()), expected_base: self .expected_base .map(ExpectedBaseYaml::into_expected_base) diff --git a/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs index 03883e2..45218bf 100644 --- a/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs +++ b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs @@ -77,6 +77,26 @@ fn ultimate_geography_fixture_uses_file_includes_for_large_source_text() { ); } +#[test] +fn ultimate_geography_fixture_uses_structured_messages_for_flag_similarity() { + let root = fixture_root(); + for relative_path in ["deck.yaml", "overlays/extensions/hardcore.yaml"] { + let source = fs::read_to_string(root.join(relative_path)).unwrap(); + assert!( + source.contains("field.flag-similarity:\n message:") + || source.contains("field.flag-similarity:\n message:"), + "{relative_path} should use structured messages for non-empty flag similarity fields" + ); + assert!( + !source.lines().any( + |line| line.trim_start().starts_with("field.flag-similarity: '") + && line.trim() != "field.flag-similarity: ''" + ), + "{relative_path} should not keep long scalar flag similarity strings" + ); + } +} + #[test] fn ultimate_geography_fixture_manifest_composes_all_targets() { let root = fixture_root(); @@ -608,6 +628,7 @@ fn ug_regression_field_definition_changes_flow_to_crowdanki_for_every_target() { FieldChange { intent: ChangeIntent::Add, value: Some(finland_value.clone()), + message: None, expected_base: None, }, ); @@ -860,6 +881,7 @@ fn ug_regression_note_changes_flow_to_crowdanki_for_every_target() { FieldChange { intent: ChangeIntent::Override, value: Some(new_value.clone()), + message: None, expected_base: Some(ExpectedBase::Value(current_value)), }, ); @@ -893,6 +915,7 @@ fn ug_regression_note_changes_flow_to_crowdanki_for_every_target() { FieldChange { intent: ChangeIntent::Override, value: Some("${regression.country}".to_owned()), + message: None, expected_base: Some(ExpectedBase::Value(current_value)), }, ); diff --git a/fixtures/ultimate-geography/deck.yaml b/fixtures/ultimate-geography/deck.yaml index 3771e0a..22ddd24 100644 --- a/fixtures/ultimate-geography/deck.yaml +++ b/fixtures/ultimate-geography/deck.yaml @@ -241,7 +241,12 @@ notes: field.country: Andorra field.country-info: '' field.flag: '' - field.flag-similarity: 'Moldova (wider, coat of arms with eagle)' + field.flag-similarity: + message: + - ref: notes.note.moldova.fields.field.country + - literal: ' (' + - text: 'wider, coat of arms with eagle' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -444,7 +449,12 @@ notes: field.country: Australia field.country-info: '' field.flag: '' - field.flag-similarity: 'New Zealand (red stars, two fewer stars)' + field.flag-similarity: + message: + - ref: notes.note.new-zealand.fields.field.country + - literal: ' (' + - text: 'red stars, two fewer stars' + - literal: ')' field.map: '' tags: - 'UG::Oceania' @@ -460,7 +470,12 @@ notes: field.country: Austria field.country-info: '' field.flag: '' - field.flag-similarity: 'Latvia (darker red, narrower white band)' + field.flag-similarity: + message: + - ref: notes.note.latvia.fields.field.country + - literal: ' (' + - text: 'darker red, narrower white band' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -508,7 +523,12 @@ notes: field.country: Bahrain field.country-info: '' field.flag: '' - field.flag-similarity: 'Qatar (wider, more serrated edges, maroon)' + field.flag-similarity: + message: + - ref: notes.note.qatar.fields.field.country + - literal: ' (' + - text: 'wider, more serrated edges, maroon' + - literal: ')' field.map: '' tags: - 'UG::Asia' @@ -791,7 +811,12 @@ notes: field.country: Bolivia field.country-info: '' field.flag: '' - field.flag-similarity: 'Ghana (star instead of coat of arms)' + field.flag-similarity: + message: + - ref: notes.note.ghana.fields.field.country + - literal: ' (' + - text: 'star instead of coat of arms' + - literal: ')' field.map: '' tags: - 'UG::South_America' @@ -972,7 +997,12 @@ notes: field.country: Cameroon field.country-info: '' field.flag: '' - field.flag-similarity: 'Senegal (green/yellow/red, green star)' + field.flag-similarity: + message: + - ref: notes.note.senegal.fields.field.country + - literal: ' (' + - text: 'green/yellow/red, green star' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -1128,7 +1158,12 @@ notes: field.country: Chad field.country-info: '' field.flag: '' - field.flag-similarity: 'Romania (slightly lighter blue)' + field.flag-similarity: + message: + - ref: notes.note.romania.fields.field.country + - literal: ' (' + - text: 'slightly lighter blue' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -1176,7 +1211,12 @@ notes: field.country: Colombia field.country-info: '' field.flag: '' - field.flag-similarity: 'Ecuador (with coat of arms)' + field.flag-similarity: + message: + - ref: notes.note.ecuador.fields.field.country + - literal: ' (' + - text: 'with coat of arms' + - literal: ')' field.map: '' tags: - 'UG::South_America' @@ -1289,7 +1329,12 @@ notes: field.country: Cuba field.country-info: '' field.flag: '' - field.flag-similarity: 'Puerto Rico (blue triangle, red stripes)' + field.flag-similarity: + message: + - ref: notes.note.puerto-rico.fields.field.country + - literal: ' (' + - text: 'blue triangle, red stripes' + - literal: ')' field.map: '' tags: - 'UG::Caribbean' @@ -1306,7 +1351,12 @@ notes: field.country: 'Curaçao' field.country-info: Constituent country of the Kingdom of the Netherlands. field.flag: '' - field.flag-similarity: 'Nauru (single star below yellow band)' + field.flag-similarity: + message: + - ref: notes.note.nauru.fields.field.country + - literal: ' (' + - text: 'single star below yellow band' + - literal: ')' field.map: '' tags: - 'UG::Caribbean' @@ -1502,7 +1552,12 @@ notes: field.country: Ecuador field.country-info: '' field.flag: '' - field.flag-similarity: 'Colombia (no coat of arms)' + field.flag-similarity: + message: + - ref: notes.note.colombia.fields.field.country + - literal: ' (' + - text: 'no coat of arms' + - literal: ')' field.map: '' tags: - 'UG::South_America' @@ -1518,7 +1573,17 @@ notes: field.country: Egypt field.country-info: '' field.flag: '' - field.flag-similarity: 'Iraq (text instead of emblem), Yemen (no emblem)' + field.flag-similarity: + message: + - ref: notes.note.iraq.fields.field.country + - literal: ' (' + - text: 'text instead of emblem' + - literal: ')' + - literal: ', ' + - ref: notes.note.yemen.fields.field.country + - literal: ' (' + - text: 'no emblem' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -1536,7 +1601,12 @@ notes: field.country: El Salvador field.country-info: '' field.flag: '' - field.flag-similarity: 'Nicaragua (different coat of arms, slightly lighter blue)' + field.flag-similarity: + message: + - ref: notes.note.nicaragua.fields.field.country + - literal: ' (' + - text: 'different coat of arms, slightly lighter blue' + - literal: ')' field.map: '' tags: - 'UG::North_America' @@ -1710,7 +1780,17 @@ notes: field.country: Faroe Islands field.country-info: Constituent country in the Kingdom of Denmark. field.flag: '' - field.flag-similarity: 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)' + field.flag-similarity: + message: + - ref: notes.note.iceland.fields.field.country + - literal: ' (' + - text: 'blue background, red and white cross' + - literal: ')' + - literal: ', ' + - ref: notes.note.norway.fields.field.country + - literal: ' (' + - text: 'red background, blue and white cross' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -1871,7 +1951,12 @@ notes: field.country: Ghana field.country-info: '' field.flag: '' - field.flag-similarity: 'Bolivia (coat of arms instead of star)' + field.flag-similarity: + message: + - ref: notes.note.bolivia.fields.field.country + - literal: ' (' + - text: 'coat of arms instead of star' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -2016,7 +2101,12 @@ notes: field.country: Guinea field.country-info: '' field.flag: '' - field.flag-similarity: 'Mali (red and green flipped, slightly brighter green)' + field.flag-similarity: + message: + - ref: notes.note.mali.fields.field.country + - literal: ' (' + - text: 'red and green flipped, slightly brighter green' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -2251,7 +2341,17 @@ notes: field.country: Iceland field.country-info: '' field.flag: '' - field.flag-similarity: 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)' + field.flag-similarity: + message: + - ref: notes.note.norway.fields.field.country + - literal: ' (' + - text: 'red background, blue cross' + - literal: ')' + - literal: ', ' + - ref: notes.note.faroe-islands.fields.field.country + - literal: ' (' + - text: 'white background, red and blue cross' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -2298,7 +2398,17 @@ notes: field.country: Indonesia field.country-info: '' field.flag: '' - field.flag-similarity: 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)' + field.flag-similarity: + message: + - ref: notes.note.monaco.fields.field.country + - literal: ' (' + - text: 'narrower, darker red' + - literal: ')' + - literal: ', ' + - ref: notes.note.poland.fields.field.country + - literal: ' (' + - text: 'red and white flipped, darker red' + - literal: ')' field.map: '' tags: - 'UG::Asia' @@ -2332,7 +2442,17 @@ notes: field.country: Iraq field.country-info: '' field.flag: '' - field.flag-similarity: 'Egypt (emblem instead of text), Yemen (no text)' + field.flag-similarity: + message: + - ref: notes.note.egypt.fields.field.country + - literal: ' (' + - text: 'emblem instead of text' + - literal: ')' + - literal: ', ' + - ref: notes.note.yemen.fields.field.country + - literal: ' (' + - text: 'no text' + - literal: ')' field.map: '' tags: - 'UG::Asia' @@ -2349,7 +2469,12 @@ notes: field.country: Ireland field.country-info: '' field.flag: '' - field.flag-similarity: 'Ivory Coast (green and orange flipped, narrower)' + field.flag-similarity: + message: + - ref: notes.note.ivory-coast.fields.field.country + - literal: ' (' + - text: 'green and orange flipped, narrower' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -2417,7 +2542,12 @@ notes: field.country: Ivory Coast field.country-info: 'Officially Côte d''Ivoire.' field.flag: '' - field.flag-similarity: 'Ireland (orange and green flipped, wider)' + field.flag-similarity: + message: + - ref: notes.note.ireland.fields.field.country + - literal: ' (' + - text: 'orange and green flipped, wider' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -2689,7 +2819,12 @@ notes: field.country: Latvia field.country-info: '' field.flag: '' - field.flag-similarity: 'Austria (brighter red, wider white band)' + field.flag-similarity: + message: + - ref: notes.note.austria.fields.field.country + - literal: ' (' + - text: 'brighter red, wider white band' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -2807,7 +2942,12 @@ notes: field.country: Luxembourg field.country-info: '' field.flag: '' - field.flag-similarity: 'Netherlands (darker blue)' + field.flag-similarity: + message: + - ref: notes.note.netherlands.fields.field.country + - literal: ' (' + - text: 'darker blue' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -2921,7 +3061,12 @@ notes: field.country: Mali field.country-info: '' field.flag: '' - field.flag-similarity: 'Guinea (green and red flipped, slightly darker green)' + field.flag-similarity: + message: + - ref: notes.note.guinea.fields.field.country + - literal: ' (' + - text: 'green and red flipped, slightly darker green' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -3099,7 +3244,12 @@ notes: field.country: Moldova field.country-info: '' field.flag: '' - field.flag-similarity: 'Andorra (narrower, coat of arms with motto)' + field.flag-similarity: + message: + - ref: notes.note.andorra.fields.field.country + - literal: ' (' + - text: 'narrower, coat of arms with motto' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -3115,7 +3265,17 @@ notes: field.country: Monaco field.country-info: '' field.flag: '' - field.flag-similarity: 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)' + field.flag-similarity: + message: + - ref: notes.note.indonesia.fields.field.country + - literal: ' (' + - text: 'wider, brighter red' + - literal: ')' + - literal: ', ' + - ref: notes.note.poland.fields.field.country + - literal: ' (' + - text: 'red and white flipped, wider' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -3232,7 +3392,12 @@ notes: field.country: Nauru field.country-info: '' field.flag: '' - field.flag-similarity: 'Curaçao (two stars in top-left corner)' + field.flag-similarity: + message: + - ref: notes.note.cura-ao.fields.field.country + - literal: ' (' + - text: 'two stars in top-left corner' + - literal: ')' field.map: '' tags: - 'UG::Oceania' @@ -3264,7 +3429,12 @@ notes: field.country: Netherlands field.country-info: '' field.flag: '' - field.flag-similarity: 'Luxembourg (lighter blue)' + field.flag-similarity: + message: + - ref: notes.note.luxembourg.fields.field.country + - literal: ' (' + - text: 'lighter blue' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -3296,7 +3466,12 @@ notes: field.country: New Zealand field.country-info: '' field.flag: '' - field.flag-similarity: 'Australia (white stars, two more stars)' + field.flag-similarity: + message: + - ref: notes.note.australia.fields.field.country + - literal: ' (' + - text: 'white stars, two more stars' + - literal: ')' field.map: '' tags: - 'UG::Oceania' @@ -3312,7 +3487,12 @@ notes: field.country: Nicaragua field.country-info: '' field.flag: '' - field.flag-similarity: 'El Salvador (different coat of arms, slightly darker blue)' + field.flag-similarity: + message: + - ref: notes.note.el-salvador.fields.field.country + - literal: ' (' + - text: 'different coat of arms, slightly darker blue' + - literal: ')' field.map: '' tags: - 'UG::North_America' @@ -3488,7 +3668,17 @@ notes: field.country: Norway field.country-info: '' field.flag: '' - field.flag-similarity: 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)' + field.flag-similarity: + message: + - ref: notes.note.iceland.fields.field.country + - literal: ' (' + - text: 'blue background, red cross' + - literal: ')' + - literal: ', ' + - ref: notes.note.faroe-islands.fields.field.country + - literal: ' (' + - text: 'white background, red and blue cross' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -3598,7 +3788,17 @@ notes: field.country: Palestine field.country-info: '' field.flag: '' - field.flag-similarity: 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)' + field.flag-similarity: + message: + - ref: notes.note.sudan.fields.field.country + - literal: ' (' + - text: 'red/white/black, green arrow' + - literal: ')' + - literal: ', ' + - ref: notes.note.sahrawi-arab-democratic-republic.fields.field.country + - literal: ' (' + - text: 'with star and crescent' + - literal: ')' field.map: '' tags: - 'UG::Asia' @@ -3727,7 +3927,17 @@ notes: field.country: Poland field.country-info: '' field.flag: '' - field.flag-similarity: 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)' + field.flag-similarity: + message: + - ref: notes.note.indonesia.fields.field.country + - literal: ' (' + - text: 'white and red flipped, brighter red' + - literal: ')' + - literal: ', ' + - ref: notes.note.monaco.fields.field.country + - literal: ' (' + - text: 'white and red flipped, narrower' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -3776,7 +3986,12 @@ notes: field.country: Puerto Rico field.country-info: Unincorporated territory of the United States. field.flag: '' - field.flag-similarity: 'Cuba (red triangle, blue stripes)' + field.flag-similarity: + message: + - ref: notes.note.cuba.fields.field.country + - literal: ' (' + - text: 'red triangle, blue stripes' + - literal: ')' field.map: '' tags: - 'UG::Caribbean' @@ -3792,7 +4007,12 @@ notes: field.country: Qatar field.country-info: '' field.flag: '' - field.flag-similarity: 'Bahrain (narrower, fewer serrated edges, red)' + field.flag-similarity: + message: + - ref: notes.note.bahrain.fields.field.country + - literal: ' (' + - text: 'narrower, fewer serrated edges, red' + - literal: ')' field.map: '' tags: - 'UG::Asia' @@ -3856,7 +4076,12 @@ notes: field.country: Romania field.country-info: '' field.flag: '' - field.flag-similarity: 'Chad (slightly darker blue)' + field.flag-similarity: + message: + - ref: notes.note.chad.fields.field.country + - literal: ' (' + - text: 'slightly darker blue' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -3873,7 +4098,12 @@ notes: field.country: Russia field.country-info: '' field.flag: '' - field.flag-similarity: 'Slovakia (with coat of arms)' + field.flag-similarity: + message: + - ref: notes.note.slovakia.fields.field.country + - literal: ' (' + - text: 'with coat of arms' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -3922,7 +4152,12 @@ notes: field.country: Sahrawi Arab Democratic Republic field.country-info: Partially recognised state claimed by Morocco. Also known as Western Sahara. field.flag: '' - field.flag-similarity: 'Palestine (no symbol)' + field.flag-similarity: + message: + - ref: notes.note.palestine.fields.field.country + - literal: ' (' + - text: 'no symbol' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -4144,7 +4379,12 @@ notes: field.country: Senegal field.country-info: '' field.flag: '' - field.flag-similarity: 'Cameroon (green/red/yellow, yellow star)' + field.flag-similarity: + message: + - ref: notes.note.cameroon.fields.field.country + - literal: ' (' + - text: 'green/red/yellow, yellow star' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -4259,7 +4499,17 @@ notes: field.country: Slovakia field.country-info: '' field.flag: '' - field.flag-similarity: 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)' + field.flag-similarity: + message: + - ref: notes.note.russia.fields.field.country + - literal: ' (' + - text: 'no coat of arms' + - literal: ')' + - literal: ', ' + - ref: notes.note.slovenia.fields.field.country + - literal: ' (' + - text: 'wider, smaller coat of arms' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -4276,7 +4526,12 @@ notes: field.country: Slovenia field.country-info: '' field.flag: '' - field.flag-similarity: 'Slovakia (narrower, bigger coat of arms)' + field.flag-similarity: + message: + - ref: notes.note.slovakia.fields.field.country + - literal: ' (' + - text: 'narrower, bigger coat of arms' + - literal: ')' field.map: '' tags: - 'UG::Europe' @@ -4488,7 +4743,12 @@ notes: field.country: Sudan field.country-info: '' field.flag: '' - field.flag-similarity: 'Palestine (black/white/green, red arrow)' + field.flag-similarity: + message: + - ref: notes.note.palestine.fields.field.country + - literal: ' (' + - text: 'black/white/green, red arrow' + - literal: ')' field.map: '' tags: - 'UG::Africa' @@ -5152,7 +5412,17 @@ notes: field.country: Yemen field.country-info: '' field.flag: '' - field.flag-similarity: 'Egypt (with emblem), Iraq (with text)' + field.flag-similarity: + message: + - ref: notes.note.egypt.fields.field.country + - literal: ' (' + - text: 'with emblem' + - literal: ')' + - literal: ', ' + - ref: notes.note.iraq.fields.field.country + - literal: ' (' + - text: 'with text' + - literal: ')' field.map: '' tags: - 'UG::Asia' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore.yaml index 8188046..949adbf 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore.yaml @@ -172,7 +172,12 @@ notes: field.country: 'Galápagos Islands' field.country-info: Province of Ecuador. field.flag: '' - field.flag-similarity: 'Sierra Leone (slightly lighter blue)' + field.flag-similarity: + message: + - ref: notes.note.sierra-leone.fields.field.country + - literal: ' (' + - text: slightly lighter blue + - literal: ')' field.map: '' tags: - 'UG::South_America' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml index ee130c5..1c290d6 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/de.yaml @@ -1,6 +1,8 @@ id: overlay.translation.hardcore.de kind: translation translations: + direct: + slightly lighter blue: etwas helleres Blau contextual: notes.note: bonaire: @@ -22,7 +24,6 @@ translations: gal-pagos-islands: 'Galápagos Islands': Galapagosinseln Province of Ecuador.: Ecuadorianische Provinz. - 'Sierra Leone (slightly lighter blue)': 'Sierra Leone (etwas helleres Blau)' melilla: Autonomous city of Spain: Autonome spanische Stadt. montserrat: diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml index d5aca1a..1639e60 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/es.yaml @@ -1,6 +1,8 @@ id: overlay.translation.hardcore.es kind: translation translations: + direct: + slightly lighter blue: 'azul ligeramente más claro' contextual: notes.note: bonaire: @@ -22,7 +24,6 @@ translations: gal-pagos-islands: 'Galápagos Islands': 'Islas Galápagos' Province of Ecuador.: Provincia de Ecuador. - 'Sierra Leone (slightly lighter blue)': 'Sierra Leona (azul ligeramente más claro)' melilla: Autonomous city of Spain: 'Ciudad autónoma de España.' montserrat: diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml index 7f607a1..42c22db 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/fr.yaml @@ -1,6 +1,8 @@ id: overlay.translation.hardcore.fr kind: translation translations: + direct: + slightly lighter blue: 'bleu légèrement plus clair' contextual: notes.note: bonaire: @@ -22,7 +24,6 @@ translations: gal-pagos-islands: 'Galápagos Islands': 'Îles Galápagos' Province of Ecuador.: 'Province de l''Équateur.' - 'Sierra Leone (slightly lighter blue)': 'Sierra Leone (bleu légèrement plus clair)' melilla: Autonomous city of Spain: 'Ville autonome d’Espagne.' montserrat: diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml index a8dc730..34cf71d 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore/translations/nb.yaml @@ -1,6 +1,8 @@ id: overlay.translation.hardcore.nb kind: translation translations: + direct: + slightly lighter blue: 'noe lysere blå' contextual: notes.note: bonaire: @@ -22,7 +24,6 @@ translations: gal-pagos-islands: 'Galápagos Islands': 'Galápagosøyene' Province of Ecuador.: Region i Ecuador. - 'Sierra Leone (slightly lighter blue)': 'Sierra Leone (noe lysere blå)' melilla: Autonomous city of Spain: 'Spansk by og enklave på Marokkos middelhavskyst.' montserrat: From 58c4955f2a1d67195d31e805db538b6ee6c8f301 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Tue, 23 Jun 2026 08:33:41 +0200 Subject: [PATCH 036/142] add inline formatted message variables --- .../src/commands/translations.rs | 34 +- crates/brain-brew-cli/tests/cli.rs | 7 +- crates/brain-brew-core/src/lib.rs | 578 +++++++++++++++--- .../brain-brew-core/tests/overlay_compose.rs | 106 +++- .../brain-brew-formats/src/canonical_yaml.rs | 114 +++- .../brain-brew-formats/src/source_includes.rs | 6 +- .../tests/canonical_yaml.rs | 73 +++ .../tests/ultimate_geography_fixture.rs | 41 +- documentation/docs/authoring/translations.md | 36 +- documentation/docs/reference/yaml.md | 26 +- fixtures/ultimate-geography/deck.yaml | 572 +++++++++-------- .../overlays/extensions/hardcore.yaml | 11 +- .../overlays/languages/cs.yaml | 101 +-- .../overlays/languages/da.yaml | 96 +-- .../overlays/languages/de.yaml | 96 +-- .../overlays/languages/es.yaml | 94 +-- .../overlays/languages/fr.yaml | 94 +-- .../overlays/languages/it.yaml | 95 +-- .../overlays/languages/nb.yaml | 104 ++-- .../overlays/languages/nl.yaml | 94 +-- .../overlays/languages/pl.yaml | 94 +-- .../overlays/languages/pt.yaml | 127 ++-- .../overlays/languages/ru.yaml | 96 +-- .../overlays/languages/sv.yaml | 94 +-- .../overlays/languages/zh-tw.yaml | 210 +++++-- .../overlays/languages/zh.yaml | 220 +++++-- 26 files changed, 2187 insertions(+), 1032 deletions(-) diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index 6feaaf8..af4df49 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -1759,8 +1759,19 @@ fn context_unit_json(unit: &TranslationContextUnit) -> serde_json::Value { "message": unit.message.as_ref().map(|message| json!({ "source": message.source, "translated": message.translated, + "format": message.format.as_ref().map(|component| json!({ + "index": component.index, + "name": component.name, + "kind": component.kind.as_str(), + "path": component.path, + "source": component.source, + "translated": component.translated, + "reference": component.reference, + "status": component.category.map(|category| category.as_str()), + })), "components": message.components.iter().map(|component| json!({ "index": component.index, + "name": component.name, "kind": component.kind.as_str(), "path": component.path, "source": component.source, @@ -1905,15 +1916,24 @@ fn print_message_context(message: &TranslationMessageContext, language: &str) { compact_context_value(&message.source), compact_context_value(&message.translated) ); + if let Some(format) = &message.format { + println!( + " {:<32} {} | {}", + "format", + compact_context_value(&format.source), + compact_context_value(&format.translated) + ); + } for component in &message.components { + let label_prefix = component + .name + .as_deref() + .map_or_else(|| format!("[{}]", component.index), ToOwned::to_owned); let label = match component.reference.as_deref() { - Some(reference) => format!( - "[{}] {} {}", - component.index, - component.kind.as_str(), - reference - ), - None => format!("[{}] {}", component.index, component.kind.as_str()), + Some(reference) => { + format!("{} {} {}", label_prefix, component.kind.as_str(), reference) + } + None => format!("{} {}", label_prefix, component.kind.as_str()), }; println!( " {:<32} {} | {}", diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index f069115..4d9093d 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -1047,7 +1047,7 @@ fn translations_context_view_wraps_long_ug_flag_similarity_context() { "overlay.translation.es", "--context", "--source", - "Iceland (blue background", + "blue background, red and white cross", ]); assert!(output.status.success(), "stderr: {}", stderr(&output)); @@ -1056,7 +1056,8 @@ fn translations_context_view_wraps_long_ug_flag_similarity_context() { assert!(out.contains("field: field.flag-similarity (Flag similarity)")); assert!(out.contains("template.flag-country [answer]")); assert!(out.contains("Iceland (blue background,")); - assert!(out.contains("Noruega (fondo rojo, cruz")); + assert!(out.contains("Islandia (fondo azul,")); + assert!(out.contains("Noruega")); } #[test] @@ -1297,7 +1298,7 @@ fn translations_default_report_focuses_on_translatable_note_text() { assert!(output.status.success(), "stderr: {}", stderr(&output)); let out = stdout(&output); assert!(out.contains("missing text translations: 0")); - assert!(out.contains("intentionally unchanged text: 306")); + assert!(out.contains("intentionally unchanged text: 329")); assert!(out.contains("hidden structural/media/tag fallbacks:")); assert!(out.contains("hint: use --full")); assert!(!out.contains("deck.description source=")); diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs index 468e56f..19541d4 100644 --- a/crates/brain-brew-core/src/lib.rs +++ b/crates/brain-brew-core/src/lib.rs @@ -420,17 +420,88 @@ fn validate_message_references( message: &StructuredMessage, errors: &mut Vec, ) { - for (index, component) in message.components.iter().enumerate() { - let MessageComponent::FieldRef(reference) = component else { - continue; - }; - if field_value_at_path(deck, reference).is_none() { + if let Some(format) = &message.format { + if !message.components.is_empty() { errors.push(ValidationError::new( ValidationErrorKind::InvalidMessageReference, - message_component_path(note_id, field_id, index), - format!("structured message field reference {reference:?} does not resolve to a note field"), + message_format_path(note_id, field_id), + "structured message cannot mix an inline format with positional message components" + .to_owned(), )); } + match parse_message_format(format) { + Ok(parts) => { + let referenced_variables = parts + .iter() + .filter_map(|part| match part { + MessageFormatPart::Literal(_) => None, + MessageFormatPart::Variable(variable) => Some(variable.clone()), + }) + .collect::>(); + for variable in &referenced_variables { + if !message.variables.contains_key(variable) { + errors.push(ValidationError::new( + ValidationErrorKind::InvalidMessageReference, + message_format_path(note_id, field_id), + format!( + "structured message format references undefined variable {variable:?}" + ), + )); + } + } + } + Err(message) => errors.push(ValidationError::new( + ValidationErrorKind::InvalidMessageReference, + message_format_path(note_id, field_id), + message, + )), + } + for (variable, component) in &message.variables { + validate_message_component_reference( + deck, + component, + message_variable_path(note_id, field_id, variable), + errors, + ); + } + return; + } + + if !message.variables.is_empty() { + errors.push(ValidationError::new( + ValidationErrorKind::InvalidMessageReference, + format!("notes.{note_id}.fields.{field_id}.message"), + "structured message variables require an inline format".to_owned(), + )); + } + + for (index, component) in message.components.iter().enumerate() { + validate_message_component_reference( + deck, + component, + message_component_path(note_id, field_id, index), + errors, + ); + } +} + +fn validate_message_component_reference( + deck: &CanonicalDeck, + component: &MessageComponent, + path: String, + errors: &mut Vec, +) { + let MessageComponent::FieldRef(reference) = component else { + return; + }; + if field_value_at_path(deck, reference).is_none() { + errors.push(ValidationError::new( + ValidationErrorKind::InvalidMessageReference, + path, + format!( + "structured message field reference {reference:?} does not resolve to a note field" + ), + )); } } @@ -444,10 +515,10 @@ fn resolve_structured_messages_with_validation_errors( for (field_id, message) in ¬e.field_messages { match render_structured_message(&snapshot, message) { Ok(value) => resolved_fields.push((note_id.clone(), field_id.clone(), value)), - Err(reference) => errors.push(ValidationError::new( + Err(error) => errors.push(ValidationError::new( ValidationErrorKind::InvalidMessageReference, format!("notes.{note_id}.fields.{field_id}.message"), - format!("structured message field reference {reference:?} does not resolve to a note field"), + error.message(), )), } } @@ -469,10 +540,10 @@ fn resolve_structured_messages_with_compose_errors( for (field_id, message) in ¬e.field_messages { match render_structured_message(&snapshot, message) { Ok(value) => resolved_fields.push((note_id.clone(), field_id.clone(), value)), - Err(reference) => errors.push(ComposeError::new( + Err(error) => errors.push(ComposeError::new( ComposeErrorKind::ValidationFailed, format!("notes.{note_id}.fields.{field_id}.message"), - format!("structured message field reference {reference:?} does not resolve to a note field"), + error.message(), )), } } @@ -487,16 +558,77 @@ fn resolve_structured_messages_with_compose_errors( fn render_structured_message( deck: &CanonicalDeck, message: &StructuredMessage, -) -> Result { +) -> Result { + if let Some(format) = &message.format { + let mut variables = BTreeMap::new(); + for (name, component) in &message.variables { + variables.insert(name.clone(), render_message_component(deck, component)?); + } + return render_message_format(format, &variables) + .map_err(StructuredMessageRenderError::Format); + } + let mut rendered = String::new(); for component in &message.components { - match component { - MessageComponent::Literal(value) | MessageComponent::Text(value) => { - rendered.push_str(value); - } - MessageComponent::FieldRef(reference) => { - let Some(value) = field_value_at_path(deck, reference) else { - return Err(reference.clone()); + rendered.push_str(&render_message_component(deck, component)?); + } + Ok(rendered) +} + +fn render_message_component( + deck: &CanonicalDeck, + component: &MessageComponent, +) -> Result { + match component { + MessageComponent::Literal(value) | MessageComponent::Text(value) => Ok(value.clone()), + MessageComponent::FieldRef(reference) => { + let Some(value) = field_value_at_path(deck, reference) else { + return Err(StructuredMessageRenderError::InvalidReference( + reference.clone(), + )); + }; + Ok(value.to_owned()) + } + } +} + +#[derive(Debug)] +enum StructuredMessageRenderError { + InvalidReference(String), + Format(String), +} + +impl StructuredMessageRenderError { + fn message(&self) -> String { + match self { + Self::InvalidReference(reference) => format!( + "structured message field reference {reference:?} does not resolve to a note field" + ), + Self::Format(message) => message.clone(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum MessageFormatPart { + Literal(String), + Variable(String), +} + +fn render_message_format( + format: &str, + variables: &BTreeMap, +) -> Result { + let parts = parse_message_format(format)?; + let mut rendered = String::new(); + for part in parts { + match part { + MessageFormatPart::Literal(value) => rendered.push_str(&value), + MessageFormatPart::Variable(variable) => { + let Some(value) = variables.get(&variable) else { + return Err(format!( + "structured message format references undefined variable {variable:?}" + )); }; rendered.push_str(value); } @@ -505,6 +637,62 @@ fn render_structured_message( Ok(rendered) } +fn parse_message_format(format: &str) -> Result, String> { + let mut parts = Vec::new(); + let mut literal = String::new(); + let mut chars = format.chars().peekable(); + while let Some(ch) = chars.next() { + match ch { + '{' => { + if chars.peek() == Some(&'{') { + chars.next(); + literal.push('{'); + continue; + } + if !literal.is_empty() { + parts.push(MessageFormatPart::Literal(std::mem::take(&mut literal))); + } + let mut variable = String::new(); + let mut closed = false; + for variable_ch in chars.by_ref() { + if variable_ch == '}' { + closed = true; + break; + } + variable.push(variable_ch); + } + if !closed { + return Err( + "structured message format has an unclosed variable placeholder".to_owned(), + ); + } + if variable.is_empty() { + return Err( + "structured message format contains an empty variable placeholder" + .to_owned(), + ); + } + parts.push(MessageFormatPart::Variable(variable)); + } + '}' => { + if chars.peek() == Some(&'}') { + chars.next(); + literal.push('}'); + } else { + return Err( + "structured message format contains an unmatched closing brace".to_owned(), + ); + } + } + other => literal.push(other), + } + } + if !literal.is_empty() { + parts.push(MessageFormatPart::Literal(literal)); + } + Ok(parts) +} + fn field_value_at_path<'a>(deck: &'a CanonicalDeck, path: &str) -> Option<&'a str> { let (note_id, field_id) = note_field_path_parts(path)?; deck.notes @@ -523,6 +711,14 @@ fn message_component_path(note_id: &StableId, field_id: &StableId, index: usize) format!("notes.{note_id}.fields.{field_id}.message.{index}") } +fn message_format_path(note_id: &StableId, field_id: &StableId) -> String { + format!("notes.{note_id}.fields.{field_id}.message.format") +} + +fn message_variable_path(note_id: &StableId, field_id: &StableId, variable: &str) -> String { + format!("notes.{note_id}.fields.{field_id}.message.variables.{variable}") +} + fn translation_coverage_report( deck: &CanonicalDeck, overlay: &Overlay, @@ -660,17 +856,33 @@ impl TranslationCoverageBuilder<'_> { return; } - for (index, component) in message.components.iter().enumerate() { - let component_path = message_component_path(note_id, field_id, index); - match component { - MessageComponent::Literal(_) => {} - MessageComponent::Text(value) => { - self.record_string(value, component_path, None); - } - MessageComponent::FieldRef(reference) => { - if let Some(value) = field_value_at_path(self.deck, reference) { - self.record_string(value, component_path, None); - } + if let Some(format) = &message.format { + self.record_optional_string(format, message_format_path(note_id, field_id)); + for (variable, component) in &message.variables { + self.record_message_component( + component, + message_variable_path(note_id, field_id, variable), + ); + } + } else { + for (index, component) in message.components.iter().enumerate() { + self.record_message_component( + component, + message_component_path(note_id, field_id, index), + ); + } + } + } + + fn record_message_component(&mut self, component: &MessageComponent, path: String) { + match component { + MessageComponent::Literal(_) => {} + MessageComponent::Text(value) => { + self.record_string(value, path, None); + } + MessageComponent::FieldRef(reference) => { + if let Some(value) = field_value_at_path(self.deck, reference) { + self.record_string(value, path, None); } } } @@ -814,6 +1026,63 @@ impl TranslationCoverageBuilder<'_> { } } + fn record_optional_string(&mut self, value: &str, path: String) { + if value.is_empty() || is_ignored_translation_path(self.translations, &path) { + return; + } + + let source = value.to_owned(); + self.seen_sources.insert(source.clone()); + let direct_translation = self.translations.direct.get(value); + if direct_translation.is_some() { + self.seen_direct.insert(source.clone()); + } + let no_change = self.translations.no_change.contains(value); + + let mut contextual_translation: Option<(&String, &String)> = None; + for (context_path, replacements) in &self.translations.contextual { + if context_matches_path(context_path, &path) + && let Some(translated) = replacements.get(value) + { + self.seen_contextual + .insert((context_path.clone(), source.clone())); + if contextual_translation + .as_ref() + .is_none_or(|(current_context, _)| context_path.len() > current_context.len()) + { + contextual_translation = Some((context_path, translated)); + } + } + } + + if let Some((context_path, translated)) = contextual_translation { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::ContextualOverride, + path, + source, + translated: Some(translated.clone()), + context: Some(context_path.clone()), + }); + } else if let Some(translated) = direct_translation { + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::DirectTranslation, + path, + source, + translated: Some(translated.clone()), + context: None, + }); + } else if no_change { + self.seen_no_change.insert(source.clone()); + self.entries.push(TranslationCoverageEntry { + category: TranslationCoverageCategory::NoChange, + path, + source: source.clone(), + translated: Some(source), + context: None, + }); + } + } + fn record_tags(&mut self, tags: &BTreeSet, path_prefix: &str) { for tag in tags { self.record_string(tag, format!("{path_prefix}.{tag}"), None); @@ -1106,52 +1375,117 @@ fn message_context( ) -> Option { let message = note.field_messages.get(field_id)?; let resolved_source = note.fields.get(field_id).cloned().unwrap_or_default(); - let mut components = Vec::new(); - for (index, component) in message.components.iter().enumerate() { - let path = message_component_path(note_id, field_id, index); - let entry = entries_by_path.get(path.as_str()).copied(); - let (kind, source, reference) = match component { - MessageComponent::Literal(value) => { - (MessageComponentKind::Literal, value.clone(), None) - } - MessageComponent::Text(value) => (MessageComponentKind::Text, value.clone(), None), - MessageComponent::FieldRef(reference) => ( - MessageComponentKind::FieldRef, - field_value_at_path(deck, reference) - .unwrap_or_default() - .to_owned(), - Some(reference.clone()), - ), - }; - let translated = entry + let full_field_translation = entries_by_path + .get(format!("notes.{note_id}.fields.{field_id}").as_str()) + .and_then(|entry| entry.translated.clone()); + + if let Some(format) = &message.format { + let format_path = message_format_path(note_id, field_id); + let format_entry = entries_by_path.get(format_path.as_str()).copied(); + let translated_format = format_entry .and_then(|entry| entry.translated.clone()) - .unwrap_or_else(|| source.clone()); - components.push(TranslationMessageComponentContext { - index, - kind, - path, - source, + .unwrap_or_else(|| format.clone()); + let format_context = TranslationMessageComponentContext { + index: 0, + name: None, + kind: MessageComponentKind::Format, + path: format_path, + source: format.clone(), + translated: translated_format.clone(), + reference: None, + category: format_entry.map(|entry| entry.category), + }; + let mut components = Vec::new(); + let mut translated_variables = BTreeMap::new(); + for (index, (name, component)) in message.variables.iter().enumerate() { + let path = message_variable_path(note_id, field_id, name); + let context = message_component_context( + deck, + component, + index, + Some(name.clone()), + path, + entries_by_path, + ); + translated_variables.insert(name.clone(), context.translated.clone()); + components.push(context); + } + let translated = full_field_translation.unwrap_or_else(|| { + render_message_format(&translated_format, &translated_variables).unwrap_or_else(|_| { + components + .iter() + .map(|component| component.translated.as_str()) + .collect::() + }) + }); + return Some(TranslationMessageContext { + source: resolved_source, translated, - reference, - category: entry.map(|entry| entry.category), + format: Some(format_context), + components, }); } - let translated = entries_by_path - .get(format!("notes.{note_id}.fields.{field_id}").as_str()) - .and_then(|entry| entry.translated.clone()) - .unwrap_or_else(|| { - components - .iter() - .map(|component| component.translated.as_str()) - .collect::() - }); + + let mut components = Vec::new(); + for (index, component) in message.components.iter().enumerate() { + components.push(message_component_context( + deck, + component, + index, + None, + message_component_path(note_id, field_id, index), + entries_by_path, + )); + } + let translated = full_field_translation.unwrap_or_else(|| { + components + .iter() + .map(|component| component.translated.as_str()) + .collect::() + }); Some(TranslationMessageContext { source: resolved_source, translated, + format: None, components, }) } +fn message_component_context( + deck: &CanonicalDeck, + component: &MessageComponent, + index: usize, + name: Option, + path: String, + entries_by_path: &BTreeMap<&str, &TranslationCoverageEntry>, +) -> TranslationMessageComponentContext { + let entry = entries_by_path.get(path.as_str()).copied(); + let (kind, source, reference) = match component { + MessageComponent::Literal(value) => (MessageComponentKind::Literal, value.clone(), None), + MessageComponent::Text(value) => (MessageComponentKind::Text, value.clone(), None), + MessageComponent::FieldRef(reference) => ( + MessageComponentKind::FieldRef, + field_value_at_path(deck, reference) + .unwrap_or_default() + .to_owned(), + Some(reference.clone()), + ), + }; + let translated = entry + .and_then(|entry| entry.translated.clone()) + .unwrap_or_else(|| source.clone()); + TranslationMessageComponentContext { + index, + name, + kind, + path, + source, + translated, + reference, + category: entry.map(|entry| entry.category), + } +} + fn card_contexts_for_field(note_type: &NoteType, field_name: &str) -> Vec { note_type .card_templates @@ -1441,26 +1775,49 @@ impl TranslationApplyContext<'_, '_> { return None; } - for (index, component) in message.components.iter_mut().enumerate() { - let component_path = message_component_path(note_id, field_id, index); - match component { - MessageComponent::Literal(_) => {} - MessageComponent::Text(value) => { - self.translate_string(value, component_path, None); - } - MessageComponent::FieldRef(reference) => { - let Some(source) = field_value_at_path(source_deck, reference) else { - continue; - }; - let mut translated = source.to_owned(); - self.translate_string(&mut translated, component_path, None); - *component = MessageComponent::Literal(translated); - } + if let Some(format) = &mut message.format { + self.translate_optional_string(format, message_format_path(note_id, field_id)); + for (variable, component) in &mut message.variables { + self.translate_message_component( + source_deck, + component, + message_variable_path(note_id, field_id, variable), + ); + } + } else { + for (index, component) in message.components.iter_mut().enumerate() { + self.translate_message_component( + source_deck, + component, + message_component_path(note_id, field_id, index), + ); } } None } + fn translate_message_component( + &mut self, + source_deck: &CanonicalDeck, + component: &mut MessageComponent, + path: String, + ) { + match component { + MessageComponent::Literal(_) => {} + MessageComponent::Text(value) => { + self.translate_string(value, path, None); + } + MessageComponent::FieldRef(reference) => { + let Some(source) = field_value_at_path(source_deck, reference) else { + return; + }; + let mut translated = source.to_owned(); + self.translate_string(&mut translated, path, None); + *component = MessageComponent::Literal(translated); + } + } + } + fn has_explicit_string_entry( &self, value: &str, @@ -1603,6 +1960,55 @@ impl TranslationApplyContext<'_, '_> { } } + fn translate_optional_string(&mut self, value: &mut String, path: String) { + if value.is_empty() || is_ignored_translation_path(self.translations, &path) { + return; + } + + let source = value.clone(); + self.source_paths + .entry(source.clone()) + .or_default() + .insert(path.clone()); + let direct_translation = self.translations.direct.get(source.as_str()); + if direct_translation.is_some() { + self.seen_direct.insert(source.clone()); + } + let mut contextual_translation: Option<(&String, &String)> = None; + for (context_path, replacements) in &self.translations.contextual { + if context_matches_path(context_path, &path) + && let Some(translated) = replacements.get(source.as_str()) + { + self.seen_contextual + .insert((context_path.clone(), source.clone())); + if contextual_translation + .as_ref() + .is_none_or(|(current_context, _)| context_path.len() > current_context.len()) + { + contextual_translation = Some((context_path, translated)); + } + } + } + + let translated = contextual_translation + .map(|(_, translated)| translated) + .or(direct_translation); + if let Some(translated) = translated + && value != translated + { + if !record_change_path( + &path, + self.overlay, + ChangeIntent::Replace, + self.changed_paths, + self.errors, + ) { + return; + } + *value = translated.clone(); + } + } + fn translate_tags(&mut self, tags: &mut BTreeSet, path_prefix: &str) { for tag in tags.iter().cloned().collect::>() { let mut translated = tag.clone(); @@ -1744,6 +2150,9 @@ fn direct_or_contextual_translation_for_path_with_candidate<'a>( } fn context_parent_candidates(context_path: &str) -> Vec { + if context_path.contains(".message.variables.") || context_path.ends_with(".message.format") { + return Vec::new(); + } if let Some((note_context, _)) = context_path.split_once(".fields.") && note_context.starts_with("notes.") { @@ -3541,6 +3950,7 @@ pub struct TranslationContextUnit { pub struct TranslationMessageContext { pub source: String, pub translated: String, + pub format: Option, pub components: Vec, } @@ -3548,6 +3958,7 @@ pub struct TranslationMessageContext { #[derive(Clone, Debug, Eq, PartialEq)] pub struct TranslationMessageComponentContext { pub index: usize, + pub name: Option, pub kind: MessageComponentKind, pub path: String, pub source: String, @@ -3814,7 +4225,12 @@ pub struct Note { /// A field value assembled from reusable references, translatable fragments, and literal glue. #[derive(Clone, Debug, Eq, PartialEq)] pub struct StructuredMessage { + /// Positional components for simple messages. pub components: Vec, + /// Optional inline format string using `{variable}` placeholders for named message variables. + pub format: Option, + /// Named components referenced from `format` placeholders. + pub variables: BTreeMap, } /// One component of a structured message field value. @@ -3831,6 +4247,7 @@ pub enum MessageComponent { /// Stable component kind for reports and translator context UIs. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum MessageComponentKind { + Format, Literal, Text, FieldRef, @@ -3839,6 +4256,7 @@ pub enum MessageComponentKind { impl MessageComponentKind { pub fn as_str(self) -> &'static str { match self { + Self::Format => "format", Self::Literal => "literal", Self::Text => "text", Self::FieldRef => "field_ref", diff --git a/crates/brain-brew-core/tests/overlay_compose.rs b/crates/brain-brew-core/tests/overlay_compose.rs index 9b3a795..976b576 100644 --- a/crates/brain-brew-core/tests/overlay_compose.rs +++ b/crates/brain-brew-core/tests/overlay_compose.rs @@ -819,13 +819,15 @@ fn translation_dictionary_resolves_structured_message_components() { .expect("translation overlay has coverage"); assert!(report.entries.iter().any(|entry| { entry.category == TranslationCoverageCategory::DirectTranslation - && entry.path == "notes.note.finland.fields.field.flag-similarity.message.0" + && entry.path + == "notes.note.finland.fields.field.flag-similarity.message.variables.country_1" && entry.source == "Iceland" && entry.translated.as_deref() == Some("Island") })); assert!(report.entries.iter().any(|entry| { entry.category == TranslationCoverageCategory::ContextualOverride - && entry.path == "notes.note.finland.fields.field.flag-similarity.message.2" + && entry.path + == "notes.note.finland.fields.field.flag-similarity.message.variables.description_1" && entry.source == "blue background with a white cross" && entry.translated.as_deref() == Some("blå bakgrunn med hvitt kors") })); @@ -838,6 +840,68 @@ fn translation_dictionary_resolves_structured_message_components() { ); } +#[test] +fn translation_dictionary_can_translate_structured_message_format() { + let base = ug_style_deck_with_flag_similarity_message(); + let format_source = "{country_1} ({description_1}), {country_2} ({description_2})"; + let overlay = Overlay { + id: sid("overlay.translation.zh"), + kind: OverlayKind::Translation, + translations: Some(TranslationDictionary { + direct: BTreeMap::from([ + ("Iceland".to_owned(), "冰岛".to_owned()), + ("Norway".to_owned(), "挪威".to_owned()), + ( + format_source.to_owned(), + "{country_1}({description_1})、{country_2}({description_2})".to_owned(), + ), + ( + "blue background with a white cross".to_owned(), + "蓝底白十字".to_owned(), + ), + ( + "red background with a blue cross".to_owned(), + "红底蓝十字".to_owned(), + ), + ]), + contextual: BTreeMap::new(), + no_change: BTreeSet::new(), + target_additions: BTreeMap::new(), + variables: BTreeMap::new(), + adapter_ids: BTreeMap::new(), + require_complete: true, + ignore_paths: BTreeSet::from([ + "deck.*".to_owned(), + "note_types.*".to_owned(), + "notes.*.fields.field.country".to_owned(), + "notes.*.fields.field.capital".to_owned(), + "notes.*.fields.field.flag".to_owned(), + "notes.*.tags.*".to_owned(), + ]), + }), + deck_change: None, + note_changes: BTreeMap::new(), + note_type_changes: BTreeMap::new(), + media_changes: BTreeMap::new(), + }; + + let report = base + .translation_coverage(&overlay) + .expect("translation overlay has coverage"); + assert!(report.entries.iter().any(|entry| { + entry.category == TranslationCoverageCategory::DirectTranslation + && entry.path == "notes.note.finland.fields.field.flag-similarity.message.format" + && entry.source == format_source + })); + + let resolved = base.compose(&[overlay]).expect("translations compose"); + + assert_eq!( + resolved.notes[&sid("note.finland")].fields[&sid("field.flag-similarity")], + "冰岛(蓝底白十字)、挪威(红底蓝十字)" + ); +} + #[test] fn translation_dictionary_can_override_full_structured_message_when_needed() { let base = ug_style_deck_with_flag_similarity_message(); @@ -930,7 +994,8 @@ fn translation_dictionary_reports_structured_message_missing_and_stale_component .expect("translation overlay has coverage"); assert!(report.entries.iter().any(|entry| { entry.category == TranslationCoverageCategory::UntranslatedFallback - && entry.path == "notes.note.finland.fields.field.flag-similarity.message.2" + && entry.path + == "notes.note.finland.fields.field.flag-similarity.message.variables.description_1" && entry.source == "blue background with a white cross" })); assert!(report.entries.iter().any(|entry| { @@ -943,7 +1008,8 @@ fn translation_dictionary_reports_structured_message_missing_and_stale_component .expect_err("strict structured message components fail"); assert!(report.errors.iter().any(|error| { error.kind == ComposeErrorKind::MissingTranslation - && error.path == "notes.note.finland.fields.field.flag-similarity.message.2" + && error.path + == "notes.note.finland.fields.field.flag-similarity.message.variables.description_1" && error .message .contains("missing direct or contextual translation") @@ -1570,16 +1636,28 @@ fn ug_style_deck_with_flag_similarity_message() -> CanonicalDeck { finland.field_messages.insert( sid("field.flag-similarity"), StructuredMessage { - components: vec![ - MessageComponent::FieldRef("notes.note.iceland.fields.field.country".to_owned()), - MessageComponent::Literal(" (".to_owned()), - MessageComponent::Text("blue background with a white cross".to_owned()), - MessageComponent::Literal("), ".to_owned()), - MessageComponent::FieldRef("notes.note.norway.fields.field.country".to_owned()), - MessageComponent::Literal(" (".to_owned()), - MessageComponent::Text("red background with a blue cross".to_owned()), - MessageComponent::Literal(")".to_owned()), - ], + components: Vec::new(), + format: Some("{country_1} ({description_1}), {country_2} ({description_2})".to_owned()), + variables: BTreeMap::from([ + ( + "country_1".to_owned(), + MessageComponent::FieldRef( + "notes.note.iceland.fields.field.country".to_owned(), + ), + ), + ( + "description_1".to_owned(), + MessageComponent::Text("blue background with a white cross".to_owned()), + ), + ( + "country_2".to_owned(), + MessageComponent::FieldRef("notes.note.norway.fields.field.country".to_owned()), + ), + ( + "description_2".to_owned(), + MessageComponent::Text("red background with a blue cross".to_owned()), + ), + ]), }, ); deck.resolve_structured_messages() diff --git a/crates/brain-brew-formats/src/canonical_yaml.rs b/crates/brain-brew-formats/src/canonical_yaml.rs index a8711aa..cd46cb8 100644 --- a/crates/brain-brew-formats/src/canonical_yaml.rs +++ b/crates/brain-brew-formats/src/canonical_yaml.rs @@ -788,8 +788,7 @@ fn write_field_change(out: &mut String, indent: &str, field_id: &StableId, chang write_multiline_or_scalar(out, &format!("{indent} "), "value", value); } if let Some(message) = &change.message { - writeln!(out, "{indent} message:").expect("writing to a string cannot fail"); - write_message_components(out, &format!("{indent} "), message); + write_structured_message_value(out, &format!("{indent} "), message); } if let Some(expected_base) = &change.expected_base { write_expected_base(out, &format!("{indent} "), expected_base); @@ -875,25 +874,49 @@ fn write_structured_message_field( message: &StructuredMessage, ) { writeln!(out, "{indent}{field_id}:").expect("writing to a string cannot fail"); - writeln!(out, "{indent} message:").expect("writing to a string cannot fail"); - write_message_components(out, &format!("{indent} "), message); + write_structured_message_value(out, &format!("{indent} "), message); +} + +fn write_structured_message_value(out: &mut String, indent: &str, message: &StructuredMessage) { + if let Some(format) = &message.format { + writeln!(out, "{indent}format: {}", yaml_scalar(format)) + .expect("writing to a string cannot fail"); + writeln!(out, "{indent}variables:").expect("writing to a string cannot fail"); + for (name, component) in &message.variables { + writeln!(out, "{indent} {name}:").expect("writing to a string cannot fail"); + write_message_component(out, &format!("{indent} "), component, false); + } + } else { + writeln!(out, "{indent}message:").expect("writing to a string cannot fail"); + write_message_components(out, &format!("{indent} "), message); + } } fn write_message_components(out: &mut String, indent: &str, message: &StructuredMessage) { for component in &message.components { - match component { - MessageComponent::Literal(value) => { - writeln!(out, "{indent}- literal: {}", yaml_scalar(value)) - .expect("writing to a string cannot fail"); - } - MessageComponent::Text(value) => { - writeln!(out, "{indent}- text: {}", yaml_scalar(value)) - .expect("writing to a string cannot fail"); - } - MessageComponent::FieldRef(reference) => { - writeln!(out, "{indent}- ref: {}", yaml_scalar(reference)) - .expect("writing to a string cannot fail"); - } + write_message_component(out, indent, component, true); + } +} + +fn write_message_component( + out: &mut String, + indent: &str, + component: &MessageComponent, + list: bool, +) { + let prefix = if list { "- " } else { "" }; + match component { + MessageComponent::Literal(value) => { + writeln!(out, "{indent}{prefix}literal: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + MessageComponent::Text(value) => { + writeln!(out, "{indent}{prefix}text: {}", yaml_scalar(value)) + .expect("writing to a string cannot fail"); + } + MessageComponent::FieldRef(reference) => { + writeln!(out, "{indent}{prefix}ref: {}", yaml_scalar(reference)) + .expect("writing to a string cannot fail"); } } } @@ -1219,6 +1242,9 @@ fn apply_field_fills( } let (value, message) = match value { FieldValueYaml::Scalar(value) => (Some(value), None), + FieldValueYaml::Formatted(message) => { + (None, Some(message.into_structured_message())) + } FieldValueYaml::Message(message) => (None, Some(message.into_structured_message())), }; note_change.fields.insert( @@ -1686,17 +1712,31 @@ struct FieldChangeYaml { #[serde(default)] message: Option>, #[serde(default)] + format: Option, + #[serde(default)] + variables: BTreeMap, + #[serde(default)] expected_base: Option, } impl FieldChangeYaml { fn into_field_change(self) -> Result { + let message = if let Some(format) = self.format { + Some( + FormattedMessageYaml { + format, + variables: self.variables, + } + .into_structured_message(), + ) + } else { + self.message + .map(|message| ComponentMessageYaml { message }.into_structured_message()) + }; Ok(FieldChange { intent: parse_change_intent(&self.intent)?, value: self.value, - message: self - .message - .map(|message| FieldMessageYaml { message }.into_structured_message()), + message, expected_base: self .expected_base .map(ExpectedBaseYaml::into_expected_base) @@ -1951,6 +1991,10 @@ impl NoteYaml { FieldValueYaml::Scalar(value) => { fields.insert(field_id, value); } + FieldValueYaml::Formatted(message) => { + fields.insert(field_id.clone(), String::new()); + field_messages.insert(field_id, message.into_structured_message()); + } FieldValueYaml::Message(message) => { fields.insert(field_id.clone(), String::new()); field_messages.insert(field_id, message.into_structured_message()); @@ -1973,16 +2017,17 @@ impl NoteYaml { #[serde(untagged)] enum FieldValueYaml { Scalar(String), - Message(FieldMessageYaml), + Formatted(FormattedMessageYaml), + Message(ComponentMessageYaml), } #[derive(Deserialize)] #[serde(deny_unknown_fields)] -struct FieldMessageYaml { +struct ComponentMessageYaml { message: Vec, } -impl FieldMessageYaml { +impl ComponentMessageYaml { fn into_structured_message(self) -> StructuredMessage { StructuredMessage { components: self @@ -1990,6 +2035,29 @@ impl FieldMessageYaml { .into_iter() .map(MessageComponentYaml::into_component) .collect(), + format: None, + variables: BTreeMap::new(), + } + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct FormattedMessageYaml { + format: String, + variables: BTreeMap, +} + +impl FormattedMessageYaml { + fn into_structured_message(self) -> StructuredMessage { + StructuredMessage { + components: Vec::new(), + format: Some(self.format), + variables: self + .variables + .into_iter() + .map(|(name, component)| (name, component.into_component())) + .collect(), } } } diff --git a/crates/brain-brew-formats/src/source_includes.rs b/crates/brain-brew-formats/src/source_includes.rs index e308053..68b70bb 100644 --- a/crates/brain-brew-formats/src/source_includes.rs +++ b/crates/brain-brew-formats/src/source_includes.rs @@ -373,7 +373,11 @@ fn is_scalar_content_path(path: &[String]) -> bool { ) { return true; } - if matches!(last, "text" | "literal") && path.iter().any(|segment| segment == "message") { + if matches!(last, "text" | "literal" | "format") + && (path.iter().any(|segment| segment == "message") + || path.iter().any(|segment| segment == "fields") + || path.iter().any(|segment| segment == "field_fills")) + { return true; } if path.iter().any(|segment| segment == "variables") && last != "variables" { diff --git a/crates/brain-brew-formats/tests/canonical_yaml.rs b/crates/brain-brew-formats/tests/canonical_yaml.rs index d18acba..931c8fc 100644 --- a/crates/brain-brew-formats/tests/canonical_yaml.rs +++ b/crates/brain-brew-formats/tests/canonical_yaml.rs @@ -161,6 +161,79 @@ tombstones: [] assert!(formatted.contains(" - text: blue background with a white cross")); } +#[test] +fn parses_formats_and_resolves_formatted_structured_message_fields() { + let yaml = r#"deck: + id: deck.message-format + name: Message Format + description: '' + adapter_ids: {} +note_types: + note-type.country: + name: Country + field_order: + - field.country + - field.flag-similarity + fields: + field.country: + name: Country + field.flag-similarity: + name: Flag similarity + card_template_order: [] + card_templates: {} + styling: '' + adapter_ids: {} +notes: + note.finland: + note_type_id: note-type.country + fields: + field.country: Finland + field.flag-similarity: + format: '{country} ({description})' + variables: + country: + ref: notes.note.iceland.fields.field.country + description: + text: blue background with a white cross + tags: [] + adapter_ids: {} + note.iceland: + note_type_id: note-type.country + fields: + field.country: Iceland + field.flag-similarity: '' + tags: [] + adapter_ids: {} +media: {} +tombstones: [] +"#; + + let deck = canonical_yaml::from_str(yaml).expect("formatted structured message yaml parses"); + + assert_eq!( + deck.notes[&sid("note.finland")].fields[&sid("field.flag-similarity")], + "Iceland (blue background with a white cross)" + ); + let message = &deck.notes[&sid("note.finland")].field_messages[&sid("field.flag-similarity")]; + assert_eq!(message.format.as_deref(), Some("{country} ({description})")); + assert!(message.variables.contains_key("description")); + + let formatted = canonical_yaml::to_string(&deck).expect("formatted message emits"); + assert!( + formatted.contains("field.flag-similarity:\n format: '{country} ({description})'") + ); + assert!( + formatted.contains( + " country:\n ref: notes.note.iceland.fields.field.country" + ) + ); + assert!( + formatted.contains( + " description:\n text: blue background with a white cross" + ) + ); +} + #[test] fn translation_dictionary_overlay_translates_variables_fields_and_adapter_ids() { let deck = canonical_yaml::from_str( diff --git a/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs index 45218bf..35707a1 100644 --- a/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs +++ b/crates/brain-brew-formats/tests/ultimate_geography_fixture.rs @@ -83,9 +83,9 @@ fn ultimate_geography_fixture_uses_structured_messages_for_flag_similarity() { for relative_path in ["deck.yaml", "overlays/extensions/hardcore.yaml"] { let source = fs::read_to_string(root.join(relative_path)).unwrap(); assert!( - source.contains("field.flag-similarity:\n message:") - || source.contains("field.flag-similarity:\n message:"), - "{relative_path} should use structured messages for non-empty flag similarity fields" + source.contains("field.flag-similarity:\n format:") + || source.contains("field.flag-similarity:\n format:"), + "{relative_path} should use inline formatted structured messages for non-empty flag similarity fields" ); assert!( !source.lines().any( @@ -95,6 +95,41 @@ fn ultimate_geography_fixture_uses_structured_messages_for_flag_similarity() { "{relative_path} should not keep long scalar flag similarity strings" ); } + + let manifest = read_manifest(&root); + let flag_similarity_sources: BTreeSet<_> = ["en-standard", "en-hardcore-standard"] + .into_iter() + .flat_map(|target| { + let deck = compose_target(&root, &manifest, target); + deck.notes + .values() + .filter_map(|note| note.fields.get(&sid("field.flag-similarity"))) + .filter(|value| !value.is_empty()) + .cloned() + .collect::>() + }) + .collect(); + + for overlay_ref in manifest + .overlays + .values() + .filter(|overlay| overlay.kind.as_deref() == Some("translation")) + { + let overlay_path = root.join(&overlay_ref.file); + let overlay = read_overlay_file(&root, &overlay_path) + .unwrap_or_else(|error| panic!("{} parses: {error}", overlay_ref.file)); + let translations = overlay + .translations + .as_ref() + .unwrap_or_else(|| panic!("{} uses translation dictionary", overlay_ref.file)); + for source in &flag_similarity_sources { + assert!( + !translations.direct.contains_key(source), + "{} should translate structured flag-similarity components, not duplicate the full composite source key {source:?}", + overlay_ref.file + ); + } + } } #[test] diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index 63c7c5f..0ba12ff 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -97,22 +97,24 @@ notes: note.finland: fields: field.flag-similarity: - message: - - ref: notes.note.iceland.fields.field.country - - literal: ' (' - - text: blue background with a white cross - - literal: '), ' - - ref: notes.note.norway.fields.field.country - - literal: ' (' - - text: red background with a blue cross - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.iceland.fields.field.country + description_1: + text: blue background with a white cross + country_2: + ref: notes.note.norway.fields.field.country + description_2: + text: red background with a blue cross ``` -Components mean: +Message variables mean: +- `format` is inline at the usage site and renders `{variable}` placeholders. It is not required for translation coverage by default, but it can be translated directly or contextually when a target language needs different glue, ordering, spacing, or separators; - `ref` points at another note field and can reuse that field's existing translation, such as a country-name entry in `translations.direct`; -- `text` is an editable translatable fragment and appears in translation coverage at a component path such as `notes.note.finland.fields.field.flag-similarity.message.2`; -- `literal` is non-translatable glue for punctuation, spaces, or markup. +- `text` is an editable translatable fragment and appears in translation coverage at a named component path such as `notes.note.finland.fields.field.flag-similarity.message.variables.description_1`; +- `literal` can still be used for a named non-translatable variable, but punctuation and separators usually belong in `format`. Translation overlay: @@ -123,10 +125,18 @@ translations: Norway: Norge red background with a blue cross: rød bakgrunn med blått kors contextual: - notes.note.finland: + notes.note.finland.fields.field.flag-similarity.message.variables.description_1: blue background with a white cross: blå bakgrunn med hvitt kors ``` +If the target language needs different glue for this field shape, translate the format string instead of overriding the whole rendered field: + +```yaml +translations: + direct: + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1}({description_1})、{country_2}({description_2})' +``` + The resolved field exported to Anki is still a plain string: ```text diff --git a/documentation/docs/reference/yaml.md b/documentation/docs/reference/yaml.md index df9ba45..3532924 100644 --- a/documentation/docs/reference/yaml.md +++ b/documentation/docs/reference/yaml.md @@ -55,21 +55,33 @@ kind: personal # learner/local source content ## Structured field messages -Note fields are usually scalar strings. For genuinely composite text that should reuse existing translations, a field may use a structured `message` instead: +Note fields are usually scalar strings. For genuinely composite text that should reuse existing translations, a field may use an inline `format` with named message variables: ```yaml notes: note.finland: fields: field.flag-similarity: - message: - - ref: notes.note.iceland.fields.field.country - - literal: ' (' - - text: blue background with a white cross - - literal: ')' + format: '{country} ({description})' + variables: + country: + ref: notes.note.iceland.fields.field.country + description: + text: blue background with a white cross ``` -`ref` resolves another note field before export, `text` is extracted for translation coverage, and `literal` is non-translatable glue. Adapter exports receive a plain resolved string. +`format` renders `{variable}` placeholders and can itself be translated when a language needs different glue or ordering. `ref` resolves another note field before export, `text` is extracted for translation coverage at the named variable path, and `literal` is available for non-translatable named variables. Adapter exports receive a plain resolved string. + +The older positional component form remains accepted for simple cases: + +```yaml +field.flag-similarity: + message: + - ref: notes.note.iceland.fields.field.country + - literal: ' (' + - text: blue background with a white cross + - literal: ')' +``` ## Translation dictionary diff --git a/fixtures/ultimate-geography/deck.yaml b/fixtures/ultimate-geography/deck.yaml index 22ddd24..0fc810d 100644 --- a/fixtures/ultimate-geography/deck.yaml +++ b/fixtures/ultimate-geography/deck.yaml @@ -242,11 +242,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.moldova.fields.field.country - - literal: ' (' - - text: 'wider, coat of arms with eagle' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.moldova.fields.field.country + description_1: + text: wider, coat of arms with eagle field.map: '' tags: - 'UG::Europe' @@ -450,11 +451,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.new-zealand.fields.field.country - - literal: ' (' - - text: 'red stars, two fewer stars' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.new-zealand.fields.field.country + description_1: + text: red stars, two fewer stars field.map: '' tags: - 'UG::Oceania' @@ -471,11 +473,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.latvia.fields.field.country - - literal: ' (' - - text: 'darker red, narrower white band' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.latvia.fields.field.country + description_1: + text: darker red, narrower white band field.map: '' tags: - 'UG::Europe' @@ -524,11 +527,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.qatar.fields.field.country - - literal: ' (' - - text: 'wider, more serrated edges, maroon' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.qatar.fields.field.country + description_1: + text: wider, more serrated edges, maroon field.map: '' tags: - 'UG::Asia' @@ -812,11 +816,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.ghana.fields.field.country - - literal: ' (' - - text: 'star instead of coat of arms' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.ghana.fields.field.country + description_1: + text: star instead of coat of arms field.map: '' tags: - 'UG::South_America' @@ -998,11 +1003,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.senegal.fields.field.country - - literal: ' (' - - text: 'green/yellow/red, green star' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.senegal.fields.field.country + description_1: + text: green/yellow/red, green star field.map: '' tags: - 'UG::Africa' @@ -1159,11 +1165,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.romania.fields.field.country - - literal: ' (' - - text: 'slightly lighter blue' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.romania.fields.field.country + description_1: + text: slightly lighter blue field.map: '' tags: - 'UG::Africa' @@ -1212,11 +1219,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.ecuador.fields.field.country - - literal: ' (' - - text: 'with coat of arms' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.ecuador.fields.field.country + description_1: + text: with coat of arms field.map: '' tags: - 'UG::South_America' @@ -1330,11 +1338,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.puerto-rico.fields.field.country - - literal: ' (' - - text: 'blue triangle, red stripes' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.puerto-rico.fields.field.country + description_1: + text: blue triangle, red stripes field.map: '' tags: - 'UG::Caribbean' @@ -1352,11 +1361,12 @@ notes: field.country-info: Constituent country of the Kingdom of the Netherlands. field.flag: '' field.flag-similarity: - message: - - ref: notes.note.nauru.fields.field.country - - literal: ' (' - - text: 'single star below yellow band' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.nauru.fields.field.country + description_1: + text: single star below yellow band field.map: '' tags: - 'UG::Caribbean' @@ -1553,11 +1563,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.colombia.fields.field.country - - literal: ' (' - - text: 'no coat of arms' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.colombia.fields.field.country + description_1: + text: no coat of arms field.map: '' tags: - 'UG::South_America' @@ -1574,16 +1585,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.iraq.fields.field.country - - literal: ' (' - - text: 'text instead of emblem' - - literal: ')' - - literal: ', ' - - ref: notes.note.yemen.fields.field.country - - literal: ' (' - - text: 'no emblem' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.iraq.fields.field.country + country_2: + ref: notes.note.yemen.fields.field.country + description_1: + text: text instead of emblem + description_2: + text: no emblem field.map: '' tags: - 'UG::Africa' @@ -1602,11 +1613,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.nicaragua.fields.field.country - - literal: ' (' - - text: 'different coat of arms, slightly lighter blue' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.nicaragua.fields.field.country + description_1: + text: different coat of arms, slightly lighter blue field.map: '' tags: - 'UG::North_America' @@ -1781,16 +1793,16 @@ notes: field.country-info: Constituent country in the Kingdom of Denmark. field.flag: '' field.flag-similarity: - message: - - ref: notes.note.iceland.fields.field.country - - literal: ' (' - - text: 'blue background, red and white cross' - - literal: ')' - - literal: ', ' - - ref: notes.note.norway.fields.field.country - - literal: ' (' - - text: 'red background, blue and white cross' - - literal: ')' + 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 field.map: '' tags: - 'UG::Europe' @@ -1952,11 +1964,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.bolivia.fields.field.country - - literal: ' (' - - text: 'coat of arms instead of star' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.bolivia.fields.field.country + description_1: + text: coat of arms instead of star field.map: '' tags: - 'UG::Africa' @@ -2102,11 +2115,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.mali.fields.field.country - - literal: ' (' - - text: 'red and green flipped, slightly brighter green' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.mali.fields.field.country + description_1: + text: red and green flipped, slightly brighter green field.map: '' tags: - 'UG::Africa' @@ -2342,16 +2356,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.norway.fields.field.country - - literal: ' (' - - text: 'red background, blue cross' - - literal: ')' - - literal: ', ' - - ref: notes.note.faroe-islands.fields.field.country - - literal: ' (' - - text: 'white background, red and blue cross' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.norway.fields.field.country + country_2: + ref: notes.note.faroe-islands.fields.field.country + description_1: + text: red background, blue cross + description_2: + text: white background, red and blue cross field.map: '' tags: - 'UG::Europe' @@ -2399,16 +2413,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.monaco.fields.field.country - - literal: ' (' - - text: 'narrower, darker red' - - literal: ')' - - literal: ', ' - - ref: notes.note.poland.fields.field.country - - literal: ' (' - - text: 'red and white flipped, darker red' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.monaco.fields.field.country + country_2: + ref: notes.note.poland.fields.field.country + description_1: + text: narrower, darker red + description_2: + text: red and white flipped, darker red field.map: '' tags: - 'UG::Asia' @@ -2443,16 +2457,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.egypt.fields.field.country - - literal: ' (' - - text: 'emblem instead of text' - - literal: ')' - - literal: ', ' - - ref: notes.note.yemen.fields.field.country - - literal: ' (' - - text: 'no text' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.egypt.fields.field.country + country_2: + ref: notes.note.yemen.fields.field.country + description_1: + text: emblem instead of text + description_2: + text: no text field.map: '' tags: - 'UG::Asia' @@ -2470,11 +2484,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.ivory-coast.fields.field.country - - literal: ' (' - - text: 'green and orange flipped, narrower' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.ivory-coast.fields.field.country + description_1: + text: green and orange flipped, narrower field.map: '' tags: - 'UG::Europe' @@ -2543,11 +2558,12 @@ notes: field.country-info: 'Officially Côte d''Ivoire.' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.ireland.fields.field.country - - literal: ' (' - - text: 'orange and green flipped, wider' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.ireland.fields.field.country + description_1: + text: orange and green flipped, wider field.map: '' tags: - 'UG::Africa' @@ -2820,11 +2836,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.austria.fields.field.country - - literal: ' (' - - text: 'brighter red, wider white band' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.austria.fields.field.country + description_1: + text: brighter red, wider white band field.map: '' tags: - 'UG::Europe' @@ -2943,11 +2960,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.netherlands.fields.field.country - - literal: ' (' - - text: 'darker blue' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.netherlands.fields.field.country + description_1: + text: darker blue field.map: '' tags: - 'UG::Europe' @@ -3062,11 +3080,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.guinea.fields.field.country - - literal: ' (' - - text: 'green and red flipped, slightly darker green' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.guinea.fields.field.country + description_1: + text: green and red flipped, slightly darker green field.map: '' tags: - 'UG::Africa' @@ -3245,11 +3264,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.andorra.fields.field.country - - literal: ' (' - - text: 'narrower, coat of arms with motto' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.andorra.fields.field.country + description_1: + text: narrower, coat of arms with motto field.map: '' tags: - 'UG::Europe' @@ -3266,16 +3286,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.indonesia.fields.field.country - - literal: ' (' - - text: 'wider, brighter red' - - literal: ')' - - literal: ', ' - - ref: notes.note.poland.fields.field.country - - literal: ' (' - - text: 'red and white flipped, wider' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.indonesia.fields.field.country + country_2: + ref: notes.note.poland.fields.field.country + description_1: + text: wider, brighter red + description_2: + text: red and white flipped, wider field.map: '' tags: - 'UG::Europe' @@ -3393,11 +3413,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.cura-ao.fields.field.country - - literal: ' (' - - text: 'two stars in top-left corner' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.cura-ao.fields.field.country + description_1: + text: two stars in top-left corner field.map: '' tags: - 'UG::Oceania' @@ -3430,11 +3451,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.luxembourg.fields.field.country - - literal: ' (' - - text: 'lighter blue' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.luxembourg.fields.field.country + description_1: + text: lighter blue field.map: '' tags: - 'UG::Europe' @@ -3467,11 +3489,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.australia.fields.field.country - - literal: ' (' - - text: 'white stars, two more stars' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.australia.fields.field.country + description_1: + text: white stars, two more stars field.map: '' tags: - 'UG::Oceania' @@ -3488,11 +3511,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.el-salvador.fields.field.country - - literal: ' (' - - text: 'different coat of arms, slightly darker blue' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.el-salvador.fields.field.country + description_1: + text: different coat of arms, slightly darker blue field.map: '' tags: - 'UG::North_America' @@ -3669,16 +3693,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.iceland.fields.field.country - - literal: ' (' - - text: 'blue background, red cross' - - literal: ')' - - literal: ', ' - - ref: notes.note.faroe-islands.fields.field.country - - literal: ' (' - - text: 'white background, red and blue cross' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.iceland.fields.field.country + country_2: + ref: notes.note.faroe-islands.fields.field.country + description_1: + text: blue background, red cross + description_2: + text: white background, red and blue cross field.map: '' tags: - 'UG::Europe' @@ -3789,16 +3813,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.sudan.fields.field.country - - literal: ' (' - - text: 'red/white/black, green arrow' - - literal: ')' - - literal: ', ' - - ref: notes.note.sahrawi-arab-democratic-republic.fields.field.country - - literal: ' (' - - text: 'with star and crescent' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.sudan.fields.field.country + country_2: + ref: notes.note.sahrawi-arab-democratic-republic.fields.field.country + description_1: + text: red/white/black, green arrow + description_2: + text: with star and crescent field.map: '' tags: - 'UG::Asia' @@ -3928,16 +3952,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.indonesia.fields.field.country - - literal: ' (' - - text: 'white and red flipped, brighter red' - - literal: ')' - - literal: ', ' - - ref: notes.note.monaco.fields.field.country - - literal: ' (' - - text: 'white and red flipped, narrower' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.indonesia.fields.field.country + country_2: + ref: notes.note.monaco.fields.field.country + description_1: + text: white and red flipped, brighter red + description_2: + text: white and red flipped, narrower field.map: '' tags: - 'UG::Europe' @@ -3987,11 +4011,12 @@ notes: field.country-info: Unincorporated territory of the United States. field.flag: '' field.flag-similarity: - message: - - ref: notes.note.cuba.fields.field.country - - literal: ' (' - - text: 'red triangle, blue stripes' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.cuba.fields.field.country + description_1: + text: red triangle, blue stripes field.map: '' tags: - 'UG::Caribbean' @@ -4008,11 +4033,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.bahrain.fields.field.country - - literal: ' (' - - text: 'narrower, fewer serrated edges, red' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.bahrain.fields.field.country + description_1: + text: narrower, fewer serrated edges, red field.map: '' tags: - 'UG::Asia' @@ -4077,11 +4103,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.chad.fields.field.country - - literal: ' (' - - text: 'slightly darker blue' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.chad.fields.field.country + description_1: + text: slightly darker blue field.map: '' tags: - 'UG::Europe' @@ -4099,11 +4126,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.slovakia.fields.field.country - - literal: ' (' - - text: 'with coat of arms' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.slovakia.fields.field.country + description_1: + text: with coat of arms field.map: '' tags: - 'UG::Europe' @@ -4153,11 +4181,12 @@ notes: field.country-info: Partially recognised state claimed by Morocco. Also known as Western Sahara. field.flag: '' field.flag-similarity: - message: - - ref: notes.note.palestine.fields.field.country - - literal: ' (' - - text: 'no symbol' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.palestine.fields.field.country + description_1: + text: no symbol field.map: '' tags: - 'UG::Africa' @@ -4380,11 +4409,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.cameroon.fields.field.country - - literal: ' (' - - text: 'green/red/yellow, yellow star' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.cameroon.fields.field.country + description_1: + text: green/red/yellow, yellow star field.map: '' tags: - 'UG::Africa' @@ -4500,16 +4530,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.russia.fields.field.country - - literal: ' (' - - text: 'no coat of arms' - - literal: ')' - - literal: ', ' - - ref: notes.note.slovenia.fields.field.country - - literal: ' (' - - text: 'wider, smaller coat of arms' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.russia.fields.field.country + country_2: + ref: notes.note.slovenia.fields.field.country + description_1: + text: no coat of arms + description_2: + text: wider, smaller coat of arms field.map: '' tags: - 'UG::Europe' @@ -4527,11 +4557,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.slovakia.fields.field.country - - literal: ' (' - - text: 'narrower, bigger coat of arms' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.slovakia.fields.field.country + description_1: + text: narrower, bigger coat of arms field.map: '' tags: - 'UG::Europe' @@ -4744,11 +4775,12 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.palestine.fields.field.country - - literal: ' (' - - text: 'black/white/green, red arrow' - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.palestine.fields.field.country + description_1: + text: black/white/green, red arrow field.map: '' tags: - 'UG::Africa' @@ -5413,16 +5445,16 @@ notes: field.country-info: '' field.flag: '' field.flag-similarity: - message: - - ref: notes.note.egypt.fields.field.country - - literal: ' (' - - text: 'with emblem' - - literal: ')' - - literal: ', ' - - ref: notes.note.iraq.fields.field.country - - literal: ' (' - - text: 'with text' - - literal: ')' + format: '{country_1} ({description_1}), {country_2} ({description_2})' + variables: + country_1: + ref: notes.note.egypt.fields.field.country + country_2: + ref: notes.note.iraq.fields.field.country + description_1: + text: with emblem + description_2: + text: with text field.map: '' tags: - 'UG::Asia' diff --git a/fixtures/ultimate-geography/overlays/extensions/hardcore.yaml b/fixtures/ultimate-geography/overlays/extensions/hardcore.yaml index 949adbf..04ced0d 100644 --- a/fixtures/ultimate-geography/overlays/extensions/hardcore.yaml +++ b/fixtures/ultimate-geography/overlays/extensions/hardcore.yaml @@ -173,11 +173,12 @@ notes: field.country-info: Province of Ecuador. field.flag: '' field.flag-similarity: - message: - - ref: notes.note.sierra-leone.fields.field.country - - literal: ' (' - - text: slightly lighter blue - - literal: ')' + format: '{country_1} ({description_1})' + variables: + country_1: + ref: notes.note.sierra-leone.fields.field.country + description_1: + text: slightly lighter blue field.map: '' tags: - 'UG::South_America' diff --git a/fixtures/ultimate-geography/overlays/languages/cs.yaml b/fixtures/ultimate-geography/overlays/languages/cs.yaml index 131fe21..e6bad11 100644 --- a/fixtures/ultimate-geography/overlays/languages/cs.yaml +++ b/fixtures/ultimate-geography/overlays/languages/cs.yaml @@ -26,7 +26,6 @@ translations: 'Also spelled as Sana''a.': 'Také psáno jako San''á.' American Samoa: 'Americká Samoa' Amman: 'Ammán' - 'Andorra (narrower, coat of arms with motto)': 'Andorra (užší, erb s mottem)' Antarctica: Antarktida Antigua and Barbuda: Antigua a Barbuda Arabian Sea: 'Arabské moře' @@ -38,9 +37,7 @@ translations: Athens: 'Athény' Atlantic Ocean: 'Atlantský oceán' Australia: 'Austrálie' - 'Australia (white stars, two more stars)': 'Austrálie (bílé hvězdy, o dvě hvězdy více)' Austria: Rakousko - 'Austria (brighter red, wider white band)': 'Rakousko (světlejší červená, širší bílý pruh)' Autonomous community of Spain.: 'Autonomní společenství Španělska.' Autonomous province of South Korea.: 'Autonomní provincie Jižní Koreji.' Autonomous region of Finland.: 'Autonomní region Finska.' @@ -50,7 +47,6 @@ translations: Azores: Azory Baghdad: 'Bagdád' Bahrain: Bahrajn - 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrajn (užší, méně zoubků, červená)' Balkan Peninsula: 'Balkán' Baltic Sea: 'Baltské moře' Banda Sea: 'Bandské moře' @@ -69,7 +65,6 @@ translations: Bishkek: 'Biškek' Black Sea: 'Černé moře' Bolivia: 'Bolívie' - 'Bolivia (coat of arms instead of star)': 'Bolívie (erb místo hvězdy)' Bosnia and Herzegovina: Bosna a Hercegovina Brazil: 'Brazílie' British Virgin Islands: 'Britské Panenské ostrovy' @@ -81,7 +76,6 @@ translations: Cairo: 'Káhira' Cambodia: 'Kambodža' Cameroon: Kamerun - 'Cameroon (green/red/yellow, yellow star)': 'Kamerun (zelená/červená/žlutá, žlutá hvězda)' Canada: Kanada Canary Islands: 'Kanárské ostrovy' Cape Verde: Kapverdy @@ -93,14 +87,12 @@ translations: Central African Republic: 'Středoafrická republika' Cetinje is an honorary capital.: 'Cetinje je historicky hlavní město.' Chad: 'Čad' - 'Chad (slightly darker blue)': 'Čad (mírně tmavší modrá)' China: 'Čína' 'Chișinău': 'Kišiněv' City of San Marino: San Marino Claimed and controlled: 'Nárokované a kontrolované' Claimed but not controlled: 'Nárokováno, ale nekontrolováno' Colombia: Kolumbie - 'Colombia (no coat of arms)': 'Kolumbie (bez erbu)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Kolombo bývá označováno jako hlavní město, ale Šrí Džajavardanapura Kotte, předměstí Kolomba, je oficiální a legislativní hlavní město.' Comoros: Komory Constituent country of the Kingdom of the Netherlands.: 'Konstituční země Nizozemského království.' @@ -113,8 +105,6 @@ translations: Croatia: Chorvatsko Crown dependency of the United Kingdom.: 'Britské korunní závislé území.' Cuba: Kuba - 'Cuba (red triangle, blue stripes)': 'Kuba (červená šipka, modré pruhy)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (dvě hvězdy v levém horním rohu)' Cyprus: Kypr Czech Republic: 'Česko' Damascus: 'Damašek' @@ -132,11 +122,7 @@ translations: East China Sea: 'Východočínské moře' East Siberian Sea: 'Východosibiřské moře' Ecuador: 'Ekvádor' - 'Ecuador (with coat of arms)': 'Ekvádor (s erbem)' - 'Egypt (emblem instead of text), Yemen (no text)': 'Egypt (emblém místo textu), Yemen (bez textu)' - 'Egypt (with emblem), Iraq (with text)': 'Egypt (má emblém), Irák (má text)' El Salvador: Salvador - 'El Salvador (different coat of arms, slightly darker blue)': 'Salvador (jiný erb, mírně tmavší modrá)' England: Anglie English Channel: 'Lamanšský průliv' Equatorial Guinea: 'Rovníková Guinea' @@ -157,11 +143,9 @@ translations: French Polynesia: 'Francouzská Polynésie' Georgia: Gruzie Germany: 'Německo' - 'Ghana (star instead of coat of arms)': 'Ghana (hvězda místo erbu)' Greece: 'Řecko' Greenland: 'Grónsko' Guatemala City: Ciudad de Guatemala - 'Guinea (green and red flipped, slightly darker green)': 'Guinea (prohozená zelená a červená, mírně tmavší zelená)' Gulf of Alaska: 'Aljašský záliv' Gulf of California: 'Kalifornský záliv' Gulf of Carpentaria: 'Carpentarský záliv' @@ -177,26 +161,19 @@ translations: Hudson Bay: 'Hudsonův záliv' Hungary: 'Maďarsko' Iceland: Island - '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říž)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Island (modré pozadí, červený kříž), Faerské ostrovy (bílé pozadí, červený a modrý kříž)' Independent state claimed by Moldova.: 'Nezávislý stát nárokovaný Moldavskem.' Independent state claimed by Somalia.: 'Vzácně označovaný jako Somálijsko, nezávislý stát nárokovaný Somálskem.' India: Indie Indian Ocean: 'Indický oceán' Indonesia: 'Indonésie' - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonésie (prohozená bílá a červená), Monako (prohozená bílá a červená, užší)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonésie (širší, světlejší červená), Polsko (prohozená červená a bílá, širší)' Iran: 'Írán' Iraq: 'Irák' - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irák (text místo emblému), Jemen (bez emblému)' Ireland: Irsko - 'Ireland (orange and green flipped, wider)': 'Irsko (prohozená oranžová a zelená, širší)' Islamabad: 'Islámábád' Isle of Man: Man Israel: Izrael Italy: 'Itálie' Ivory Coast: 'Pobřeží slonoviny' - 'Ivory Coast (green and orange flipped, narrower)': 'Pobřeží slonoviny (prohozená zelená a oranžová, užší)' Jamaica: Jamajka Japan: Japonsko Java: 'Jáva' @@ -219,7 +196,6 @@ translations: Laayoune: 'Al-´Ajún' Labrador Sea: 'Labradorské moře' Latvia: 'Lotyšsko' - 'Latvia (darker red, narrower white band)': 'Lotyšsko (tmavší červená, užší bílý pruh)' Lebanon: Libanon Liberia: 'Libérie' Libya: Libye @@ -229,12 +205,10 @@ translations: Ljubljana: 'Lublaň' London: 'Londýn' Luxembourg: Lucembursko - 'Luxembourg (lighter blue)': 'Lucembursko (světlejší modrá)' Luxembourg City: Lucemburk Macau: Macao Malaysia: Malajsie Maldives: Maledivy - 'Mali (red and green flipped, slightly brighter green)': 'Mali (prohozená červená a zelená, mírně světlejší zelená)' 'Malé': Male Manama: 'Manáma' Marshall Islands: Marshallovy ostrov @@ -248,8 +222,6 @@ translations: Micronesia: 'Mikronésie' Mogadishu: 'Mogadišo' Moldova: Moldavsko - 'Moldova (wider, coat of arms with eagle)': 'Moldavsko (širší, erb s orlem)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (užší, tmavší červená), Polsko (prohozená červená a bílá, tmavší červená)' Mongolia: Mongolsko Montenegro: 'Černá Hora' Morocco: Maroko @@ -257,17 +229,13 @@ translations: Mozambique: Mosambik Muscat: Maskat Namibia: Namibie - 'Nauru (single star below yellow band)': 'Nauru (jen jedna hvězda pod žlutým pruhem)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru nemá žádné oficiální hlavní město, okres Yaren je de facto hlavní město.' Naypyidaw: Neipyijto Nepal: 'Nepál' Netherlands: Nizozemsko - 'Netherlands (darker blue)': 'Nizozemsko (tmavší modrá)' New Caledonia: 'Nová Kaledonie' New Delhi: 'Nové Dillí' New Zealand: 'Nový Zéland' - 'New Zealand (red stars, two fewer stars)': 'Nový Zéland (červené hvězdy, o dvě hvězdy méně)' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (jiný erb, mírně světlejší modrá)' Nicosia: 'Nikósie' Nigeria: 'Nigérie' North America: 'Severní Amerika' @@ -279,7 +247,6 @@ translations: Northern Ireland: 'Severní Irsko' Northern Mariana Islands: 'Severní Mariany' Norway: Norsko - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norsko (červené pozadí, modrý a bílý kříž), Faerské ostrovy (bílé pozadí, červený a modrý kříž)' Norwegian Sea: 'Norské moře' Not a sovereign country: 'Nejedná se o suverénní stát' Nouakchott: 'Nuakšott' @@ -294,8 +261,6 @@ translations: Pacific Ocean: 'Tichý oceán' Pakistan: 'Pákistán' Palestine: 'Stát Palestina' - 'Palestine (black/white/green, red arrow)': 'Stát Palestina (černá/bílá/zelená, červená šipka)' - 'Palestine (no symbol)': 'Stát Palestina (bez symbol)' Panama City: 'Panamá' Papua New Guinea: 'Papua Nová Guinea' Paris: 'Paříž' @@ -314,19 +279,15 @@ translations: Pretoria, Cape Town, Bloemfontein: 'Pretorie, Kapské Město, Bloemfontein' Pristina: 'Priština' Puerto Rico: Portoriko - 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (modrá šipka, červené pruhy)' Pyongyang: Pchjongjang Qatar: Katar - 'Qatar (wider, more serrated edges, maroon)': 'Katar (širší, větší počet zoubků, tmavší červená)' Red Sea: 'Rudé moře' Region of France.: Region Francie. Republic of the Congo: 'Konžská republika' Riyadh: 'Rijád' Romania: Rumunsko - 'Romania (slightly lighter blue)': 'Rumunsko (mírně světlejší modrá)' Rome: 'Řím' Russia: Rusko - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rusko (bez erbu), Slovinsko (širší, menší erb)' Sahrawi Arab Democratic Republic: 'Saharská arabská demokratická republika' Saint Kitts and Nevis: 'Svatý Kryštof a Nevis' Saint Lucia: 'Svatá Lucie' @@ -342,7 +303,6 @@ translations: Sea of Japan: 'Japonské moře' Sea of Okhotsk: 'Ochotské moře' Semi-autonomous region of Tanzania.: 'Částečně autonomní region Tanzanie.' - 'Senegal (green/yellow/red, green star)': 'Senegal (zelená/žlutá/červená, zelená hvězda)' Seoul: Soul Serbia: Srbsko Seychelles: Seychely @@ -350,8 +310,6 @@ translations: Singapore: Singapur Sint Maarten: 'Svatý Martin (nizozemská část)' Slovakia: Slovensko - 'Slovakia (narrower, bigger coat of arms)': 'Slovensko (užší, větší erb)' - 'Slovakia (with coat of arms)': 'Slovensko (s erbem)' Slovenia: Slovinsko Sofia: Sofie Solomon Islands: 'Šalomounovy ostrovy' @@ -378,7 +336,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Subregion Oceánie zahrnjící tisíce malých ostrovů v západním Pacifiku.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Subregion Oceánie zahrnující Vanuatu, Šalomounovy ostrovy, Fidži a Papua Novou Guineu.' Sudan: 'Súdán' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Súdán (červená/bílá/černá, zelená šipka), Saharská arabská demokratická republika (s hvězdou a půlměsícem)' Sukhumi: Suchumi Suriname: Surinam Svalbard: 'Špicberky' @@ -436,6 +393,57 @@ translations: Yerevan: Jerevan Zagreb: 'Záhřeb' Zambia: Zambie + black/white/green, red arrow: 'černá/bílá/zelená, červená šipka' + blue background, red and white cross: 'modré pozadí, červený a bílý kříž' + blue background, red cross: 'modré pozadí, červený kříž' + blue triangle, red stripes: 'modrá šipka, červené pruhy' + brighter red, wider white band: 'světlejší červená, širší bílý pruh' + coat of arms instead of star: 'erb místo hvězdy' + darker blue: 'tmavší modrá' + darker red, narrower white band: 'tmavší červená, užší bílý pruh' + different coat of arms, slightly darker blue: 'jiný erb, mírně tmavší modrá' + different coat of arms, slightly lighter blue: 'jiný erb, mírně světlejší modrá' + emblem instead of text: 'emblém místo textu' + green and orange flipped, narrower: 'prohozená zelená a oranžová, užší' + green and red flipped, slightly darker green: 'prohozená zelená a červená, mírně tmavší zelená' + green/red/yellow, yellow star: 'zelená/červená/žlutá, žlutá hvězda' + green/yellow/red, green star: 'zelená/žlutá/červená, zelená hvězda' + lighter blue: 'světlejší modrá' + narrower, bigger coat of arms: 'užší, větší erb' + narrower, coat of arms with motto: 'užší, erb s mottem' + narrower, darker red: 'užší, tmavší červená' + narrower, fewer serrated edges, red: 'užší, méně zoubků, červená' + no coat of arms: bez erbu + no emblem: 'bez emblému' + no symbol: bez symbol + no text: bez textu + orange and green flipped, wider: 'prohozená oranžová a zelená, širší' + red and green flipped, slightly brighter green: 'prohozená červená a zelená, mírně světlejší zelená' + red and white flipped, darker red: 'prohozená červená a bílá, tmavší červená' + red and white flipped, wider: 'prohozená červená a bílá, širší' + red background, blue and white cross: 'červené pozadí, modrý a bílý kříž' + red background, blue cross: 'červené pozadí, modrý a bílý kříž' + red stars, two fewer stars: 'červené hvězdy, o dvě hvězdy méně' + red triangle, blue stripes: 'červená šipka, modré pruhy' + red/white/black, green arrow: 'červená/bílá/černá, zelená šipka' + single star below yellow band: 'jen jedna hvězda pod žlutým pruhem' + slightly darker blue: 'mírně tmavší modrá' + slightly lighter blue: 'mírně světlejší modrá' + star instead of coat of arms: 'hvězda místo erbu' + text instead of emblem: 'text místo emblému' + two stars in top-left corner: 'dvě hvězdy v levém horním rohu' + white and red flipped, brighter red: 'prohozená bílá a červená' + white and red flipped, narrower: 'prohozená bílá a červená, užší' + white background, red and blue cross: 'bílé pozadí, červený a modrý kříž' + white stars, two more stars: 'bílé hvězdy, o dvě hvězdy více' + wider, brighter red: 'širší, světlejší červená' + wider, coat of arms with eagle: 'širší, erb s orlem' + wider, more serrated edges, maroon: 'širší, větší počet zoubků, tmavší červená' + wider, smaller coat of arms: 'širší, menší erb' + with coat of arms: s erbem + with emblem: 'má emblém' + with star and crescent: 's hvězdou a půlměsícem' + with text: 'má text' 'Åland Islands': Alandy contextual: notes.note: @@ -455,6 +463,8 @@ translations: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' cayman-islands: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' + cuba.fields.field.flag-similarity.message.variables.country_1: + Puerto Rico: Puerto Rico djibouti: Djibouti: 'Džibutsko' djibouti.fields.field.capital: @@ -469,6 +479,8 @@ translations: Overseas territory of the United Kingdom.: 'Zámořské území Velké Británie.' greenland: Constituent country in the Kingdom of Denmark.: 'Autonomní součást Dánského království.' + iraq.fields.field.flag-similarity.message.variables.country_2: + Yemen: Yemen java: Island of Indonesia.: 'Indonéský ostrov.' madeira: @@ -479,6 +491,8 @@ translations: Monaco: Monaco-Ville new-caledonia: Overseas territory of France.: 'Zámořské území Francie.' + poland.fields.field.flag-similarity.message.variables.country_2: + Monaco: Monako saint-martin: Overseas territory of France.: 'Konstituční země Francie.' south-ossetia: @@ -604,6 +618,7 @@ translations: - Mayotte - Mbabane - Minsk + - Monaco - Monrovia - Montevideo - Moroni diff --git a/fixtures/ultimate-geography/overlays/languages/da.yaml b/fixtures/ultimate-geography/overlays/languages/da.yaml index b02924d..81ff8a0 100644 --- a/fixtures/ultimate-geography/overlays/languages/da.yaml +++ b/fixtures/ultimate-geography/overlays/languages/da.yaml @@ -22,7 +22,6 @@ translations: Also known as the Sea of Cortez.: '' 'Also spelled as Sana''a.': '' American Samoa: Amerikansk Samoa - 'Andorra (narrower, coat of arms with motto)': 'Andora (smallere, våbenskjold med motto)' Antarctica: Antarktis Antigua and Barbuda: Antigua og Barbuda Arabian Sea: Det Arabiske Hav @@ -34,9 +33,7 @@ translations: Athens: Athen Atlantic Ocean: Atlanterhavet Australia: Australien - 'Australia (white stars, two more stars)': 'Australien (hvide stjerner, to stjerner mere)' Austria: 'Østrig' - 'Austria (brighter red, wider white band)': 'Østrig (lysere rød, bredere hvid stribe)' Autonomous community of Spain.: Selvstyrende region af Spanien. Autonomous province of South Korea.: Selvstyrende provins i Sydkorea. Autonomous region of Finland.: Selvstyrende region i Finland. @@ -45,7 +42,6 @@ translations: Azerbaijan: Aserbajdsjan Azores: Azorerne Baghdad: Bagdad - 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrain (smallere, færre takkede kanter, rød)' Balkan Peninsula: 'Balkanhalvøen' Baltic Sea: 'Østersøen' Banda Sea: Bandahavet @@ -57,7 +53,6 @@ translations: Bering Strait: 'Beringstrædet' Bishkek: Bisjkek Black Sea: Sortehavet - 'Bolivia (coat of arms instead of star)': 'Bolivia (våbenskjold i stedet for stjerne)' Bosnia and Herzegovina: Bosnien-Hercegovina 'Brasília': Brasilia Brazil: Brasilien @@ -67,7 +62,6 @@ translations: Bulgaria: Bulgarien Cambodia: Cambodja Cameroon: Cameroun - 'Cameroon (green/red/yellow, yellow star)': 'Cameroun (grøn/rød/gul, gul stjerne)' Canary Islands: 'De Kanariske Øer' Cape Verde: Kap Verde Caribbean Sea: Caribiske Hav @@ -78,13 +72,11 @@ translations: Central African Republic: Den Centralafrikanske Republik Cetinje is an honorary capital.: Cetinje har status som Montenegros gamle royale hovedstad. Chad: Tchad - 'Chad (slightly darker blue)': 'Tchad (lidt mørkere blå)' China: Kina 'Chișinău': Chisinau City of San Marino: San Marino Claimed and controlled: 'Gjort krav på og kontrolleret' Claimed but not controlled: 'Gjort krav på, men ikke kontrolleret' - 'Colombia (no coat of arms)': 'Colombia (intet våbenskjold)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Colombo bliver ofte benævnt som hovedstad, men Sri Jayawardenapura Kotte, en forstad til Colombo, er den officielle hovedstad.' Comoros: Comorerne Constituent country in the Kingdom of Denmark.: 'Selvstyrende nation i Det Danske Rigsfællesskab.' @@ -96,8 +88,6 @@ translations: Corsica: Korsika Croatia: Kroatien Crown dependency of the United Kingdom.: Britisk kronbesiddelse. - 'Cuba (red triangle, blue stripes)': 'Cuba (rød trekant, blå striber)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (to stjerne i øverste venstre hjørne)' Cyprus: Cypern Czech Republic: Tjekkiet Damascus: Damaskus @@ -111,11 +101,7 @@ translations: Dushanbe: Dusjanbe East China Sea: 'Det Østkinesiske Hav' East Siberian Sea: 'Det Østsibiriske Hav' - 'Ecuador (with coat of arms)': 'Ecuador (med våbenskjold)' Egypt: 'Ægypten' - 'Egypt (emblem instead of text), Yemen (no text)': 'Ægypten (emblem i stedet for tekst), Yemen (ingen tekst)' - 'Egypt (with emblem), Iraq (with text)': 'Ægypten (med emblem), Irak (med tekst)' - 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (forskelligt våbenskjold, lidt mørkere blå)' English Channel: Den Engelske Kanal Equatorial Guinea: 'Ækvatorialguinea' Estonia: Estland @@ -132,10 +118,8 @@ translations: French Polynesia: Fransk Polynesien Georgia: Georgien Germany: Tyskland - 'Ghana (star instead of coat of arms)': 'Ghana (stjerner i stedet for våbenskjold)' Greece: 'Grækenland' Greenland: 'Grønland' - 'Guinea (green and red flipped, slightly darker green)': 'Guinea (grøn og rød byttet rundt, lidt mørkere grøn)' Gulf of Alaska: Alaskagolfen Gulf of California: Den Californiske Havbugt Gulf of Carpentaria: Carpentariabugten @@ -149,24 +133,17 @@ translations: Hudson Bay: Hudsonbugten Hungary: Ungarn Iceland: Island - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Island (blå baggrund, rødt og hvidt kors), Norge (rød baggrund, blåt og hvidt kors)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Island (blå baggrun, rødt kors), Færøerne (hvid baggrund, rødt og blåt kors)' Independent state claimed by Georgia.: 'Uafhængig stat som Georgien gør krav på.' Independent state claimed by Moldova.: 'Uafhængig stat som Moldova gør krav på.' Independent state claimed by Somalia.: 'Uafhængig stat som Somalia gør krav på.' India: Indien Indian Ocean: Det Indiske Ocean Indonesia: Indonesien - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesien (hvid og rød byttet rundt, lysere rød), Monaco (hvid og rød byttet rundt, smallere)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesien (bredere, lysere rød), Polen (rød og hvid byttet rundt, bredere)' Iraq: Irak - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (tekst i stedet for emblem), Yemen (intet emblem)' Ireland: Irland - 'Ireland (orange and green flipped, wider)': 'Irland (orange og grøn byttet rundt, bredere)' Island of Indonesia.: 'Ø i Indonesien.' Italy: Italien Ivory Coast: Elfenbenskysten - 'Ivory Coast (green and orange flipped, narrower)': 'Elfenbenskysten (grøn og orange byttet rundt, smallere)' Jakarta: Djakarta Jeju: Cheju-do Kathmandu: Katmandu @@ -178,35 +155,26 @@ translations: Laayoune: El Aaiun Labrador Sea: Labradorhavet Latvia: Letland - 'Latvia (darker red, narrower white band)': 'Letland (mørkere rød, smallere hvid stribe)' Lebanon: Libanon Libya: Libyen Lisbon: Lissabon Lithuania: Litauen - 'Luxembourg (lighter blue)': 'Luxembourg (lysere blå)' Luxembourg City: Luxembourg Macau: Macao Madagascar: Madagaskar Maldives: Maldiverne - 'Mali (red and green flipped, slightly brighter green)': 'Mali (rød og grøn byttet rundt, lidt lysere grøn)' Marshall Islands: 'Marshalløerne' Mauritania: Mauretanien Mediterranean Sea: Middelhavet Melanesia: Melanesien Micronesia: Mikronesien - 'Moldova (wider, coat of arms with eagle)': 'Moldova (bredere, våbenskjold med ørn)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (smallere, mørkere rød), Polen (rød og hvid byttet rundt, mørkere rød)' Mongolia: Mongoliet Morocco: Marokko Moscow: Moskva 'N''Djamena': Ndjamena - 'Nauru (single star below yellow band)': 'Nauru (enkelt stjerne under gul stribe)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru har ingen officiel hovedstad; Yaren distriktet er de facto hovedstad.' Netherlands: Nederlandene - 'Netherlands (darker blue)': 'Nederlandene (mørkere blå)' New Caledonia: Ny Kaledonien - 'New Zealand (red stars, two fewer stars)': 'New Zealand (røde stjerner, to færre stjerner)' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (forskelligt våbenskjold, lidt lysere blå)' North America: Nordamerika North Korea: Nordkorea North Macedonia: Nordmakedonien @@ -216,7 +184,6 @@ translations: Northern Ireland: Nordirland Northern Mariana Islands: Nordmarianerne Norway: Norge - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norge (rød baggrund, blåt og hvidt kors), Færøerne (hvid baggrund, rødt og blåt kors)' Norwegian Sea: Norskehavet Not a sovereign country: 'Ikke et selvstændigt land' 'Nukuʻalofa': Nukualofa @@ -229,8 +196,6 @@ translations: Overseas department of France.: 'Fransk oversøisk departement.' Pacific Ocean: Stillehavet Palestine: 'Palæstina' - 'Palestine (black/white/green, red arrow)': 'Palæstina (sort/hvid/grøn, rød pil)' - 'Palestine (no symbol)': 'Palæstina (intet symbol)' Papua New Guinea: Papua Ny Guinea Partially recognised state claimed by China.: 'Delvist anerkendt stat som Kina gør krav på.' Partially recognised state claimed by Morocco. Also known as Western Sahara.: 'Delvist anerkendt stat som Marokko gør krav på. Også kendt som Saharawi Arabiske Demokratiske Republik.' @@ -242,16 +207,12 @@ translations: Polynesia: Polynesien Porto-Novo: Porto Novo Prague: Prag - 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blå trekant, røde striber)' - 'Qatar (wider, more serrated edges, maroon)': 'Qatar (bredere, flere takkede kanter, kastanjabrun)' Red Sea: 'Det Røde Hav' Region of France.: Region i Frankrig. Republic of the Congo: Republikken Congo Romania: 'Rumænien' - 'Romania (slightly lighter blue)': 'Rumænien (lidt lysere blå)' Rome: Rom Russia: Rusland - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rusland (intet våbenskjold), Slovenien (bredere, mindre våbenskjold)' Sahrawi Arab Democratic Republic: Vestsahara Saint Kitts and Nevis: Saint Kitts og Nevis Saint Martin: Saint-Martin @@ -265,14 +226,11 @@ translations: Sea of Japan: Japanske Hav Sea of Okhotsk: Det Okhotske Hav Semi-autonomous region of Tanzania.: Semi-autonom del af Tanzania. - 'Senegal (green/yellow/red, green star)': 'Senegal (grøn/gul/rød, grøn stjerne)' Serbia: Serbien Seychelles: Seychellerne Sicily: Sicilien Sint Maarten: Sint-Maarten Slovakia: Slovakiet - 'Slovakia (narrower, bigger coat of arms)': 'Slovakiet (smallere, større våbenskjold)' - 'Slovakia (with coat of arms)': 'Slovakiet (med våbenskjold)' Slovenia: Slovenien Solomon Islands: 'Salomonøerne' South Africa: Sydafrika @@ -295,7 +253,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Område af Oceanien som indeholder tusindvis af små øer i det centrale og sydlige Stillehav.' Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Område af Oceanien der indeholder tusindvis af små øer i det vestlige Stillehav.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Område af Oceanien som indeholder Vanuatu, Salomonøerne, Fiji og Papua Ny Guinea.' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (rød/hvid/sort, grøn pil), Vestsahara (med stjerne og halvmåne)' Suriname: Surinam Sweden: Sverige Switzerland: Schweiz @@ -341,6 +298,57 @@ translations: World region covering the Australian continent and most of the islands in the Pacific Ocean.: 'Verdensdel som dækker det Australske kontinent og de fleste af øerne i Stillehavet.' Yellow Sea: Gule Hav Yerevan: Jerevan + black/white/green, red arrow: 'sort/hvid/grøn, rød pil' + blue background, red and white cross: 'blå baggrund, rødt og hvidt kors' + blue background, red cross: 'blå baggrun, rødt kors' + blue triangle, red stripes: 'blå trekant, røde striber' + brighter red, wider white band: 'lysere rød, bredere hvid stribe' + coat of arms instead of star: 'våbenskjold i stedet for stjerne' + darker blue: 'mørkere blå' + darker red, narrower white band: 'mørkere rød, smallere hvid stribe' + different coat of arms, slightly darker blue: 'forskelligt våbenskjold, lidt mørkere blå' + different coat of arms, slightly lighter blue: 'forskelligt våbenskjold, lidt lysere blå' + emblem instead of text: emblem i stedet for tekst + green and orange flipped, narrower: 'grøn og orange byttet rundt, smallere' + green and red flipped, slightly darker green: 'grøn og rød byttet rundt, lidt mørkere grøn' + green/red/yellow, yellow star: 'grøn/rød/gul, gul stjerne' + green/yellow/red, green star: 'grøn/gul/rød, grøn stjerne' + lighter blue: 'lysere blå' + narrower, bigger coat of arms: 'smallere, større våbenskjold' + narrower, coat of arms with motto: 'smallere, våbenskjold med motto' + narrower, darker red: 'smallere, mørkere rød' + narrower, fewer serrated edges, red: 'smallere, færre takkede kanter, rød' + no coat of arms: 'intet våbenskjold' + no emblem: intet emblem + no symbol: intet symbol + no text: ingen tekst + orange and green flipped, wider: 'orange og grøn byttet rundt, bredere' + red and green flipped, slightly brighter green: 'rød og grøn byttet rundt, lidt lysere grøn' + red and white flipped, darker red: 'rød og hvid byttet rundt, mørkere rød' + red and white flipped, wider: 'rød og hvid byttet rundt, bredere' + red background, blue and white cross: 'rød baggrund, blåt og hvidt kors' + red background, blue cross: 'rød baggrund, blåt og hvidt kors' + red stars, two fewer stars: 'røde stjerner, to færre stjerner' + red triangle, blue stripes: 'rød trekant, blå striber' + red/white/black, green arrow: 'rød/hvid/sort, grøn pil' + single star below yellow band: enkelt stjerne under gul stribe + slightly darker blue: 'lidt mørkere blå' + slightly lighter blue: 'lidt lysere blå' + star instead of coat of arms: 'stjerner i stedet for våbenskjold' + text instead of emblem: tekst i stedet for emblem + two stars in top-left corner: 'to stjerne i øverste venstre hjørne' + white and red flipped, brighter red: 'hvid og rød byttet rundt, lysere rød' + white and red flipped, narrower: 'hvid og rød byttet rundt, smallere' + white background, red and blue cross: 'hvid baggrund, rødt og blåt kors' + white stars, two more stars: hvide stjerner, to stjerner mere + wider, brighter red: 'bredere, lysere rød' + wider, coat of arms with eagle: 'bredere, våbenskjold med ørn' + wider, more serrated edges, maroon: bredere, flere takkede kanter, kastanjabrun + wider, smaller coat of arms: 'bredere, mindre våbenskjold' + with coat of arms: 'med våbenskjold' + with emblem: med emblem + with star and crescent: 'med stjerne og halvmåne' + with text: med tekst 'Åland Islands': 'Åland' contextual: notes.note: @@ -364,6 +372,8 @@ translations: Overseas territory of the United Kingdom.: 'Oversøisk territorie i Storbritannien.' madeira: Autonomous region of Portugal.: 'Selvstændig region i Portugal.' + moldova.fields.field.flag-similarity.message.variables.country_1: + Andorra: Andora new-caledonia: Overseas territory of France.: 'Fransk oversøisk departement.' saint-martin: diff --git a/fixtures/ultimate-geography/overlays/languages/de.yaml b/fixtures/ultimate-geography/overlays/languages/de.yaml index 294ca1d..1bdf339 100644 --- a/fixtures/ultimate-geography/overlays/languages/de.yaml +++ b/fixtures/ultimate-geography/overlays/languages/de.yaml @@ -22,7 +22,6 @@ translations: Also known as the Sea of Cortez.: 'Auch Cortés-See.' 'Also spelled as Sana''a.': '' American Samoa: Amerikanisch-Samoa - 'Andorra (narrower, coat of arms with motto)': 'Andorra (schmaler, Wappen mit Wahlspruch)' Antarctica: Antarktis Antigua and Barbuda: Antigua und Barbuda Arabian Sea: Arabisches Meer @@ -35,9 +34,7 @@ translations: Athens: Athen Atlantic Ocean: Atlantischer Ozean Australia: Australien - 'Australia (white stars, two more stars)': 'Australien (weiße Sterne, zwei weitere Sterne)' Austria: 'Österreich' - 'Austria (brighter red, wider white band)': 'Österreich (helleres Rot, breiteres weißes Band)' Autonomous community of Spain.: Autonome Gemeinschaft Spaniens. Autonomous province of South Korea.: 'Autonome Provinz Südkoreas.' Autonomous region of Finland.: 'Auch Ålandinseln. Autonome Region Finnlands.' @@ -47,7 +44,6 @@ translations: Azerbaijan: Aserbaidschan Azores: 'Azoren (Habichtsinseln)' Baghdad: Bagdad - 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrain (enger, weniger Zacken, rot)' Balkan Peninsula: Balkanhalbinsel Baltic Sea: Ostsee Banda Sea: Bandasee @@ -62,7 +58,6 @@ translations: Bishkek: Bischkek Black Sea: Schwarzes Meer Bolivia: Bolivien - 'Bolivia (coat of arms instead of star)': 'Bolivien (Wappen anstatt Stern)' Bosnia and Herzegovina: Bosnien und Herzegowina Brazil: Brasilien British Virgin Islands: Britische Jungferninseln @@ -72,7 +67,6 @@ translations: Cairo: Kairo Cambodia: Kambodscha Cameroon: Kamerun - 'Cameroon (green/red/yellow, yellow star)': 'Kamerun (Grün/Rot/Gelb, gelber Stern)' Canada: Kanada Canary Islands: Kanarische Inseln Cape Verde: Kap Verde @@ -84,13 +78,11 @@ translations: Central African Republic: Zentralafrikanische Republik Cetinje is an honorary capital.: Cetinje ist eine Ehrenhauptstadt. Chad: Tschad - 'Chad (slightly darker blue)': 'Tschad (etwas dunkleres Blau)' China: Volksrepublik China City of San Marino: Stadt San Marino Claimed and controlled: Behauptet und kontrolliert Claimed but not controlled: Behauptet, aber nicht kontrolliert Colombia: Kolumbien - 'Colombia (no coat of arms)': 'Kolumbien (kein Wappen)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: Colombo wird oft als die Hauptstadt bezeichnet, aber Sri Jayawardenepura Kotte, ein Vorort von Colombo, ist die offizielle, legislative Hauptstadt. Comoros: Komoren Constituent country in the Kingdom of Denmark.: 'Autonomer Bestandteil des Königreichs Dänemark.' @@ -103,8 +95,6 @@ translations: Croatia: Kroatien Crown dependency of the United Kingdom.: Kronbesitz der britischen Krone. Cuba: Kuba - 'Cuba (red triangle, blue stripes)': 'Kuba (rotes Dreieck, blaue Streifen)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (zwei Sterne in der oberen linken Ecke)' Cyprus: Republik Zypern Czech Republic: Tschechien Damascus: Damaskus @@ -119,11 +109,7 @@ translations: Dushanbe: Duschanbe East China Sea: Ostchinesisches Meer East Siberian Sea: Ostsibirische See - 'Ecuador (with coat of arms)': 'Ecuador (mit Wappen)' Egypt: 'Ägypten' - 'Egypt (emblem instead of text), Yemen (no text)': 'Ägypten (Emblem statt Text), Jemen (kein Text)' - 'Egypt (with emblem), Iraq (with text)': 'Ägypten (mit Emblem), Irak (mit Text)' - 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (anderes Wappen, etwas dunkleres Blau)' English Channel: 'Ärmelkanal' Equatorial Guinea: 'Äquatorialguinea' Estonia: Estland @@ -143,11 +129,9 @@ translations: Gabon: Gabun Georgia: Georgien Germany: Deutschland - 'Ghana (star instead of coat of arms)': 'Ghana (Stern anstatt Wappen)' Greece: Griechenland Greenland: 'Grönland' Guatemala City: Guatemala-Stadt - 'Guinea (green and red flipped, slightly darker green)': 'Guinea (Grün und Rot vertauscht, etwas dunkleres Grün)' Gulf of Alaska: Golf von Alaska Gulf of California: Golf von Kalifornien Gulf of Carpentaria: Golf von Carpentaria @@ -160,23 +144,16 @@ translations: Hong Kong: Hongkong Hungary: Ungarn Iceland: Island - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Island (blauer Hintergrund, rotes und weißes Kreuz), Norwegen (roter Hintergrund, blaues und weißes Kreuz)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Island (blauer Hintergrund, rotes Kreuz), Färöer (weißer Hintergrund, rotes und blaues Kreuz)' Independent state claimed by Georgia.: 'Unabhängiger Staat, von Georgien beansprucht.' Independent state claimed by Moldova.: 'Unabhängiger Staat, von Republik Moldau (Moldawien) beansprucht.' Independent state claimed by Somalia.: 'Unabhängiger Staat, von Somalia beansprucht.' India: Indien Indian Ocean: Indischer Ozean Indonesia: Indonesien - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesien (Weiß und Rot vertauscht, helleres Rot), Monaco (Weiß und Rot vertauscht, schmaler)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesien (breiter, helleres Rot), Polen (Rot und Weiß vertauscht, breiter)' Iraq: Irak - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (Text anstatt Emblem), Jemen (kein Emblem)' Ireland: Irland - 'Ireland (orange and green flipped, wider)': 'Irland (Orange und Grün vertauscht, breiter)' Italy: Italien Ivory Coast: 'Elfenbeinküste' - 'Ivory Coast (green and orange flipped, narrower)': 'Elfenbeinküste (Grün und Orange vertauscht, schmaler)' Jamaica: Jamaika Jeju: Jeju-do Jordan: Jordanien @@ -192,18 +169,15 @@ translations: Laayoune: 'El Aaiún' Labrador Sea: Labradorsee Latvia: Lettland - 'Latvia (darker red, narrower white band)': 'Lettland (dunkleres rot, dünneres weißes Band)' Lebanon: Libanon Libya: Libyen Lisbon: Lissabon Lithuania: Litauen Luxembourg: Luxemburg - 'Luxembourg (lighter blue)': 'Luxemburg (helleres Blau)' Luxembourg City: Luxemburg Madagascar: Madagaskar Madeira: Autonome Region Madeira Maldives: Malediven - 'Mali (red and green flipped, slightly brighter green)': 'Mali (Rot und Grün vertauscht, etwas helleres Grün)' Marshall Islands: Marshallinseln Mauritania: Mauretanien Mediterranean Sea: Mittelmeer @@ -213,23 +187,17 @@ translations: Micronesia: Mikronesien Mogadishu: Mogadischu Moldova: Republik Moldau - 'Moldova (wider, coat of arms with eagle)': 'Republik Moldau/Moldawien (breiter, Wappen mit Adler)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (schmaler, dunkleres rot), Polen (Rot und Weiß vertauscht, dunkleres rot)' Mongolia: Mongolei Morocco: Marokko Moscow: Moskau Mozambique: Mosambik Muscat: Maskat 'N''Djamena': 'N’Djamena' - 'Nauru (single star below yellow band)': 'Nauru (einzelner Stern unter gelbem Band)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru hat keine offizielle Hauptstadt; der Yaren District ist der Regierungssitz.' Netherlands: Niederlande - 'Netherlands (darker blue)': 'Niederlande (dunkleres Blau)' New Caledonia: Neukaledonien New Delhi: Neu-Delhi New Zealand: Neuseeland - 'New Zealand (red stars, two fewer stars)': 'Neuseeland (rote Sterne, zwei Sterne weniger)' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (anderes Wappen, etwas helleres Blau)' Nicosia: Nikosia North America: Nordamerika North Korea: Nordkorea @@ -240,7 +208,6 @@ translations: Northern Ireland: Nordirland Northern Mariana Islands: 'Nördliche Marianen' Norway: Norwegen - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norwegen (roter Hintergrund, blaues Kreuz), Färöer (weißer Hintergrund, rotes und blaues Kreuz)' Norwegian Sea: 'Europäisches Nordmeer' Not a sovereign country: 'Kein souveräner Staat' 'Oblast (administrative region) of the Russian Federation.': 'Oblast (administratives Gebiet) der Russischen Föderation.' @@ -253,8 +220,6 @@ translations: Overseas territory of France.: 'Französisches Überseegebiet.' Pacific Ocean: Pazifischer Ozean Palestine: 'Palästina' - 'Palestine (black/white/green, red arrow)': 'Palästina (Schwarz/Weiß/Grün, roter Pfeil)' - 'Palestine (no symbol)': 'Palästina (kein Symbol)' Panama City: Panama-Stadt Papua New Guinea: Papua-Neuguinea Partially recognised state claimed by China.: Teilweise anerkannter Staat, von China beansprucht. @@ -267,19 +232,15 @@ translations: Polynesia: Polynesien Prague: Prag Pretoria, Cape Town, Bloemfontein: Pretoria, Kapstadt, Bloemfontein - 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blaues Dreieck, rote Streifen)' Pyongyang: 'Pjöngjang' Qatar: Katar - 'Qatar (wider, more serrated edges, maroon)': 'Katar (breiter, mehr kleinere Zacken, rotbraun)' Red Sea: Rotes Meer Region of France.: 'Gebietskörperschaft Frankreichs.' Republic of the Congo: Republik Kongo Riyadh: Riad Romania: 'Rumänien' - 'Romania (slightly lighter blue)': 'Rumänien (etwas helleres Blau)' Rome: Rom Russia: Russland - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Russland (kein Wappen), Slowenien (breiter, kleineres Wappen)' Rwanda: Ruanda 'Réunion': 'La Réunion' Sahrawi Arab Democratic Republic: Demokratische Arabische Republik Sahara @@ -296,14 +257,11 @@ translations: Sea of Japan: Japanisches Meer Sea of Okhotsk: Ochotskisches Meer Semi-autonomous region of Tanzania.: Halbautonomer Teilstaat Tansanias. - 'Senegal (green/yellow/red, green star)': 'Senegal (Grün/Gelb/Rot, grüner Stern)' Serbia: Serbien Seychelles: Seychellen Sicily: Sizilien Singapore: Singapur Slovakia: Slowakei - 'Slovakia (narrower, bigger coat of arms)': 'Slowakei (schmaler, größeres Wappen)' - 'Slovakia (with coat of arms)': 'Slowakei (mit Wappen)' Slovenia: Slowenien Solomon Islands: Salomonen South Africa: 'Südafrika' @@ -324,7 +282,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Größte Inselregion Ozeaniens.' Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Teilregion von Ozeanien bestehend über 2000 tropischen Inseln und Atollen im westlichen Pazifik.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Teilregion von Ozeanien, zu der Vanuatu, die Salomonen, Fidschi und Papua-Neuguinea gehören.' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (Rot/Weiß/Schwarz, grüner Pfeil), Demokratische Arabische Republik Sahara (mit Stern und Halbmond)' Sukhumi: Sochumi Svalbard: Spitzbergen Sweden: Schweden @@ -377,11 +334,64 @@ translations: Zambia: Sambia Zanzibar: Sansibar Zimbabwe: Simbabwe + black/white/green, red arrow: 'Schwarz/Weiß/Grün, roter Pfeil' + blue background, red and white cross: 'blauer Hintergrund, rotes und weißes Kreuz' + blue background, red cross: blauer Hintergrund, rotes Kreuz + blue triangle, red stripes: blaues Dreieck, rote Streifen + brighter red, wider white band: 'helleres Rot, breiteres weißes Band' + coat of arms instead of star: Wappen anstatt Stern + darker blue: dunkleres Blau + darker red, narrower white band: 'dunkleres rot, dünneres weißes Band' + different coat of arms, slightly darker blue: anderes Wappen, etwas dunkleres Blau + different coat of arms, slightly lighter blue: anderes Wappen, etwas helleres Blau + emblem instead of text: Emblem statt Text + green and orange flipped, narrower: 'Grün und Orange vertauscht, schmaler' + green and red flipped, slightly darker green: 'Grün und Rot vertauscht, etwas dunkleres Grün' + green/red/yellow, yellow star: 'Grün/Rot/Gelb, gelber Stern' + green/yellow/red, green star: 'Grün/Gelb/Rot, grüner Stern' + lighter blue: helleres Blau + narrower, bigger coat of arms: 'schmaler, größeres Wappen' + narrower, coat of arms with motto: schmaler, Wappen mit Wahlspruch + narrower, darker red: schmaler, dunkleres rot + narrower, fewer serrated edges, red: enger, weniger Zacken, rot + no coat of arms: kein Wappen + no emblem: kein Emblem + no symbol: kein Symbol + no text: kein Text + orange and green flipped, wider: 'Orange und Grün vertauscht, breiter' + red and green flipped, slightly brighter green: 'Rot und Grün vertauscht, etwas helleres Grün' + red and white flipped, darker red: 'Rot und Weiß vertauscht, dunkleres rot' + red and white flipped, wider: 'Rot und Weiß vertauscht, breiter' + red background, blue and white cross: 'roter Hintergrund, blaues und weißes Kreuz' + red background, blue cross: roter Hintergrund, blaues Kreuz + red stars, two fewer stars: rote Sterne, zwei Sterne weniger + red triangle, blue stripes: rotes Dreieck, blaue Streifen + red/white/black, green arrow: 'Rot/Weiß/Schwarz, grüner Pfeil' + single star below yellow band: einzelner Stern unter gelbem Band + slightly darker blue: etwas dunkleres Blau + slightly lighter blue: etwas helleres Blau + star instead of coat of arms: Stern anstatt Wappen + text instead of emblem: Text anstatt Emblem + two stars in top-left corner: zwei Sterne in der oberen linken Ecke + white and red flipped, brighter red: 'Weiß und Rot vertauscht, helleres Rot' + white and red flipped, narrower: 'Weiß und Rot vertauscht, schmaler' + white background, red and blue cross: 'weißer Hintergrund, rotes und blaues Kreuz' + white stars, two more stars: 'weiße Sterne, zwei weitere Sterne' + wider, brighter red: breiter, helleres Rot + wider, coat of arms with eagle: breiter, Wappen mit Adler + wider, more serrated edges, maroon: breiter, mehr kleinere Zacken, rotbraun + wider, smaller coat of arms: breiter, kleineres Wappen + with coat of arms: mit Wappen + with emblem: mit Emblem + with star and crescent: mit Stern und Halbmond + with text: mit Text 'Åland Islands': 'Åland' contextual: notes.note: akrotiri-and-dhekelia: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' + andorra.fields.field.flag-similarity.message.variables.country_1: + Moldova: Republik Moldau/Moldawien anguilla: Overseas territory of the United Kingdom.: 'Britisches Überseegebiet.' bali: diff --git a/fixtures/ultimate-geography/overlays/languages/es.yaml b/fixtures/ultimate-geography/overlays/languages/es.yaml index 127d458..e53fa5d 100644 --- a/fixtures/ultimate-geography/overlays/languages/es.yaml +++ b/fixtures/ultimate-geography/overlays/languages/es.yaml @@ -27,7 +27,6 @@ translations: American Samoa: Samoa Americana Amman: 'Amán' Amsterdam: 'Ámsterdam' - 'Andorra (narrower, coat of arms with motto)': 'Andorra (más estrecha, escudo con lema)' Andorra la Vella: Andorra la Vieja Anguilla: Anguila Antarctica: 'Antártida' @@ -39,8 +38,6 @@ translations: Astana: 'Astaná' Athens: Atenas Atlantic Ocean: 'Océano Atlántico' - 'Australia (white stars, two more stars)': 'Australia (estrellas blancas, dos estrellas más)' - 'Austria (brighter red, wider white band)': 'Austria (rojo más vivo, banda blanca más ancha)' Autonomous community of Spain.: 'Comunidad autónoma de España.' Autonomous province of South Korea.: 'Provincia autónoma de Corea del Sur.' Autonomous region of Finland.: 'Región autónoma de Finlandia.' @@ -50,7 +47,6 @@ translations: Azerbaijan: 'Azerbaiyán' Baghdad: Bagdad Bahrain: 'Baréin' - 'Bahrain (narrower, fewer serrated edges, red)': 'Baréin (más estrecha, menos dientes de sierra, roja)' Baku: 'Bakú' Balkan Peninsula: 'Península balcánica' Baltic Sea: 'Mar Báltico' @@ -74,7 +70,6 @@ translations: Bishkek: Biskek Bissau: 'Bisáu' Black Sea: Mar Negro - 'Bolivia (coat of arms instead of star)': 'Bolivia (escudo en lugar de la estrella)' Bosnia and Herzegovina: Bosnia y Herzegovina Botswana: Botsuana 'Brasília': Brasilia @@ -86,7 +81,6 @@ translations: Cairo: El Cairo Cambodia: Camboya Cameroon: 'Camerún' - 'Cameroon (green/red/yellow, yellow star)': 'Camerún (verde/rojo/amarillo, estrella amarilla)' Canada: 'Canadá' Canary Islands: Canarias Cape Verde: Cabo Verde @@ -97,12 +91,10 @@ translations: Celtic Sea: 'Mar Céltico' Central African Republic: 'República Centroafricana' Cetinje is an honorary capital.: 'Cetiña es una capital honoraria.' - 'Chad (slightly darker blue)': 'Chad (azul ligeramente más oscuro)' 'Chișinău': 'Chisináu' City of San Marino: San Marino Claimed and controlled: Reclamada y controlada Claimed but not controlled: Reclamada pero no controlada - 'Colombia (no coat of arms)': 'Colombia (sin escudo)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: Colombo es a menudo referida como la capital, pero Sri Jayawardenapura Kotte, una ciudad a las afueras de Colombo, es la capital oficial y legislativa. Comoros: Comoras Conakry: Conakri @@ -115,9 +107,7 @@ translations: Corsica: 'Córcega' Croatia: Croacia Crown dependency of the United Kingdom.: 'Dependencia de la Corona británica.' - 'Cuba (red triangle, blue stripes)': 'Cuba (triángulo rojo, bandas azules)' 'Curaçao': Curazao - 'Curaçao (two stars in top-left corner)': 'Curazao (dos estrellas en la esquina de arriba a la izquierda)' Cyprus: Chipre Czech Republic: 'República Checa' Damascus: Damasco @@ -134,12 +124,8 @@ translations: Dushanbe: 'Dusambé' East China Sea: Mar de la China Oriental East Siberian Sea: Mar de Siberia Oriental - 'Ecuador (with coat of arms)': 'Ecuador (con escudo)' Edinburgh: Edimburgo Egypt: Egipto - 'Egypt (emblem instead of text), Yemen (no text)': 'Egipto (escudo en lugar del texto), Yemen (sin texto)' - 'Egypt (with emblem), Iraq (with text)': 'Egipto (con escudo), Irak (con texto)' - 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (escudo diferente, azul ligeramente más oscuro)' England: Inglaterra English Channel: Canal de la Mancha Equatorial Guinea: Guinea Ecuatorial @@ -159,14 +145,12 @@ translations: French Polynesia: Polinesia Francesa Gabon: 'Gabón' Germany: Alemania - 'Ghana (star instead of coat of arms)': 'Ghana (estrella en lugar del escudo)' Gitega: Guitega Greece: Grecia Greenland: Groenlandia Grenada: Granada Guadeloupe: Guadalupe Guatemala City: Ciudad de Guatemala - 'Guinea (green and red flipped, slightly darker green)': 'Guinea (verde y rojo intercambiados, verde ligeramente más oscuro)' Guinea-Bissau: 'Guinea-Bisáu' Gulf of Alaska: Golfo de Alaska Gulf of California: Golfo de California @@ -183,24 +167,17 @@ translations: Hudson Bay: 'Bahía de Hudson' Hungary: 'Hungría' Iceland: Islandia - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Islandia (fondo azul, cruz roja y blanca), Noruega (fondo rojo, cruz azul y blanca)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Islandia (fondo azul, cruz roja), Islas Feroe (fondo blanco, cruz roja y azul)' Independent state claimed by Georgia.: Estado independiente reclamado por Georgia. Independent state claimed by Moldova.: Estado independiente reclamado por Moldavia. Independent state claimed by Somalia.: Estado independiente reclamado por Somalia. Indian Ocean: 'Océano Índico' - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesia (rojo y blanco intercambiados, rojo más vivo), Mónaco (rojo y blanco intercambiados, más estrecha)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesia (más ancha, rojo más vivo), Polonia (rojo y blanco intercambiados, más ancha)' Iran: 'Irán' Iraq: Irak - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (texto en lugar de escudo), Yemen (sin escudo)' Ireland: Irlanda - 'Ireland (orange and green flipped, wider)': 'Irlanda (naranja y verde intercambiados, más ancha)' Island of Indonesia.: Isla de Indonesia. Isle of Man: Isla de Man Italy: Italia Ivory Coast: Costa de Marfil - 'Ivory Coast (green and orange flipped, narrower)': 'Costa de Marfil (verde y naranja intercambiados, más estrecha)' Jakarta: Yakarta Japan: 'Japón' Jerusalem: 'Jerusalén' @@ -220,7 +197,6 @@ translations: Laayoune: 'El Aaiún' Labrador Sea: Mar de Labrador Latvia: Letonia - 'Latvia (darker red, narrower white band)': 'Letonia (rojo más oscuro, banda blanca más estrecha)' Lebanon: 'Líbano' Lesotho: Lesoto Libya: Libia @@ -230,14 +206,12 @@ translations: Ljubljana: Liubliana London: Londres Luxembourg: Luxemburgo - 'Luxembourg (lighter blue)': 'Luxemburgo (azul más claro)' Luxembourg City: Luxemburgo Macau: Macao Malawi: Malaui Malaysia: Malasia Maldives: Maldivas Mali: 'Malí' - 'Mali (red and green flipped, slightly brighter green)': 'Malí (rojo y verde intercambiados, verde ligeramente más vivo)' Marshall Islands: Islas Marshall Martinique: Martinica Mauritius: Mauricio @@ -246,25 +220,19 @@ translations: Mexico City: 'Ciudad de México' Mogadishu: Mogadiscio Moldova: Moldavia - 'Moldova (wider, coat of arms with eagle)': 'Moldavia (más ancha, escudo con águila)' Monaco: 'Mónaco' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Mónaco (más estrecha, rojo más oscuro), Polonia (rojo y blanco intercambiados, rojo más oscuro)' Morocco: Marruecos Moscow: 'Moscú' Muscat: Mascate Myanmar: Birmania 'N''Djamena': Yamena Nassau: 'Nasáu' - 'Nauru (single star below yellow band)': 'Nauru (una sola estrella debajo de la banda amarilla)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru no tiene capital oficial; el distrito de Yaren es la capital de facto.' Naypyidaw: 'Naipyidó' Netherlands: 'Países Bajos' - 'Netherlands (darker blue)': 'Países Bajos (azul más oscuro)' New Caledonia: Nueva Caledonia New Delhi: Nueva Delhi New Zealand: Nueva Zelanda - 'New Zealand (red stars, two fewer stars)': 'Nueva Zelanda (estrellas rojas, dos estrellas menos)' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (escudo diferente, azul ligeramente más claro)' Niger: 'Níger' North America: 'América del Norte' North Korea: Corea del Norte @@ -275,7 +243,6 @@ translations: Northern Ireland: Irlanda del Norte Northern Mariana Islands: Islas Marianas del Norte Norway: Noruega - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Noruega (fondo rojo, cruz azul), Islas Feroe (fondo blanco, cruz roja y azul)' Norwegian Sea: Mar de Noruega Not a sovereign country: 'No es un país soberano' Nouakchott: Nuakchot @@ -296,8 +263,6 @@ translations: Pakistan: 'Pakistán' Palau: Palaos Palestine: Palestina - 'Palestine (black/white/green, red arrow)': 'Palestina (negro/blanco/verde, triángulo rojo)' - 'Palestine (no symbol)': 'Palestina (sin símbolo)' Panama: 'Panamá' Panama City: 'Ciudad de Panamá' Papua New Guinea: 'Papúa Nueva Guinea' @@ -318,20 +283,16 @@ translations: Porto-Novo: Porto Novo Prague: Praga Pretoria, Cape Town, Bloemfontein: Pretoria, Ciudad del Cabo, Bloemfontein - 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (triángulo azul, bandas rojas)' Pyongyang: Pionyang Qatar: Catar - 'Qatar (wider, more serrated edges, maroon)': 'Catar (más ancha, dientes más serrados, granate)' Red Sea: Mar Rojo Region of France.: 'Región de Francia.' Republic of the Congo: 'República del Congo' 'Reykjavík': Reikiavik Riyadh: Riad Romania: Rumania - 'Romania (slightly lighter blue)': 'Rumania (azul ligeramente más claro)' Rome: Roma Russia: Rusia - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rusia (sin escudo), Eslovenia (más ancha, escudo más pequeño)' Rwanda: Ruanda 'Réunion': 'Reunión' Sahrawi Arab Democratic Republic: 'República Árabe Saharaui Democrática' @@ -349,7 +310,6 @@ translations: Sea of Japan: 'Mar del Japón' Sea of Okhotsk: Mar de Ojotsk Semi-autonomous region of Tanzania.: 'Región semiautónoma de Tanzania.' - 'Senegal (green/yellow/red, green star)': 'Senegal (verde/amarillo/rojo, estrella verde)' Seoul: 'Seúl' Sicily: Sicilia Sierra Leone: Sierra Leona @@ -357,8 +317,6 @@ translations: Sint Maarten: 'San Martín (Países Bajos)' Skopje: Skopie Slovakia: Eslovaquia - 'Slovakia (narrower, bigger coat of arms)': 'Eslovaquia (más estrecha, escudo más grande)' - 'Slovakia (with coat of arms)': 'Eslovaquia (con escudo)' Slovenia: Eslovenia Sofia: 'Sofía' Solomon Islands: 'Islas Salomón' @@ -385,7 +343,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Subregión de Oceanía compuesta por miles de islas pequeñas en la parte oeste del océano Pacífico.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Subregión de Oceanía que incluye Vanuatu, las Islas Salomón, Fiyi y Papúa Nueva Guinea.' Sudan: 'Sudán' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudán (rojo/blanco/negro, triángulo verde), República Árabe Saharaui Democrática (con creciente y estrella)' Sukhumi: Sujumi Suriname: Surinam Sweden: Suecia @@ -451,6 +408,57 @@ translations: Yerevan: 'Ereván' Zanzibar: 'Zanzíbar' Zimbabwe: Zimbabue + black/white/green, red arrow: 'negro/blanco/verde, triángulo rojo' + blue background, red and white cross: fondo azul, cruz roja y blanca + blue background, red cross: fondo azul, cruz roja + blue triangle, red stripes: 'triángulo azul, bandas rojas' + brighter red, wider white band: 'rojo más vivo, banda blanca más ancha' + coat of arms instead of star: escudo en lugar de la estrella + darker blue: 'azul más oscuro' + darker red, narrower white band: 'rojo más oscuro, banda blanca más estrecha' + different coat of arms, slightly darker blue: 'escudo diferente, azul ligeramente más oscuro' + different coat of arms, slightly lighter blue: 'escudo diferente, azul ligeramente más claro' + emblem instead of text: escudo en lugar del texto + green and orange flipped, narrower: 'verde y naranja intercambiados, más estrecha' + green and red flipped, slightly darker green: 'verde y rojo intercambiados, verde ligeramente más oscuro' + green/red/yellow, yellow star: verde/rojo/amarillo, estrella amarilla + green/yellow/red, green star: verde/amarillo/rojo, estrella verde + lighter blue: 'azul más claro' + narrower, bigger coat of arms: 'más estrecha, escudo más grande' + narrower, coat of arms with motto: 'más estrecha, escudo con lema' + narrower, darker red: 'más estrecha, rojo más oscuro' + narrower, fewer serrated edges, red: 'más estrecha, menos dientes de sierra, roja' + no coat of arms: sin escudo + no emblem: sin escudo + no symbol: 'sin símbolo' + no text: sin texto + orange and green flipped, wider: 'naranja y verde intercambiados, más ancha' + red and green flipped, slightly brighter green: 'rojo y verde intercambiados, verde ligeramente más vivo' + red and white flipped, darker red: 'rojo y blanco intercambiados, rojo más oscuro' + red and white flipped, wider: 'rojo y blanco intercambiados, más ancha' + red background, blue and white cross: fondo rojo, cruz azul y blanca + red background, blue cross: fondo rojo, cruz azul + red stars, two fewer stars: estrellas rojas, dos estrellas menos + red triangle, blue stripes: 'triángulo rojo, bandas azules' + red/white/black, green arrow: 'rojo/blanco/negro, triángulo verde' + single star below yellow band: una sola estrella debajo de la banda amarilla + slightly darker blue: 'azul ligeramente más oscuro' + slightly lighter blue: 'azul ligeramente más claro' + star instead of coat of arms: estrella en lugar del escudo + text instead of emblem: texto en lugar de escudo + two stars in top-left corner: dos estrellas en la esquina de arriba a la izquierda + white and red flipped, brighter red: 'rojo y blanco intercambiados, rojo más vivo' + white and red flipped, narrower: 'rojo y blanco intercambiados, más estrecha' + white background, red and blue cross: fondo blanco, cruz roja y azul + white stars, two more stars: 'estrellas blancas, dos estrellas más' + wider, brighter red: 'más ancha, rojo más vivo' + wider, coat of arms with eagle: 'más ancha, escudo con águila' + wider, more serrated edges, maroon: 'más ancha, dientes más serrados, granate' + wider, smaller coat of arms: 'más ancha, escudo más pequeño' + with coat of arms: con escudo + with emblem: con escudo + with star and crescent: con creciente y estrella + with text: con texto 'Åland Islands': 'Åland' no_change: - Alaska diff --git a/fixtures/ultimate-geography/overlays/languages/fr.yaml b/fixtures/ultimate-geography/overlays/languages/fr.yaml index 7c7f7d0..c509c1b 100644 --- a/fixtures/ultimate-geography/overlays/languages/fr.yaml +++ b/fixtures/ultimate-geography/overlays/languages/fr.yaml @@ -24,7 +24,6 @@ translations: 'Also spelled as Sana''a.': '' American Samoa: 'Samoa américaines' Andorra: Andorre - 'Andorra (narrower, coat of arms with motto)': 'Andorre (plus étroit, blason avec devise)' Andorra la Vella: Andorre-la-Vieille Antarctica: Antarctique Antigua and Barbuda: Antigua-et-Barbuda @@ -39,9 +38,7 @@ translations: Athens: 'Athènes' Atlantic Ocean: 'Océan Atlantique' Australia: Australie - 'Australia (white stars, two more stars)': 'Australie (étoiles blanches, deux étoiles de plus)' Austria: Autriche - 'Austria (brighter red, wider white band)': 'Autriche (rouge plus clair, bande blanche plus large)' Autonomous community of Spain.: 'Communauté autonome d''Espagne.' Autonomous province of South Korea.: 'Province autonome de Corée du Sud.' Autonomous region of Finland.: Province autonome de Finlande. @@ -52,7 +49,6 @@ translations: Azores: 'Açores' Baghdad: Bagdad Bahrain: 'Bahreïn' - 'Bahrain (narrower, fewer serrated edges, red)': 'Bahreïn (séparation plus étroite et moins dentelée, rouge)' Baku: Bakou Balkan Peninsula: Balkans Baltic Sea: Mer Baltique @@ -74,7 +70,6 @@ translations: Black Sea: Mer Noire 'Bogotá': Bogota Bolivia: Bolivie - 'Bolivia (coat of arms instead of star)': 'Bolivie (blason à la place de l''étoile)' Bosnia and Herzegovina: 'Bosnie-Herzégovine' 'Brasília': Brasilia Brazil: 'Brésil' @@ -85,7 +80,6 @@ translations: Cairo: Le Caire Cambodia: Cambodge Cameroon: Cameroun - 'Cameroon (green/red/yellow, yellow star)': 'Cameroun (vert/rouge/jaune, étoile jaune)' Canary Islands: 'Îles Canaries' Cape Verde: Cap-Vert Caribbean Sea: 'Mer des Caraïbes' @@ -96,7 +90,6 @@ translations: Central African Republic: 'République centrafricaine' Cetinje is an honorary capital.: Cetinje est une capitale honoraire. Chad: Tchad - 'Chad (slightly darker blue)': 'Tchad (bleu légèrement plus foncé)' Charlotte Amalie: 'Charlotte-Amélie' Chile: Chili China: Chine @@ -104,7 +97,6 @@ translations: Claimed and controlled: 'Revendiquée et contrôlée' Claimed but not controlled: 'Revendiquée mais non contrôlée' Colombia: Colombie - 'Colombia (no coat of arms)': 'Colombie (pas de blason)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'On fait souvent référence à Colombo comme capitale, mais Sri Jayawardenapura Kotte, une ville de la banlieue de Colombo, est la capitale législative officielle.' Comoros: Comores Constituent country in the Kingdom of Denmark.: Pays constitutif du royaume du Danemark. @@ -116,8 +108,6 @@ translations: Corsica: Corse Croatia: Croatie Crown dependency of the United Kingdom.: 'Dépendance de la Couronne britannique.' - 'Cuba (red triangle, blue stripes)': 'Cuba (triangle rouge, bandes bleues)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (deux étoiles en haut à gauche)' Cyprus: Chypre Czech Republic: 'Tchéquie' Damascus: Damas @@ -134,13 +124,9 @@ translations: East China Sea: Mer de Chine orientale East Siberian Sea: 'Mer de Sibérie orientale' Ecuador: 'Équateur' - 'Ecuador (with coat of arms)': 'Équateur (avec blason)' Edinburgh: 'Édimbourg' Egypt: 'Égypte' - 'Egypt (emblem instead of text), Yemen (no text)': 'Égypte (emblème à la place du texte), Yémen (pas de texte)' - 'Egypt (with emblem), Iraq (with text)': 'Égypte (avec un emblème), Irak (avec du texte)' El Salvador: Salvador - 'El Salvador (different coat of arms, slightly darker blue)': 'Salvador (blason différent, bleu légèrement plus foncé)' England: Angleterre English Channel: Manche Equatorial Guinea: 'Guinée équatoriale' @@ -159,14 +145,12 @@ translations: French Polynesia: 'Polynésie française' Georgia: 'Géorgie' Germany: Allemagne - 'Ghana (star instead of coat of arms)': 'Ghana (étoile à la place du blason)' Greece: 'Grèce' Greenland: Groenland Grenada: Grenade Guatemala City: Guatemala Guernsey: Guernesey Guinea: 'Guinée' - 'Guinea (green and red flipped, slightly darker green)': 'Guinée (vert et rouge inversés, vert légèrement plus foncé)' Guinea-Bissau: 'Guinée-Bissau' Gulf of Alaska: 'Golfe d''Alaska' Gulf of California: Golfe de Californie @@ -182,25 +166,18 @@ translations: Hudson Bay: 'Baie d''Hudson' Hungary: Hongrie Iceland: Islande - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Islande (fond bleu, croix rouge et blanche), Norvège (fond rouge, croix bleue et blanche)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Islande (fond bleu, croix rouge), Îles Féroé (fond blanc, croix rouge et bleue)' Independent state claimed by Georgia.: 'État indépendant revendiqué par la Géorgie.' Independent state claimed by Moldova.: 'État indépendant revendiqué par la Moldavie.' Independent state claimed by Somalia.: 'État indépendant revendiqué par la Somalie.' India: Inde Indian Ocean: 'Océan Indien' Indonesia: 'Indonésie' - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonésie (blanc et rouge inversés, rouge plus clair), Monaco (blanc et rouge inversés, plus étroit)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonésie (plus large, rouge plus clair), Pologne (rouge et blanc inversés, plus large)' Iraq: Irak - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (texte à la place de l''emblème), Yémen (pas d''emblème)' Ireland: Irlande - 'Ireland (orange and green flipped, wider)': 'Irlande (orange et vert inversés, plus large)' Isle of Man: 'Île de Man' Israel: 'Israël' Italy: Italie Ivory Coast: 'Côte d''Ivoire' - 'Ivory Coast (green and orange flipped, narrower)': 'Côte d''Ivoire (vert et orange inversés, plus étroit)' Jamaica: 'Jamaïque' Japan: Japon Jerusalem: 'Jérusalem' @@ -218,18 +195,15 @@ translations: Laayoune: 'Laâyoune' Labrador Sea: Mer du Labrador Latvia: Lettonie - 'Latvia (darker red, narrower white band)': 'Lettonie (rouge plus foncé, bande blanche plus étroite)' Lebanon: Liban Libya: Libye Lisbon: Lisbonne Lithuania: Lituanie London: Londres - 'Luxembourg (lighter blue)': 'Luxembourg (bleu plus clair)' Luxembourg City: Luxembourg Macau: Macao Madeira: 'Madère' Malaysia: Malaisie - 'Mali (red and green flipped, slightly brighter green)': 'Mali (rouge et vert inversés, vert légèrement plus clair)' Malta: Malte Manila: Manille Marshall Islands: 'Îles Marshall' @@ -242,8 +216,6 @@ translations: Micronesia: 'Micronésie' Mogadishu: Mogadiscio Moldova: Moldavie - 'Moldova (wider, coat of arms with eagle)': 'Moldavie (plus large, blason avec un aigle)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (plus étroit, rouge plus foncé), Pologne (rouge et blanc inversés, rouge plus foncé)' Mongolia: Mongolie Montenegro: 'Monténégro' Morocco: Maroc @@ -252,15 +224,11 @@ translations: Myanmar: Birmanie 'N''Djamena': 'N''Djaména' Namibia: Namibie - 'Nauru (single star below yellow band)': 'Nauru (une seule étoile sous la bande jaune)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru n''a pas de capitale officielle ; le district de Yaren est la capitale de fait.' Nepal: 'Népal' Netherlands: Pays-Bas - 'Netherlands (darker blue)': 'Pays-Bas (bleu plus foncé)' New Caledonia: 'Nouvelle-Calédonie' New Zealand: 'Nouvelle-Zélande' - 'New Zealand (red stars, two fewer stars)': 'Nouvelle-Zélande (étoiles rouges, deux étoiles de moins)' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (blason différent, bleu légèrement plus clair)' Nicosia: Nicosie North America: 'Amérique du Nord' North Korea: 'Corée du Nord' @@ -271,7 +239,6 @@ translations: Northern Ireland: Irlande du Nord Northern Mariana Islands: 'Îles Mariannes du Nord' Norway: 'Norvège' - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norvège (fond rouge, croix bleue), Îles Féroé (fond blanc, croix rouge et bleue)' Norwegian Sea: 'Mer de Norvège' Not a sovereign country: Pas une nation souveraine 'Oblast (administrative region) of the Russian Federation.': 'Oblast (région administrative) de la fédération de Russie.' @@ -284,8 +251,6 @@ translations: Overseas territory of the United Kingdom.: 'Territoire britannique d''outre-mer.' Pacific Ocean: 'Océan Pacifique' Palau: Palaos - 'Palestine (black/white/green, red arrow)': 'Palestine (noir/blanc/vert, flèche rouge)' - 'Palestine (no symbol)': 'Palestine (pas de symbole)' Panama City: Panama Papua New Guinea: 'Papouasie-Nouvelle-Guinée' Partially recognised state claimed by China.: 'État partiellement reconnu revendiqué par la Chine.' @@ -301,17 +266,13 @@ translations: Port of Spain: 'Port-d''Espagne' Pretoria, Cape Town, Bloemfontein: Pretoria, Le Cap, Bloemfontein Puerto Rico: Porto Rico - 'Puerto Rico (blue triangle, red stripes)': 'Porto Rico (triangle bleu, bandes rouges)' - 'Qatar (wider, more serrated edges, maroon)': 'Qatar (séparation plus large et dentelée, pourpre/marron)' Red Sea: Mer Rouge Region of France.: 'Région française.' Republic of the Congo: 'République du Congo' 'Reykjavík': Reykjavik Riyadh: Riyad Romania: Roumanie - 'Romania (slightly lighter blue)': 'Roumanie (bleu légèrement plus clair)' Russia: Russie - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Russie (pas de blason), Slovénie (plus large, blason plus petit)' 'Réunion': 'La Réunion' Sahrawi Arab Democratic Republic: 'République arabe sahraouie démocratique' Saint Kitts and Nevis: 'Saint-Christophe-et-Niévès' @@ -329,15 +290,12 @@ translations: Sea of Okhotsk: 'Mer d''Okhotsk' Semi-autonomous region of Tanzania.: 'Région autonome de Tanzanie.' Senegal: 'Sénégal' - 'Senegal (green/yellow/red, green star)': 'Sénégal (vert/jaune/rouge, étoile verte)' Seoul: 'Séoul' Serbia: Serbie Sicily: Sicile Singapore: Singapour Sint Maarten: 'Saint-Martin (royaume des Pays-Bas)' Slovakia: Slovaquie - 'Slovakia (narrower, bigger coat of arms)': 'Slovaquie (plus étroit, blason plus grand)' - 'Slovakia (with coat of arms)': 'Slovaquie (avec blason)' Slovenia: 'Slovénie' Solomon Islands: 'Îles Salomon' Somalia: Somalie @@ -362,7 +320,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Région d''Océanie comprenant des milliers de petites îles dans l''ouest de l''océan Pacifique.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Région d''Océanie qui inclut le Vanuatu, les Îles Salomon, les Fidji et la Papouasie-Nouvelle-Guinée.' Sudan: Soudan - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Soudan (rouge/blanc/noir, flèche verte), République arabe sahraouie démocratique (avec étoile et croissant)' Sukhumi: Soukhoumi Sweden: 'Suède' Switzerland: Suisse @@ -418,6 +375,57 @@ translations: Yemen: 'Yémen' Yerevan: Erevan Zambia: Zambie + black/white/green, red arrow: 'noir/blanc/vert, flèche rouge' + blue background, red and white cross: fond bleu, croix rouge et blanche + blue background, red cross: fond bleu, croix rouge + blue triangle, red stripes: triangle bleu, bandes rouges + brighter red, wider white band: rouge plus clair, bande blanche plus large + coat of arms instead of star: 'blason à la place de l''étoile' + darker blue: 'bleu plus foncé' + darker red, narrower white band: 'rouge plus foncé, bande blanche plus étroite' + different coat of arms, slightly darker blue: 'blason différent, bleu légèrement plus foncé' + different coat of arms, slightly lighter blue: 'blason différent, bleu légèrement plus clair' + emblem instead of text: 'emblème à la place du texte' + green and orange flipped, narrower: 'vert et orange inversés, plus étroit' + green and red flipped, slightly darker green: 'vert et rouge inversés, vert légèrement plus foncé' + green/red/yellow, yellow star: 'vert/rouge/jaune, étoile jaune' + green/yellow/red, green star: 'vert/jaune/rouge, étoile verte' + lighter blue: bleu plus clair + narrower, bigger coat of arms: 'plus étroit, blason plus grand' + narrower, coat of arms with motto: 'plus étroit, blason avec devise' + narrower, darker red: 'plus étroit, rouge plus foncé' + narrower, fewer serrated edges, red: 'séparation plus étroite et moins dentelée, rouge' + no coat of arms: pas de blason + no emblem: 'pas d''emblème' + no symbol: pas de symbole + no text: pas de texte + orange and green flipped, wider: 'orange et vert inversés, plus large' + red and green flipped, slightly brighter green: 'rouge et vert inversés, vert légèrement plus clair' + red and white flipped, darker red: 'rouge et blanc inversés, rouge plus foncé' + red and white flipped, wider: 'rouge et blanc inversés, plus large' + red background, blue and white cross: fond rouge, croix bleue et blanche + red background, blue cross: fond rouge, croix bleue + red stars, two fewer stars: 'étoiles rouges, deux étoiles de moins' + red triangle, blue stripes: triangle rouge, bandes bleues + red/white/black, green arrow: 'rouge/blanc/noir, flèche verte' + single star below yellow band: 'une seule étoile sous la bande jaune' + slightly darker blue: 'bleu légèrement plus foncé' + slightly lighter blue: 'bleu légèrement plus clair' + star instead of coat of arms: 'étoile à la place du blason' + text instead of emblem: 'texte à la place de l''emblème' + two stars in top-left corner: 'deux étoiles en haut à gauche' + white and red flipped, brighter red: 'blanc et rouge inversés, rouge plus clair' + white and red flipped, narrower: 'blanc et rouge inversés, plus étroit' + white background, red and blue cross: fond blanc, croix rouge et bleue + white stars, two more stars: 'étoiles blanches, deux étoiles de plus' + wider, brighter red: plus large, rouge plus clair + wider, coat of arms with eagle: plus large, blason avec un aigle + wider, more serrated edges, maroon: 'séparation plus large et dentelée, pourpre/marron' + wider, smaller coat of arms: plus large, blason plus petit + with coat of arms: avec blason + with emblem: 'avec un emblème' + with star and crescent: 'avec étoile et croissant' + with text: avec du texte 'Åland Islands': 'Åland' contextual: notes.note: diff --git a/fixtures/ultimate-geography/overlays/languages/it.yaml b/fixtures/ultimate-geography/overlays/languages/it.yaml index eb10cc6..14b3290 100644 --- a/fixtures/ultimate-geography/overlays/languages/it.yaml +++ b/fixtures/ultimate-geography/overlays/languages/it.yaml @@ -19,7 +19,6 @@ translations: Also known as the Sea of Cortez.: Conosciuto anche come Mare di Cortez. 'Also spelled as Sana''a.': '' American Samoa: Samoa Americane - 'Andorra (narrower, coat of arms with motto)': 'Andorra (più stretta, stemma con motto)' Antarctica: Antartide Antigua and Barbuda: Antigua e Barbuda Arabian Sea: Mar Arabico @@ -28,8 +27,6 @@ translations: Ashgabat: 'Aşgabat' Athens: Atene Atlantic Ocean: Oceano Atlantico - 'Australia (white stars, two more stars)': 'Australia (stelle bianche, due stelle in più)' - 'Austria (brighter red, wider white band)': 'Austria (rosso più chiaro, banda bianca più larga)' Autonomous community of Spain.: 'Comunità autonoma della Spagna.' Autonomous province of South Korea.: Provincia autonoma della Corea del Sud. Autonomous region of Finland.: Regione autonoma della Finlandia. @@ -39,7 +36,6 @@ translations: Azerbaijan: Azerbaigian Azores: Azzorre Bahrain: Bahrein - 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrein (più stretta, meno bordi seghettati, rossa)' Balkan Peninsula: Penisola balcanica Baltic Sea: Mar Baltico Banda Sea: Mar di Banda @@ -56,7 +52,6 @@ translations: Bishkek: 'Biškek' Black Sea: Mar Nero 'Bogotá': 'Bogotà' - 'Bolivia (coat of arms instead of star)': 'Bolivia (stemma al posto della stella)' Bosnia and Herzegovina: Bosnia ed Erzegovina 'Brasília': Brasilia Brazil: Brasile @@ -66,7 +61,6 @@ translations: Cairo: Il Cairo Cambodia: Cambogia Cameroon: Camerun - 'Cameroon (green/red/yellow, yellow star)': 'Camerun (verde/rosso/giallo, stella gialla)' Canary Islands: Isole Canarie Cape Verde: Capo Verde Caribbean Sea: Mare Caraibico @@ -77,13 +71,11 @@ translations: Central African Republic: Repubblica Centrafricana Cetinje is an honorary capital.: 'Cettigne è una capitale onoraria.' Chad: Ciad - 'Chad (slightly darker blue)': 'Ciad (blu leggermente più scuro)' Chile: Cile China: Cina City of San Marino: 'Città di San Marino' Claimed and controlled: Rivendicata e controllata Claimed but not controlled: Rivendicata ma non controllata - 'Colombia (no coat of arms)': 'Colombia (senza stemma)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Colombo è spesso considerata la capitale ma Sri Jayawardenepura Kotte, un sobborgo di Colombo, è la capitale ufficiale e legislativa.' Comoros: Comore Constituent country of the Kingdom of the Netherlands.: Nazione costitutiva del Regno dei Paesi Bassi. @@ -93,8 +85,6 @@ translations: Coral Sea: Mar dei Coralli Croatia: Croazia Crown dependency of the United Kingdom.: Dipendenza della Corona britannica. - 'Cuba (red triangle, blue stripes)': 'Cuba (triangolo rosso, strisce blu)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (due stelle nell''angolo in alto a sinistra)' Cyprus: Cipro Czech Republic: Repubblica Ceca Damascus: Damasco @@ -111,12 +101,8 @@ translations: Dushanbe: 'Dušanbe' East China Sea: Mar Cinese Orientale East Siberian Sea: Mar della Siberia Orientale - 'Ecuador (with coat of arms)': 'Ecuador (con lo stemma)' Edinburgh: Edimburgo Egypt: Egitto - 'Egypt (emblem instead of text), Yemen (no text)': 'Egitto (emblema al posto del testo), Yemen (senza testo)' - 'Egypt (with emblem), Iraq (with text)': 'Egitto (con l''emblema), Iraq (con il testo)' - 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (stemma diverso, blu leggermente più scuro)' England: Inghilterra English Channel: La Manica Equatorial Guinea: Guinea Equatoriale @@ -135,13 +121,11 @@ translations: French Guiana: Guyana francese French Polynesia: Polinesia francese Germany: Germania - 'Ghana (star instead of coat of arms)': 'Ghana (stella al posto dello stemma)' Gibraltar: Gibilterra Greece: Grecia Greenland: Groenlandia Guadeloupe: Guadalupa Guatemala City: 'Città del Guatemala' - 'Guinea (green and red flipped, slightly darker green)': 'Guinea (verde e rosso invertiti, verde leggermente più scuro)' Gulf of Alaska: 'Golfo dell''Alaska' Gulf of California: Golfo di California Gulf of Carpentaria: Golfo di Carpentaria @@ -154,23 +138,16 @@ translations: Hudson Bay: Baia di Hudson Hungary: Ungheria Iceland: Islanda - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Islanda (sfondo blu, croce rossa e bianca), Norvegia (sfondo rosso, croce blu e bianca)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Islanda (sfondo blu, croce rossa), Fær Øer (sfondo bianco, croce rossa e blu)' Independent state claimed by Georgia.: Stato indipendente rivendicato dalla Georgia. Independent state claimed by Moldova.: Stato indipendente rivendicato dalla Moldavia. Independent state claimed by Somalia.: Stato indipendente rivendicato dalla Somalia. Indian Ocean: Oceano Indiano - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesia (bianco e rosso invertiti, rosso più chiaro), Monaco (bianco e rosso invertiti, più stretta)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesia (più larga, rosso più chiaro), Polonia (rosso e bianco invertiti, più larga)' - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Iraq (testo al posto dell''emblema), Yemen (senza emblema)' Ireland: Irlanda - 'Ireland (orange and green flipped, wider)': 'Irlanda (arancione e verde invertiti, più larga)' Island of Indonesia.: 'Isola dell''Indonesia.' Isle of Man: Isola di Man Israel: Israele Italy: Italia Ivory Coast: 'Costa d''Avorio' - 'Ivory Coast (green and orange flipped, narrower)': 'Costa d''Avorio (verde e arancione invertiti, più stretta)' Jakarta: Giacarta Jamaica: Giamaica Japan: Giappone @@ -190,7 +167,6 @@ translations: Laayoune: 'El Aaiún' Labrador Sea: Mare del Labrador Latvia: Lettonia - 'Latvia (darker red, narrower white band)': 'Lettonia (rosso più scuro, banda bianca più stretta)' Lebanon: Libano Libya: Libia Lisbon: Lisbona @@ -198,12 +174,10 @@ translations: Ljubljana: Lubiana London: Londra Luxembourg: Lussemburgo - 'Luxembourg (lighter blue)': 'Lussemburgo (blu più chiaro)' Luxembourg City: Lussemburgo Macau: Macao Madeira: Madera Maldives: Maldive - 'Mali (red and green flipped, slightly brighter green)': 'Mali (rosso e verde invertiti, verde leggermente più chiaro)' Marshall Islands: Isole Marshall Martinique: Martinica Mediterranean Sea: Mar Mediterraneo @@ -211,22 +185,16 @@ translations: Mexico City: 'Città del Messico' Mogadishu: Mogadiscio Moldova: Moldavia - 'Moldova (wider, coat of arms with eagle)': 'Moldavia (più larga, stemma con un''aquila)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (più stretta, rosso più scuro), Polonia (rosso e bianco invertiti, rosso più scuro)' Morocco: Marocco Moscow: Mosca Mozambique: Mozambico Muscat: Mascate Myanmar: Birmania - 'Nauru (single star below yellow band)': 'Nauru (una sola stella sotto la banda gialla)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru non ha una capitale ufficiale; il Distretto di Yaren è la capitale de facto.' Netherlands: Paesi Bassi - 'Netherlands (darker blue)': 'Paesi Bassi (blu più scuro)' New Caledonia: Nuova Caledonia New Delhi: Nuova Delhi New Zealand: Nuova Zelanda - 'New Zealand (red stars, two fewer stars)': 'Nuova Zelanda (stelle rosse, due stelle in meno)' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (stemma diverso, blu leggermente più chiaro)' North America: America del Nord North Korea: Corea del Nord North Macedonia: Macedonia del Nord @@ -236,7 +204,6 @@ translations: Northern Ireland: Irlanda del Nord Northern Mariana Islands: Isole Marianne Settentrionali Norway: Norvegia - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norvegia (sfondo rosso, croce blu), Fær Øer (sfondo bianco, croce rossa e blu)' Norwegian Sea: Mare di Norvegia Not a sovereign country: 'Non è uno Stato sovrano' 'Nouméa': Numea @@ -250,8 +217,6 @@ translations: Overseas territory of the United Kingdom.: 'Territorio d''oltremare del Regno Unito.' Pacific Ocean: Oceano Pacifico Palestine: Palestina - 'Palestine (black/white/green, red arrow)': 'Palestina (nero/bianco/verde, triangolo rosso)' - 'Palestine (no symbol)': 'Palestina (nessun simbolo)' Panama City: Panama Papua New Guinea: Papua Nuova Guinea Paris: Parigi @@ -268,15 +233,11 @@ translations: Prague: Praga Pretoria, Cape Town, Bloemfontein: 'Pretoria, Città del Capo, Bloemfontein' Puerto Rico: Porto Rico - 'Puerto Rico (blue triangle, red stripes)': 'Porto Rico (triangolo blu, strisce rosse)' - 'Qatar (wider, more serrated edges, maroon)': 'Qatar (più larga, più bordi seghettati, rosso granata)' Red Sea: Mar Rosso Region of France.: Regione francese. Republic of the Congo: Repubblica del Congo Riyadh: Riad - 'Romania (slightly lighter blue)': 'Romania (blu leggermente più chiaro)' Rome: Roma - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Russia (senza stemma), Slovenia (più larga, stemma più piccolo)' Rwanda: Ruanda 'Réunion': La Riunione Sahrawi Arab Democratic Republic: Repubblica Democratica Araba dei Sahrawi @@ -292,12 +253,9 @@ translations: Sea of Japan: Mar del Giappone Sea of Okhotsk: Mare di Ochotsk Semi-autonomous region of Tanzania.: Regione semi-autonoma della Tanzania. - 'Senegal (green/yellow/red, green star)': 'Senegal (verde/giallo/rosso, stella verde)' Seoul: Seul Sicily: Sicilia Slovakia: Slovacchia - 'Slovakia (narrower, bigger coat of arms)': 'Slovacchia (più stretta, stemma più grande)' - 'Slovakia (with coat of arms)': 'Slovacchia (con lo stemma)' Solomon Islands: Isole Salomone South Africa: Sudafrica 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'Il Sudafrica non ha una capitale ufficiale: i poteri dello Stato sono suddivisi fra tre città: Pretoria (esecutivo), Città del Capo (legislativo) e Bloemfontein (giudiziario).' @@ -320,7 +278,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Macroregione dell''Oceania che comprende migliaia di piccole isole nell''Oceano Pacifico centrale e meridionale.' Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Macroregione dell''Oceania che comprende migliaia di piccole isole nell''Oceano Pacifico occidentale.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Macroregione dell''Oceania che comprende Vanuatu, le Isole Salomone, le Figi e la Papua Nuova Guinea.' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (rosso/bianco/nero, triangolo verde), Repubblica Democratica Araba dei Sahrawi (con stella e mezzaluna)' Sweden: Svezia Switzerland: Svizzera 'Switzerland has no official capital; Bern is the de facto capital.': 'La Svizzera non ha una capitale ufficiale; Berna è la capitale de facto.' @@ -364,6 +321,57 @@ translations: Yellow Sea: Mar Giallo Yerevan: Erevan Zagreb: Zagabria + black/white/green, red arrow: nero/bianco/verde, triangolo rosso + blue background, red and white cross: sfondo blu, croce rossa e bianca + blue background, red cross: sfondo blu, croce rossa + blue triangle, red stripes: triangolo blu, strisce rosse + brighter red, wider white band: 'rosso più chiaro, banda bianca più larga' + coat of arms instead of star: stemma al posto della stella + darker blue: 'blu più scuro' + darker red, narrower white band: 'rosso più scuro, banda bianca più stretta' + different coat of arms, slightly darker blue: 'stemma diverso, blu leggermente più scuro' + different coat of arms, slightly lighter blue: 'stemma diverso, blu leggermente più chiaro' + emblem instead of text: emblema al posto del testo + green and orange flipped, narrower: 'verde e arancione invertiti, più stretta' + green and red flipped, slightly darker green: 'verde e rosso invertiti, verde leggermente più scuro' + green/red/yellow, yellow star: verde/rosso/giallo, stella gialla + green/yellow/red, green star: verde/giallo/rosso, stella verde + lighter blue: 'blu più chiaro' + narrower, bigger coat of arms: 'più stretta, stemma più grande' + narrower, coat of arms with motto: 'più stretta, stemma con motto' + narrower, darker red: 'più stretta, rosso più scuro' + narrower, fewer serrated edges, red: 'più stretta, meno bordi seghettati, rossa' + no coat of arms: senza stemma + no emblem: senza emblema + no symbol: nessun simbolo + no text: senza testo + orange and green flipped, wider: 'arancione e verde invertiti, più larga' + red and green flipped, slightly brighter green: 'rosso e verde invertiti, verde leggermente più chiaro' + red and white flipped, darker red: 'rosso e bianco invertiti, rosso più scuro' + red and white flipped, wider: 'rosso e bianco invertiti, più larga' + red background, blue and white cross: sfondo rosso, croce blu e bianca + red background, blue cross: sfondo rosso, croce blu + red stars, two fewer stars: stelle rosse, due stelle in meno + red triangle, blue stripes: triangolo rosso, strisce blu + red/white/black, green arrow: rosso/bianco/nero, triangolo verde + single star below yellow band: una sola stella sotto la banda gialla + slightly darker blue: 'blu leggermente più scuro' + slightly lighter blue: 'blu leggermente più chiaro' + star instead of coat of arms: stella al posto dello stemma + text instead of emblem: 'testo al posto dell''emblema' + two stars in top-left corner: 'due stelle nell''angolo in alto a sinistra' + white and red flipped, brighter red: 'bianco e rosso invertiti, rosso più chiaro' + white and red flipped, narrower: 'bianco e rosso invertiti, più stretta' + white background, red and blue cross: sfondo bianco, croce rossa e blu + white stars, two more stars: 'stelle bianche, due stelle in più' + wider, brighter red: 'più larga, rosso più chiaro' + wider, coat of arms with eagle: 'più larga, stemma con un''aquila' + wider, more serrated edges, maroon: 'più larga, più bordi seghettati, rosso granata' + wider, smaller coat of arms: 'più larga, stemma più piccolo' + with coat of arms: con lo stemma + with emblem: 'con l''emblema' + with star and crescent: con stella e mezzaluna + with text: con il testo 'Åland Islands': 'Isole Åland' contextual: notes.note: @@ -531,6 +539,7 @@ translations: - Melanesia - Micronesia - Minsk + - Monaco - Mongolia - Monrovia - Montenegro diff --git a/fixtures/ultimate-geography/overlays/languages/nb.yaml b/fixtures/ultimate-geography/overlays/languages/nb.yaml index b83fac9..06673fa 100644 --- a/fixtures/ultimate-geography/overlays/languages/nb.yaml +++ b/fixtures/ultimate-geography/overlays/languages/nb.yaml @@ -21,7 +21,6 @@ translations: Also known as the Sea of Cortez.: '' 'Also spelled as Sana''a.': '' American Samoa: Amerikansk Samoa - 'Andorra (narrower, coat of arms with motto)': 'Andorra (smalere, våpen med valgspråk)' Antarctica: Antarktis Antigua and Barbuda: Antigua og Barbuda Arabian Sea: Det arabiske hav @@ -30,9 +29,7 @@ translations: Ashgabat: Asjkhabad Athens: Aten Atlantic Ocean: Atlanterhavet - 'Australia (white stars, two more stars)': 'Australia (hvite stjerner og to ekstra stjerner)' Austria: 'Østerrike' - 'Austria (brighter red, wider white band)': 'Østerrike (lysere rød, brede hvit stripe)' Autonomous community of Spain.: 'Selvstyrt område i Spania.' Autonomous province of South Korea.: 'Selvstyrt provins i Sør-Korea.' Autonomous region of Finland.: 'Selvstyrt og delimilitarisert øygruppe og landskap i Finland.' @@ -42,7 +39,6 @@ translations: Azerbaijan: Aserbajdsjan Azores: Asorene Baghdad: Bagdad - 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrain (smalere, færre tagger, rødt)' Balkan Peninsula: Balkan Baltic Sea: 'Østersjøen' Banda Sea: 'Bandasjøen' @@ -54,7 +50,6 @@ translations: Bering Strait: Beringstredet Bishkek: Bisjkek Black Sea: Svartehavet - 'Bolivia (coat of arms instead of star)': 'Bolivia (våpenskjold i stedet for stjerne)' Bosnia and Herzegovina: Bosnia-Hercegovina 'Brasília': Brasilia Brazil: Brasil @@ -64,7 +59,6 @@ translations: Cairo: Kairo Cambodia: Kambodsja Cameroon: Kamerun - 'Cameroon (green/red/yellow, yellow star)': 'Kamerun (grønn/rød/gul, gul stjerne)' Canary Islands: 'Kanariøyene' Cape Verde: Kapp Verde Caribbean Sea: Det karibiske hav @@ -75,12 +69,10 @@ translations: Central African Republic: Den sentralafrikanske republikk Cetinje is an honorary capital.: Administrativ hovedstad. Cetinje er historisk hovedstad og presidentens tilholdssted. Chad: Tsjad - 'Chad (slightly darker blue)': 'Tsjad (noe mørkere blå)' China: Kina City of San Marino: San Marino by Claimed and controlled: Krevd og kontrollert Claimed but not controlled: Krevd, men ikke kontrollert - 'Colombia (no coat of arms)': 'Colombia (uten våpenskjold)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Regjeringen har sete i Colombo. Sri Jayawardenepura Kotte (kalt Kotte) er en forstad til Colombo der nasjonalforsamlingen har sete. Kotte er altså lovgivende hovedstad.' Comoros: Komorene Constituent country in the Kingdom of Denmark.: 'Land som utgjør en del av kongeriket Danmark.' @@ -90,8 +82,6 @@ translations: Coral Sea: Korallhavet Corsica: Korsika Croatia: Kroatia - 'Cuba (red triangle, blue stripes)': 'Cuba (rød trekant, blå striper)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (to stjerner i øverste venstre hjørne)' Cyprus: Kypros Czech Republic: Tsjekkia Damascus: Damaskus @@ -105,10 +95,6 @@ translations: Dushanbe: Dusjanbe East China Sea: 'Øst-Kina-havet' East Siberian Sea: 'Øst-Sibirhavet' - 'Ecuador (with coat of arms)': 'Ecuador (med våpenskjold)' - 'Egypt (emblem instead of text), Yemen (no text)': 'Jemen (blank midstripe), Egypt (emblem)' - 'Egypt (with emblem), Iraq (with text)': 'Egypt (emblem), Irak (tekst)' - 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (annerledes våpenskjold, noe mørkere blå)' English Channel: Den engelske kanal Equatorial Guinea: Ekvatorial-Guinea Estonia: Estland @@ -124,11 +110,9 @@ translations: French Guiana: Fransk Guyana French Polynesia: Fransk Polynesia Germany: Tyskland - 'Ghana (star instead of coat of arms)': 'Ghana (stjerne i stedet for våpenskjold)' Greece: Hellas Greenland: 'Grønland' Guatemala City: Guatemala by - 'Guinea (green and red flipped, slightly darker green)': 'Guinea (byttet plass på grønt og rødt, noe mørkere grønnfarge)' Gulf of Alaska: Alaskabukta Gulf of California: Californiabukta Gulf of Carpentaria: Carpentariabukta @@ -142,23 +126,16 @@ translations: Hudson Bay: Hudsonbukta Hungary: Ungarn Iceland: Island - '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)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Island (blå bakgrunn, rødt kors), Færøyene (hvit bakgrunn, rødt og blått kors)' Independent state claimed by Georgia.: Utbryterstat i Georgia. Independent state claimed by Moldova.: 'Region i Moldova som er en selverklært, men ikke internasjonalt anerkjent, stat.' Independent state claimed by Somalia.: Utbryterrepublikk i Somalia. Fungerer som selvstendig stat, men ikke anerkjent av andre. Indian Ocean: Det indiske hav - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesia (rødt og hvitt byttet, lysere rød), Monaco (rødt og hvitt byttet, smalere)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesia (bredere, lysere rød), Polen (rødt og hvitt byttet, bredere)' Iraq: Irak - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Jemen (blank midstripe), Irak (tekst)' Ireland: Irland - 'Ireland (orange and green flipped, wider)': 'Ireland (byttet plass på oransje og grønt, bredere)' Island of Indonesia.: 'Øy i Indonesia.' Isle of Man: Man Italy: Italia Ivory Coast: Elfenbenskysten - 'Ivory Coast (green and orange flipped, narrower)': 'Elfenbenskysten (grønn og oransj har byttet plass, smalere)' Kaliningrad Oblast: Kaliningrad oblast Kathmandu: Katmandu Kazakhstan: Kasakhstan @@ -169,33 +146,24 @@ translations: Kyrgyzstan: Kirgisistan Laayoune: 'El Aaiún' Labrador Sea: 'Labradorsjøen' - 'Latvia (darker red, narrower white band)': 'Latvia (mørkere rød, smalere hvitt bånd)' Lebanon: Libanon Lisbon: Lisboa Lithuania: Litauen - 'Luxembourg (lighter blue)': 'Luxembourg (lysere blå)' Luxembourg City: Luxembourg Macau: Macao Madagascar: Madagaskar Maldives: Maldivene - 'Mali (red and green flipped, slightly brighter green)': 'Mali (byttet plass på rødt og grønt; noe lysere grønn)' Marshall Islands: 'Marshalløyene' Mediterranean Sea: Middelhavet Mexico City: Mexico by Micronesia: Mikronesia - 'Moldova (wider, coat of arms with eagle)': 'Moldova (bredere, våpenskjold med ørn)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (smalere, mørkere rød), Polen (rødt og hvitt byttet, mørkere rød)' Morocco: Marokko Moscow: Moskva Mozambique: Mosambik 'N''Djamena': Ndjamena - 'Nauru (single star below yellow band)': 'Nauru (en enkelt stjerne under et gult bånd)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru har ingen offisiell hovedstad; Yaren er regjeringssete.' Netherlands: Nederland - 'Netherlands (darker blue)': 'Nederland (mørkere blå)' New Caledonia: Ny-Caledonia - 'New Zealand (red stars, two fewer stars)': 'New Zealand (røde stjerner)' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (annerledes våpenskjold, noe lysere blå)' Nicosia: Nikosia North America: Nord-Amerika North Korea: Nord-Korea @@ -206,7 +174,6 @@ translations: Northern Ireland: Nord-Irland Northern Mariana Islands: Nord-Marianene Norway: Norge - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norge (rød bakgrunn, blått kors), Færøyene (hvit bakgrunn, rødt og blått kors)' Norwegian Sea: Norskehavet Not a sovereign country: Ikke selvstendig land 'Nukuʻalofa': Nukualofa @@ -218,8 +185,6 @@ translations: Officially Luxembourg.: '' Pacific Ocean: Stillehavet Palestine: Palestina - 'Palestine (black/white/green, red arrow)': 'Palestina (svart/hvit/grønn, rød trekant)' - 'Palestine (no symbol)': 'Palestina (uten symbol)' Panama City: Panama by Papua New Guinea: Papua Ny-Guinea Partially recognised state claimed by China.: 'Offisielt Republikken Kina. Folkerepublikken Kina gjør også krav på områdene.' @@ -231,15 +196,11 @@ translations: Poland: Polen Port Vila: Vila Prague: Praha - 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blå trekant, røde striper)' - 'Qatar (wider, more serrated edges, maroon)': 'Qatar (bredere, flere tagger og jordbrun farge)' Red Sea: 'Rødehavet' Region of France.: Fransk region. Republic of the Congo: Kongo-Brazzaville - 'Romania (slightly lighter blue)': 'Romania (noe lysere blå)' Rome: Roma Russia: Russland - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Russland (uten våpenskjold), Slovenia (bredere, mindre våpenskjold)' Sahrawi Arab Democratic Republic: Vest-Sahara Saint Kitts and Nevis: Saint Kitts og Nevis Saint Martin: Saint-Martin @@ -253,11 +214,8 @@ translations: Sea of Japan: Japanhavet Sea of Okhotsk: Okhotskhavet Semi-autonomous region of Tanzania.: Delvis selvstendig stat i Tanzania. - 'Senegal (green/yellow/red, green star)': 'Senegal (grønn/gul/rød, grønn stjerne)' Seychelles: Seychellene Sicily: Sicilia - 'Slovakia (narrower, bigger coat of arms)': 'Slovakia (smalere, større våpenskjold)' - 'Slovakia (with coat of arms)': 'Slovakia (med våpenskjold)' Solomon Islands: 'Salomonøyene' South Africa: 'Sør-Afrika' 'South Africa has no legally defined capital: the branches of government are split over three cities: Pretoria (executive), Cape Town (legislative) and Bloemfontein (judicial).': 'Sør-Afrika har ingen fastsatt hovedstad; makten er fordelt på tre byer: Pretoria (utøvende), Cape Town (lovgivende) og Bloemfontein (dømmende).' @@ -276,7 +234,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Område i det sentrale og sydlige Stillehavet som inneholder tusener av små øyer.' Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Område i det vestlige Stillehavet som inneholder tusenvis av små øyer.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Område i Stillehavet. Inneholder Vanuatu, Salomonøyene, Fiji og Papua Ny-Guinea.' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (rød/hvit/svart, grønn trekant), Vest-Sahara (med stjerne og halvmåne)' Suriname: Surinam Sweden: Sverige Switzerland: Sveits @@ -318,6 +275,57 @@ translations: Yellow Sea: Gulehavet Yemen: Jemen Yerevan: Jerevan + black/white/green, red arrow: 'svart/hvit/grønn, rød trekant' + blue background, red and white cross: 'blå bakgrunn, rødt og hvitt kors' + blue background, red cross: 'blå bakgrunn, rødt kors' + blue triangle, red stripes: 'blå trekant, røde striper' + brighter red, wider white band: 'lysere rød, brede hvit stripe' + coat of arms instead of star: 'våpenskjold i stedet for stjerne' + darker blue: 'mørkere blå' + darker red, narrower white band: 'mørkere rød, smalere hvitt bånd' + different coat of arms, slightly darker blue: 'annerledes våpenskjold, noe mørkere blå' + different coat of arms, slightly lighter blue: 'annerledes våpenskjold, noe lysere blå' + emblem instead of text: blank midstripe + green and orange flipped, narrower: 'grønn og oransj har byttet plass, smalere' + green and red flipped, slightly darker green: 'byttet plass på grønt og rødt, noe mørkere grønnfarge' + green/red/yellow, yellow star: 'grønn/rød/gul, gul stjerne' + green/yellow/red, green star: 'grønn/gul/rød, grønn stjerne' + lighter blue: 'lysere blå' + narrower, bigger coat of arms: 'smalere, større våpenskjold' + narrower, coat of arms with motto: 'smalere, våpen med valgspråk' + narrower, darker red: 'smalere, mørkere rød' + narrower, fewer serrated edges, red: 'smalere, færre tagger, rødt' + no coat of arms: 'uten våpenskjold' + no emblem: tekst + no symbol: uten symbol + no text: emblem + orange and green flipped, wider: 'byttet plass på oransje og grønt, bredere' + red and green flipped, slightly brighter green: 'byttet plass på rødt og grønt; noe lysere grønn' + red and white flipped, darker red: 'rødt og hvitt byttet, mørkere rød' + red and white flipped, wider: 'rødt og hvitt byttet, bredere' + red background, blue and white cross: 'rød bakgrunn, blått og hvitt kors' + red background, blue cross: 'rød bakgrunn, blått kors' + red stars, two fewer stars: 'røde stjerner' + red triangle, blue stripes: 'rød trekant, blå striper' + red/white/black, green arrow: 'rød/hvit/svart, grønn trekant' + single star below yellow band: 'en enkelt stjerne under et gult bånd' + slightly darker blue: 'noe mørkere blå' + slightly lighter blue: 'noe lysere blå' + star instead of coat of arms: 'stjerne i stedet for våpenskjold' + text instead of emblem: blank midstripe + two stars in top-left corner: 'to stjerner i øverste venstre hjørne' + white and red flipped, brighter red: 'rødt og hvitt byttet, lysere rød' + white and red flipped, narrower: 'rødt og hvitt byttet, smalere' + white background, red and blue cross: 'hvit bakgrunn, rødt og blått kors' + white stars, two more stars: hvite stjerner og to ekstra stjerner + wider, brighter red: 'bredere, lysere rød' + wider, coat of arms with eagle: 'bredere, våpenskjold med ørn' + wider, more serrated edges, maroon: bredere, flere tagger og jordbrun farge + wider, smaller coat of arms: 'bredere, mindre våpenskjold' + with coat of arms: 'med våpenskjold' + with emblem: emblem + with star and crescent: 'med stjerne og halvmåne' + with text: tekst 'Åland Islands': 'Åland' contextual: notes.note: @@ -335,6 +343,10 @@ translations: Overseas territory of the United Kingdom.: 'Selvstyrt britisk oversjøisk territorium i Karibia.' cura-ao: Constituent country of the Kingdom of the Netherlands.: '«Land» som sammen med Nederland, Aruba og Sint Maarten utgjør føderasjonen Kongeriket Nederlandene.' + egypt.fields.field.flag-similarity.message.variables.country_1: + Iraq: Jemen + egypt.fields.field.flag-similarity.message.variables.country_2: + Yemen: Irak falkland-islands: Overseas territory of the United Kingdom.: 'Britisk øygruppe.' french-guiana: @@ -349,8 +361,14 @@ translations: Crown dependency of the United Kingdom.: 'Kanaløy underlagt britisk kronbesittelse.' hong-kong: Special Administrative Region of China.: 'Også kjent som Hong Kong. Spesiell administrativ region i Kina.' + iraq.fields.field.flag-similarity.message.variables.country_1: + Egypt: Jemen + iraq.fields.field.flag-similarity.message.variables.country_2: + Yemen: Egypt isle-of-man: Crown dependency of the United Kingdom.: 'Også kalt Isle of Man. Kanaløy underlagt britisk kronbesittelse. Hverken del av Storbritannia eller EU.' + ivory-coast.fields.field.flag-similarity.message.variables.country_1: + Ireland: Ireland jersey: Crown dependency of the United Kingdom.: 'Kanaløy underlagt britisk kronbesittelse.' macau: diff --git a/fixtures/ultimate-geography/overlays/languages/nl.yaml b/fixtures/ultimate-geography/overlays/languages/nl.yaml index f1a828c..519f20c 100644 --- a/fixtures/ultimate-geography/overlays/languages/nl.yaml +++ b/fixtures/ultimate-geography/overlays/languages/nl.yaml @@ -21,7 +21,6 @@ translations: Also known as the Sea of Cortez.: 'Ook wel de Zee van Cortés genoemd.' 'Also spelled as Sana''a.': '' American Samoa: Amerikaans Samoa - 'Andorra (narrower, coat of arms with motto)': 'Andorra (smaller, wapen met het motto)' Antigua and Barbuda: Antigua en Barbuda Arabian Sea: Arabische Zee Aral Sea: Aralmeer @@ -33,9 +32,7 @@ translations: Athens: Athene Atlantic Ocean: Atlantische Oceaan Australia: 'Australië' - 'Australia (white stars, two more stars)': 'Australië (witte sterren, twee sterren meer)' Austria: Oostenrijk - 'Austria (brighter red, wider white band)': 'Oostenrijk (helderder rood, bredere witte band)' Autonomous community of Spain.: Autonome gemeenschap in Spanje. Autonomous province of South Korea.: Autonome provincie van Zuid-Korea. Autonomous region of Finland.: Autonome regio van Finland. @@ -46,7 +43,6 @@ translations: Azores: Azoren Baghdad: Bagdad Bahrain: Bahrein - 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrein (smaller, minder gekartelde randen, rood)' Baku: Bakoe Balkan Peninsula: Balkan Baltic Sea: Oostzee @@ -64,7 +60,6 @@ translations: Bishkek: Bisjkek Black Sea: Zwarte Zee 'Bogotá': Bogota - 'Bolivia (coat of arms instead of star)': 'Bolivia (wapen in plaats van ster)' Bosnia and Herzegovina: 'Bosnië en Herzegovina' 'Brasília': Brasilia Brazil: 'Brazilië' @@ -76,7 +71,6 @@ translations: Cairo: 'Caïro' Cambodia: Cambodja Cameroon: Kameroen - 'Cameroon (green/red/yellow, yellow star)': 'Kameroen (groen/rood/geel, gele ster)' Canary Islands: Canarische Eilanden Cape Verde: 'Kaapverdië' Caribbean Sea: Caribische Zee @@ -87,12 +81,10 @@ translations: Central African Republic: Centraal-Afrikaanse Republiek Cetinje is an honorary capital.: Voormalige hoofdstad is Cetinje. Chad: Tsjaad - 'Chad (slightly darker blue)': 'Tsjaad (donkerder blauw)' Chile: Chili City of San Marino: San Marino Claimed and controlled: Geclaimd en gecontroleerd Claimed but not controlled: Geclaimd maar niet gecontroleerd - 'Colombia (no coat of arms)': 'Colombia (geen wapen)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Colombo wordt vaak de hoofdstad genoemd, maar Sri Jayawardenapura Kotte, een buitenwijk van Colombo, is de officiële, wetgevende hoofdstad.' Comoros: Comoren Constituent country in the Kingdom of Denmark.: Autonoom gebied binnen het Koninkrijk Denemarken. @@ -103,8 +95,6 @@ translations: Coral Sea: Koraalzee Croatia: 'Kroatië' Crown dependency of the United Kingdom.: Brits Kroonbezit. - 'Cuba (red triangle, blue stripes)': 'Cuba (rode driehoek, blauwe strepen)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (twee sterren in de linkerbovenhoek)' Czech Republic: 'Tsjechië' Dead Sea: Dode Zee Democratic Republic of the Congo: Democratische Republiek Congo @@ -116,11 +106,7 @@ translations: Dushanbe: Doesjanbe East China Sea: Oost-Chinese Zee East Siberian Sea: Oost-Siberische Zee - 'Ecuador (with coat of arms)': 'Ecuador (met wapen)' Egypt: Egypte - 'Egypt (emblem instead of text), Yemen (no text)': 'Egypte (embleem in plaats van tekst), Jemen (geen tekst)' - 'Egypt (with emblem), Iraq (with text)': 'Egypte (met embleem), Irak (met tekst)' - 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (ander wapen, donkerder blauw)' England: Engeland English Channel: Het Kanaal Equatorial Guinea: Equatoriaal-Guinea @@ -138,12 +124,10 @@ translations: French Polynesia: 'Frans-Polynesië' Georgia: 'Georgië' Germany: Duitsland - 'Ghana (star instead of coat of arms)': 'Ghana (ster in plaats van wapen)' Greece: Griekenland Greenland: Groenland Guatemala City: Guatemala-Stad Guinea: Guinee - 'Guinea (green and red flipped, slightly darker green)': 'Guinee (groen en rood omgekeerd, donkerder groen)' Guinea-Bissau: Guinee-Bissau Gulf of Alaska: Golf van Alaska Gulf of California: 'Golf van Californië' @@ -158,25 +142,18 @@ translations: Hudson Bay: Hudsonbaai Hungary: Hongarije Iceland: IJsland - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'IJsland (blauwe achtergrond, rood met wit kruis), Noorwegen (rode achtergrond, blauw met wit kruis)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'IJsland (blauwe achtergrond, rood met wit kruis), Faeröer (witte achtergrond, rood/blauw kruis)' Independent state claimed by Georgia.: 'Onafhankelijke staat opgeëist door Georgië.' Independent state claimed by Moldova.: 'Onafhankelijke staat opgeëist door Moldavië.' Independent state claimed by Somalia.: 'Onafhankelijke staat opgeëist door Somalië.' Indian Ocean: Indische Oceaan Indonesia: 'Indonesië' - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesië (breder, helderder rood), Monaco (wit en rood omgekeerd, smaller)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesië (breder, helderder rood), Polen (rood en wit omgekeerd, breder)' Iraq: Irak - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (tekst in plaats van embleem), Jemen (geen embleem)' Ireland: Ierland - 'Ireland (orange and green flipped, wider)': 'Ierland (oranje en groen omgekeerd, breder)' Island of Indonesia.: 'Eiland van Indonesië.' Isle of Man: Man Israel: 'Israël' Italy: 'Italië' Ivory Coast: Ivoorkust - 'Ivory Coast (green and orange flipped, narrower)': 'Ivoorkust (groen en oranje omgekeerd, smaller)' Jerusalem: Jeruzalem Jordan: 'Jordanië' Juba: Djoeba @@ -193,19 +170,16 @@ translations: Laayoune: al-Ajoen Labrador Sea: Labradorzee Latvia: Letland - 'Latvia (darker red, narrower white band)': 'Letland (donkerder rood, smallere witte band)' Lebanon: Libanon Libya: 'Libië' Lisbon: Lissabon Lithuania: Litouwen London: Londen Luxembourg: Luxemburg - 'Luxembourg (lighter blue)': 'Luxemburg (lichter blauw)' Luxembourg City: Luxemburg Madagascar: Madagaskar Malaysia: 'Maleisië' Maldives: Malediven - 'Mali (red and green flipped, slightly brighter green)': 'Mali (rood en groen omgekeerd, helderder groen)' Manila: Manilla Marshall Islands: Marshalleilanden Mauritania: 'Mauritanië' @@ -214,22 +188,16 @@ translations: Mexico City: Mexico-Stad Micronesia: 'Micronesië' Moldova: 'Moldavië' - 'Moldova (wider, coat of arms with eagle)': 'Moldavië (breder, wapen met adelaar)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (smaller, donkerder rood), Polen (rood en wit omgekeerd, donkerder rood)' Mongolia: 'Mongolië' Morocco: Marokko Moscow: Moskou Muscat: Masqat 'N''Djamena': Ndjamena Namibia: 'Namibië' - 'Nauru (single star below yellow band)': 'Nauru (enkele ster onder de gele band)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru heeft geen officiële hoofdstad; het Yaren-district is de de facto hoofdstad.' Netherlands: Nederland - 'Netherlands (darker blue)': 'Nederland (donkerder blauw)' New Caledonia: 'Nieuw-Caledonië' New Zealand: Nieuw-Zeeland - 'New Zealand (red stars, two fewer stars)': 'Nieuw-Zeeland (rode sterren, twee sterren minder)' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (ander wapen, lichter blauw)' North America: Noord-Amerika North Korea: Noord-Korea North Macedonia: 'Noord-Macedonië' @@ -239,7 +207,6 @@ translations: Northern Ireland: Noord-Ierland Northern Mariana Islands: Noordelijke Marianen Norway: Noorwegen - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Noorwegen (rode achtergrond, blauw kruis), Faeröer (witte achtergrond, rood/blauw kruis)' Norwegian Sea: Noorse Zee Not a sovereign country: Geen soevereine staat 'Oblast (administrative region) of the Russian Federation.': 'Oblast (administratieve regio) van de Russische Federatie.' @@ -253,8 +220,6 @@ translations: Overseas territory of the United Kingdom.: Overzees land van het Verenigd Koninkrijk. Pacific Ocean: Stille Oceaan Palestine: Palestina - 'Palestine (black/white/green, red arrow)': 'Palestina (zwart/wit/groen, rode pijl)' - 'Palestine (no symbol)': 'Palestina (geen symbool)' Panama City: Panama-stad Papua New Guinea: Papoea-Nieuw-Guinea Paris: Parijs @@ -268,17 +233,13 @@ translations: Polynesia: 'Polynesië' Prague: Praag Pretoria, Cape Town, Bloemfontein: Pretoria, Kaapstad, Bloemfontein - 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blauwe driehoek, rode strepen)' - 'Qatar (wider, more serrated edges, maroon)': 'Qatar (breder, meer gekartelde randen, kastanjebruin)' Red Sea: Rode Zee Region of France.: Franse regio. Republic of the Congo: Congo 'Reykjavík': Reykjavik Riyadh: Riyad Romania: 'Roemenië' - 'Romania (slightly lighter blue)': 'Roemenië (lichter blauw)' Russia: Rusland - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rusland (geen wapen), Slovenië (breder, kleiner wapen)' Sahrawi Arab Democratic Republic: Sahrawi Arabische Democratische Republiek Saint Kitts and Nevis: Saint Kitts en Nevis Saint Martin: 'Sint-Maarten (Franse Antillen)' @@ -291,15 +252,12 @@ translations: Sea of Japan: Japanse Zee Sea of Okhotsk: Zee van Ochotsk Semi-autonomous region of Tanzania.: Half-autonome regio van Tanzania. - 'Senegal (green/yellow/red, green star)': 'Senegal (groen/geel/rood, groene ster)' Seoul: Seoel Serbia: 'Servië' Seychelles: Seychellen Sicily: 'Sicilië' Sint Maarten: 'Sint Maarten (Koninkrijk der Nederlanden)' Slovakia: Slowakije - 'Slovakia (narrower, bigger coat of arms)': 'Slowakije (smaller, groter wapen)' - 'Slovakia (with coat of arms)': 'Slowakije (met wapen)' Slovenia: 'Slovenië' Solomon Islands: Salomonseilanden Somalia: 'Somalië' @@ -324,7 +282,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Subregio van Oceanië met duizenden kleine eilanden in de westerse Stille Oceaan.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Subregio van Oceanië, waaronder Vanuatu, de Salomonseilanden, Fiji en Papoea-Nieuw-Guinea.' Sudan: Soedan - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Soedan (rood/wit/zwart, groene pijl), Sahrawi Arabische Democratische Republiek (met wassende maan en ster)' Sukhumi: Soechoemi Svalbard: Spitsbergen Sweden: Zweden @@ -374,6 +331,57 @@ translations: Yellow Sea: Gele Zee Yemen: Jemen Yerevan: Jerevan + black/white/green, red arrow: zwart/wit/groen, rode pijl + blue background, red and white cross: blauwe achtergrond, rood met wit kruis + blue background, red cross: blauwe achtergrond, rood met wit kruis + blue triangle, red stripes: blauwe driehoek, rode strepen + brighter red, wider white band: helderder rood, bredere witte band + coat of arms instead of star: wapen in plaats van ster + darker blue: donkerder blauw + darker red, narrower white band: donkerder rood, smallere witte band + different coat of arms, slightly darker blue: ander wapen, donkerder blauw + different coat of arms, slightly lighter blue: ander wapen, lichter blauw + emblem instead of text: embleem in plaats van tekst + green and orange flipped, narrower: groen en oranje omgekeerd, smaller + green and red flipped, slightly darker green: groen en rood omgekeerd, donkerder groen + green/red/yellow, yellow star: groen/rood/geel, gele ster + green/yellow/red, green star: groen/geel/rood, groene ster + lighter blue: lichter blauw + narrower, bigger coat of arms: smaller, groter wapen + narrower, coat of arms with motto: smaller, wapen met het motto + narrower, darker red: smaller, donkerder rood + narrower, fewer serrated edges, red: smaller, minder gekartelde randen, rood + no coat of arms: geen wapen + no emblem: geen embleem + no symbol: geen symbool + no text: geen tekst + orange and green flipped, wider: oranje en groen omgekeerd, breder + red and green flipped, slightly brighter green: rood en groen omgekeerd, helderder groen + red and white flipped, darker red: rood en wit omgekeerd, donkerder rood + red and white flipped, wider: rood en wit omgekeerd, breder + red background, blue and white cross: rode achtergrond, blauw met wit kruis + red background, blue cross: rode achtergrond, blauw kruis + red stars, two fewer stars: rode sterren, twee sterren minder + red triangle, blue stripes: rode driehoek, blauwe strepen + red/white/black, green arrow: rood/wit/zwart, groene pijl + single star below yellow band: enkele ster onder de gele band + slightly darker blue: donkerder blauw + slightly lighter blue: lichter blauw + star instead of coat of arms: ster in plaats van wapen + text instead of emblem: tekst in plaats van embleem + two stars in top-left corner: twee sterren in de linkerbovenhoek + white and red flipped, brighter red: breder, helderder rood + white and red flipped, narrower: wit en rood omgekeerd, smaller + white background, red and blue cross: witte achtergrond, rood/blauw kruis + white stars, two more stars: witte sterren, twee sterren meer + wider, brighter red: breder, helderder rood + wider, coat of arms with eagle: breder, wapen met adelaar + wider, more serrated edges, maroon: breder, meer gekartelde randen, kastanjebruin + wider, smaller coat of arms: breder, kleiner wapen + with coat of arms: met wapen + with emblem: met embleem + with star and crescent: met wassende maan en ster + with text: met tekst 'Åland Islands': 'Åland' no_change: - Abu Dhabi diff --git a/fixtures/ultimate-geography/overlays/languages/pl.yaml b/fixtures/ultimate-geography/overlays/languages/pl.yaml index 48405d1..81d6b3b 100644 --- a/fixtures/ultimate-geography/overlays/languages/pl.yaml +++ b/fixtures/ultimate-geography/overlays/languages/pl.yaml @@ -26,7 +26,6 @@ translations: 'Also spelled as Sana''a.': '' American Samoa: 'Samoa Amerykańskie' Andorra: Andora - 'Andorra (narrower, coat of arms with motto)': 'Andora (węższa, godło z dewizą)' Andorra la Vella: Andora Antananarivo: Antananarywa Antarctica: Antarktyda @@ -39,8 +38,6 @@ translations: Asia: Azja Athens: Ateny Atlantic Ocean: Ocean Atlantycki - 'Australia (white stars, two more stars)': 'Australia (białe gwiazdy, o dwie gwiazdy więcej)' - 'Austria (brighter red, wider white band)': 'Austria (jaśniejsza czerwień, szerszy biały pas)' Autonomous community of Spain.: 'Wspólnota autonomiczna Hiszpanii.' Autonomous province of South Korea.: 'Terytorium autonomiczne Korei Południowej, znane także jako Cheju lub Jeju.' Autonomous region of Finland.: Terytorium autonomiczne Finlandii. @@ -51,7 +48,6 @@ translations: Azores: Azory Baghdad: Bagdad Bahrain: Bahrajn - 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrajn (węższa, mniej klinów, czerwona)' Balkan Peninsula: 'Półwysep Bałkański' Baltic Sea: 'Morze Bałtyckie' Banda Sea: Morze Banda @@ -73,7 +69,6 @@ translations: Black Sea: Morze Czarne 'Bogotá': Bogota Bolivia: Boliwia - 'Bolivia (coat of arms instead of star)': 'Boliwia (godło zamiast gwiazd)' Bosnia and Herzegovina: 'Bośnia i Hercegowina' 'Brasília': Brasilia Bratislava: 'Bratysława' @@ -86,7 +81,6 @@ translations: Cairo: Kair Cambodia: 'Kambodża' Cameroon: Kamerun - 'Cameroon (green/red/yellow, yellow star)': 'Kamerun (zielono-czerwono-żółta, żółta gwiazda)' Canada: Kanada Canary Islands: Wyspy Kanaryjskie Cape Verde: 'Republika Zielonego Przylądka' @@ -98,14 +92,12 @@ translations: Central African Republic: 'Republika Środkowoafrykańska' Cetinje is an honorary capital.: 'Honorową stolicą jest Cetynia.' Chad: Czad - 'Chad (slightly darker blue)': 'Czad (lekko ciemniejszy niebieski)' China: Chiny 'Chișinău': 'Kiszyniów' City of San Marino: San Marino Claimed and controlled: 'Terytorium sporne, nad którym sprawuje on władzę' Claimed but not controlled: 'Terytorium sporne, nad którym nie sprawuje ona władzy' Colombia: Kolumbia - 'Colombia (no coat of arms)': 'Kolumbia (brak godła)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Kolombo często nazywane jest stolicą, ale Sri Dźajawardanapura Kotte, przedmieście Kolombo, jest oficjalną stolicą ustawodawczą.' Comoros: Komory Conakry: Konakry @@ -120,8 +112,6 @@ translations: Croatia: Chorwacja Crown dependency of the United Kingdom.: Dependencja korony brytyjskiej. Cuba: Kuba - 'Cuba (red triangle, blue stripes)': 'Kuba (czerwony trójkąt, niebieskie pasy)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (dwie gwiazdy w lewym górnym rogu)' Cyprus: Cypr Czech Republic: Czechy Damascus: Damaszek @@ -138,13 +128,9 @@ translations: East China Sea: 'Morze Wschodniochińskie' East Siberian Sea: Morze Wschodniosyberyjskie Ecuador: Ekwador - 'Ecuador (with coat of arms)': 'Ekwador (godło)' Edinburgh: Edynburg Egypt: Egipt - 'Egypt (emblem instead of text), Yemen (no text)': 'Egipt (godło zamiast napisu), Jemen (brak napisu)' - 'Egypt (with emblem), Iraq (with text)': 'Egipt (godło), Irak (napis)' El Salvador: Salwador - 'El Salvador (different coat of arms, slightly darker blue)': 'Salwador (inne godło, lekko ciemniejszy niebieski)' England: Anglia English Channel: La Manche Equatorial Guinea: 'Gwinea Równikowa' @@ -164,14 +150,12 @@ translations: French Polynesia: Polinezja Francuska Georgia: Gruzja Germany: Niemcy - 'Ghana (star instead of coat of arms)': 'Ghana (gwiazdy zamiast godła)' Greece: Grecja Greenland: Grenlandia Guadeloupe: Gwadelupa Guatemala: Gwatemala Guatemala City: Gwatemala Guinea: Gwinea - 'Guinea (green and red flipped, slightly darker green)': 'Gwinea (zamieniona zieleń i czerwień, lekko ciemniejsza zieleń)' Guinea-Bissau: Gwinea Bissau Gulf of Alaska: Zatoka Alaska Gulf of California: Zatoka Kalifornijska @@ -188,26 +172,19 @@ translations: Hudson Bay: Zatoka Hudsona Hungary: 'Węgry' Iceland: Islandia - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Islandia (niebieskie tło, czerwono-biały krzyż), Norwegia (czerwone tło, niebiesko-biały krzyż)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Islandia (niebieskie tło, czerwony krzyż), Wyspy Owcze (białe tło, czerwono-niebieski krzyż)' Independent state claimed by Georgia.: 'Państwo nieuznawane, terytorium sporne z Gruzją.' Independent state claimed by Moldova.: 'Państwo nieuznawane, terytorium sporne z Mołdawią.' Independent state claimed by Somalia.: 'Państwo nieuznawane, terytorium sporne z Somalią.' India: Indie Indian Ocean: Ocean Indyjski Indonesia: Indonezja - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonezja (zamieniona biel i czerwień, jaśniejsza czerwień), Monako (zamieniona biel i czerwień, węższa)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonezja (szersza, jaśniejsza czerwień), Polska (zamieniona czerwień i biel, szersza)' Iraq: Irak - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (napis zamiast godła), Jemen (brak godła)' Ireland: Irlandia - 'Ireland (orange and green flipped, wider)': 'Irlandia (zamieniona pomarańcz i zieleń, szersza)' Island of Indonesia.: Wyspa w Indonezji. Isle of Man: Wyspa Man Israel: Izrael Italy: 'Włochy' Ivory Coast: 'Wybrzeże Kości Słoniowej' - 'Ivory Coast (green and orange flipped, narrower)': 'Wybrzeże Kości Słoniowej (zamieniona zieleń i pomarańcz, węższa)' Jakarta: 'Dżakarta' Jamaica: Jamajka Japan: Japonia @@ -232,7 +209,6 @@ translations: Laayoune: Al-Ujun Labrador Sea: Morze Labradorskie Latvia: 'Łotwa' - 'Latvia (darker red, narrower white band)': 'Łotwa (ciemniejsza czerwień, węższy biały pas)' Lebanon: Liban Libya: Libia Lisbon: Lizbona @@ -240,7 +216,6 @@ translations: Ljubljana: Lublana London: Londyn Luxembourg: Luksemburg - 'Luxembourg (lighter blue)': 'Luksemburg (jaśniejszy niebieski)' Luxembourg City: Luksemburg Macau: Makau Madagascar: Madagaskar @@ -248,7 +223,6 @@ translations: Madrid: Madryt Malaysia: Malezja Maldives: Malediwy - 'Mali (red and green flipped, slightly brighter green)': 'Mali (zamieniona czerwień i zieleń, lekko jaśniejsza zieleń)' 'Malé': Male Mariehamn: Maarianhamina Marshall Islands: Wyspy Marshalla @@ -263,9 +237,7 @@ translations: Minsk: 'Mińsk' Mogadishu: Mogadiszu Moldova: 'Mołdawia' - 'Moldova (wider, coat of arms with eagle)': 'Mołdawia (szersza, godło z orłem)' Monaco: Monako - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monako (węższa, ciemniejsza czerwień), Polska (zamieniona czerwień i biel, ciemniejsza czerwień)' Montenegro: 'Czarnogóra' Morocco: Maroko Moscow: Moskwa @@ -273,16 +245,12 @@ translations: Muscat: Muskat Myanmar: Mjanma 'N''Djamena': 'Ndżamena' - 'Nauru (single star below yellow band)': 'Nauru (jedna gwiazda pod żółtym pasem)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru nie ma oficjalnej stolicy; de facto stolicą jest Yaren.' Netherlands: Holandia - 'Netherlands (darker blue)': 'Holandia (ciemniejszy niebieski)' New Caledonia: Nowa Kaledonia New Delhi: Nowe Delhi New Zealand: Nowa Zelandia - 'New Zealand (red stars, two fewer stars)': 'Nowa Zelandia (czerwone gwiazdy, o dwie gwiazdy mniej)' Nicaragua: Nikaragua - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nikaragua (inne godło, lekko jaśniejszy niebieski)' Nicosia: Nikozja North America: 'Ameryka Północna' North Korea: 'Korea Północna' @@ -293,7 +261,6 @@ translations: Northern Ireland: 'Irlandia Północna' Northern Mariana Islands: 'Mariany Północne' Norway: Norwegia - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norwegia (czerwone tło, niebieski krzyż), Wyspy Owcze (białe tło, czerwono-niebieski krzyż)' Norwegian Sea: Morze Norweskie Not a sovereign country: 'Nie jest suwerennym państwem' Nouakchott: Nawakszut @@ -309,8 +276,6 @@ translations: Overseas territory of the United Kingdom.: Terytorium zamorskie Wielkiej Brytanii. Pacific Ocean: Ocean Spokojny Palestine: Palestyna - 'Palestine (black/white/green, red arrow)': 'Palestyna (czarno-biało-zielona, czerwony trójkąt)' - 'Palestine (no symbol)': 'Palestyna (brak symbolu)' Panama City: Panama Papua New Guinea: Papua-Nowa Gwinea Paraguay: Paragwaj @@ -329,20 +294,16 @@ translations: Pretoria, Cape Town, Bloemfontein: Pretoria, Kapsztad, Bloemfontein Pristina: Prisztina Puerto Rico: Portoryko - 'Puerto Rico (blue triangle, red stripes)': 'Portoryko (niebieski trójkąt, czerwone pasy)' Pyongyang: Pjongjang Qatar: Katar - 'Qatar (wider, more serrated edges, maroon)': 'Katar (szersza, więcej klinów, koloru bordowego)' Red Sea: Morze Czerwone Region of France.: Region Francji. Republic of the Congo: Kongo Riga: Ryga Riyadh: Rijad Romania: Rumunia - 'Romania (slightly lighter blue)': 'Rumunia (lekko jaśniejszy niebieski)' Rome: Rzym Russia: Rosja - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rosja (brak godła), Słowenia (szersza, mniejsze godło)' 'Réunion': Reunion Sahrawi Arab Democratic Republic: Sahara Zachodnia Saint Kitts and Nevis: Saint Kitts i Nevis @@ -358,14 +319,11 @@ translations: Sea of Japan: 'Morze Japońskie' Sea of Okhotsk: Morze Ochockie Semi-autonomous region of Tanzania.: 'Częściowo autonomiczna część Tanzanii.' - 'Senegal (green/yellow/red, green star)': 'Senegal (zielono-żółto-czerwona, zielona gwiazda)' Seoul: Seul Seychelles: Seszele Sicily: Sycylia Singapore: Singapur Slovakia: 'Słowacja' - 'Slovakia (narrower, bigger coat of arms)': 'Słowacja (węższa, większe godło)' - 'Slovakia (with coat of arms)': 'Słowacja (godło)' Slovenia: 'Słowenia' Solomon Islands: Wyspy Salomona South Africa: 'Południowa Afryka' @@ -388,7 +346,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Region Oceanii obejmujący tysiące małych wysp w środkowej i południowej części Oceanu Spokojnego.' Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Region Oceanii obejmujący tysiące małych wysp w zachodniej części Oceanu Spokojnego.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Region Oceanii obejmujący Vanuatu, Wyspy Salomona, Fidżi, oraz Papuę-Nową Gwineę.' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (czerwono-biało-czarna, zielony trójkąt), Sahara Zachodnia (z gwiazdą i półksiężycem)' Sukhumi: Suchumi Suriname: Surinam Sweden: Szwecja @@ -451,6 +408,57 @@ translations: Yemen: Jemen Yerevan: 'Erywań' Zagreb: Zagrzeb + black/white/green, red arrow: 'czarno-biało-zielona, czerwony trójkąt' + blue background, red and white cross: 'niebieskie tło, czerwono-biały krzyż' + blue background, red cross: 'niebieskie tło, czerwony krzyż' + blue triangle, red stripes: 'niebieski trójkąt, czerwone pasy' + brighter red, wider white band: 'jaśniejsza czerwień, szerszy biały pas' + coat of arms instead of star: 'godło zamiast gwiazd' + darker blue: ciemniejszy niebieski + darker red, narrower white band: 'ciemniejsza czerwień, węższy biały pas' + different coat of arms, slightly darker blue: 'inne godło, lekko ciemniejszy niebieski' + different coat of arms, slightly lighter blue: 'inne godło, lekko jaśniejszy niebieski' + emblem instead of text: 'godło zamiast napisu' + green and orange flipped, narrower: 'zamieniona zieleń i pomarańcz, węższa' + green and red flipped, slightly darker green: 'zamieniona zieleń i czerwień, lekko ciemniejsza zieleń' + green/red/yellow, yellow star: 'zielono-czerwono-żółta, żółta gwiazda' + green/yellow/red, green star: 'zielono-żółto-czerwona, zielona gwiazda' + lighter blue: 'jaśniejszy niebieski' + narrower, bigger coat of arms: 'węższa, większe godło' + narrower, coat of arms with motto: 'węższa, godło z dewizą' + narrower, darker red: 'węższa, ciemniejsza czerwień' + narrower, fewer serrated edges, red: 'węższa, mniej klinów, czerwona' + no coat of arms: 'brak godła' + no emblem: 'brak godła' + no symbol: brak symbolu + no text: brak napisu + orange and green flipped, wider: 'zamieniona pomarańcz i zieleń, szersza' + red and green flipped, slightly brighter green: 'zamieniona czerwień i zieleń, lekko jaśniejsza zieleń' + red and white flipped, darker red: 'zamieniona czerwień i biel, ciemniejsza czerwień' + red and white flipped, wider: 'zamieniona czerwień i biel, szersza' + red background, blue and white cross: 'czerwone tło, niebiesko-biały krzyż' + red background, blue cross: 'czerwone tło, niebieski krzyż' + red stars, two fewer stars: czerwone gwiazdy, o dwie gwiazdy mniej + red triangle, blue stripes: 'czerwony trójkąt, niebieskie pasy' + red/white/black, green arrow: 'czerwono-biało-czarna, zielony trójkąt' + single star below yellow band: 'jedna gwiazda pod żółtym pasem' + slightly darker blue: lekko ciemniejszy niebieski + slightly lighter blue: 'lekko jaśniejszy niebieski' + star instead of coat of arms: 'gwiazdy zamiast godła' + text instead of emblem: 'napis zamiast godła' + two stars in top-left corner: 'dwie gwiazdy w lewym górnym rogu' + white and red flipped, brighter red: 'zamieniona biel i czerwień, jaśniejsza czerwień' + white and red flipped, narrower: 'zamieniona biel i czerwień, węższa' + white background, red and blue cross: 'białe tło, czerwono-niebieski krzyż' + white stars, two more stars: 'białe gwiazdy, o dwie gwiazdy więcej' + wider, brighter red: 'szersza, jaśniejsza czerwień' + wider, coat of arms with eagle: 'szersza, godło z orłem' + wider, more serrated edges, maroon: 'szersza, więcej klinów, koloru bordowego' + wider, smaller coat of arms: 'szersza, mniejsze godło' + with coat of arms: 'godło' + with emblem: 'godło' + with star and crescent: 'z gwiazdą i półksiężycem' + with text: napis 'Åland Islands': Wyspy Alandzkie no_change: - Alaska diff --git a/fixtures/ultimate-geography/overlays/languages/pt.yaml b/fixtures/ultimate-geography/overlays/languages/pt.yaml index cd9e8e1..484cf0b 100644 --- a/fixtures/ultimate-geography/overlays/languages/pt.yaml +++ b/fixtures/ultimate-geography/overlays/languages/pt.yaml @@ -28,7 +28,6 @@ translations: American Samoa: Samoa Americana Amman: 'Amã' Amsterdam: 'Amsterdã (BR), Amsterdão (PT)' - 'Andorra (narrower, coat of arms with motto)': 'Andorra (mais estreita, brasão com lema)' Andorra la Vella: Andorra-a-Velha Anguilla: Anguila Ankara: Ancara @@ -44,9 +43,7 @@ translations: Athens: Atenas Atlantic Ocean: 'Oceano Atlântico' Australia: 'Austrália' - 'Australia (white stars, two more stars)': 'Austrália (estrelas brancas, duas estrelas a mais)' Austria: 'Áustria' - 'Austria (brighter red, wider white band)': 'Áustria (vermelho mais brilhante, faixa branca mais larga)' Autonomous community of Spain.: 'Comunidade autônoma da Espanha.' Autonomous province of South Korea.: 'Província autônoma da Coreia do Sul.' Autonomous region of Finland.: 'Região autônoma da Finlândia.' @@ -56,7 +53,6 @@ translations: Azores: 'Açores' Baghdad: 'Bagdá (BR), Bagdade (PT)' Bahrain: 'Barém' - 'Bahrain (narrower, fewer serrated edges, red)': 'Barém (mais estreita, menos bordas serrilhadas, vermelha)' Baku: Bacu Balkan Peninsula: 'Bálcãs' Baltic Sea: 'Mar Báltico' @@ -84,7 +80,6 @@ translations: Bishkek: Bisqueque Black Sea: Mar Negro Bolivia: 'Bolívia' - 'Bolivia (coat of arms instead of star)': ' Bolívia (brasão de armas em vez de estrela)' Bosnia and Herzegovina: 'Bósnia e Herzegovina' Botswana: Botsuana Brazil: Brasil @@ -97,7 +92,6 @@ translations: Burkina Faso: Burquina Fasso Cambodia: Camboja Cameroon: 'Camarões' - 'Cameroon (green/red/yellow, yellow star)': 'Camarões (verde/vermelho/amarelo, estrela amarela)' Canada: 'Canadá' Canary Islands: 'Ilhas Canárias' Canberra: Camberra @@ -111,7 +105,6 @@ translations: Central African Republic: 'República Centro-Africana' Cetinje is an honorary capital.: 'Cetinje é uma capital honorária.' Chad: Chade - 'Chad (slightly darker blue)': 'Chade (azul ligeiramente mais escuro)' Charlotte Amalie: 'Carlota Amália' 'Chișinău': Quixinau City of San Marino: 'São Marinho' @@ -119,7 +112,6 @@ translations: Claimed and controlled: Reivindicado e controlado Claimed but not controlled: 'Reivindicada, mas não controlada' Colombia: 'Colômbia' - 'Colombia (no coat of arms)': 'Colômbia (sem brasão)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Colombo é muitas vezes referido como a capital, mas Seri Jaiavardenapura-Cota, um subúrbio de Colombo, é a capital legislativa oficial.' Comoros: Comores Conakry: Conacri @@ -131,8 +123,6 @@ translations: Coral Sea: Mar de Coral Corsica: 'Córsega' Croatia: 'Croácia' - 'Cuba (red triangle, blue stripes)': 'Cuba (triângulo vermelho, listras azuis)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (duas estrelas no canto superior esquerdo)' Cyprus: Chipre Czech Republic: 'Tchéquia (BR), Chéquia (PT)' Dakar: Dacar @@ -153,12 +143,8 @@ translations: East China Sea: Mar da China Oriental East Siberian Sea: Mar Siberiano Oriental Ecuador: Equador - 'Ecuador (with coat of arms)': 'Equador (com brasão)' Edinburgh: Edimburgo Egypt: 'Egito (BR), Egipto (PT)' - 'Egypt (emblem instead of text), Yemen (no text)': 'Egito (emblema em vez de texto) e Iêmen (sem texto)' - 'Egypt (with emblem), Iraq (with text)': 'Egito (com emblema), e Iraque (com texto)' - 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (brasão diferente, azul ligeiramente mais escuro)' England: Inglaterra English Channel: Canal da Mancha Equatorial Guinea: 'Guiné Equatorial' @@ -181,7 +167,6 @@ translations: Georgia: 'Geórgia' Germany: Alemanha Ghana: Gana - 'Ghana (star instead of coat of arms)': 'Gana (estrela em vez de brasão)' Gitega: Guitega Greece: 'Grécia' Greenland: 'Groenlândia' @@ -190,7 +175,6 @@ translations: Guatemala City: Cidade da Guatemala Guernsey: Guernesei Guinea: 'Guiné' - 'Guinea (green and red flipped, slightly darker green)': 'Guiné (verde e vermelho invertidos, verde ligeiramente mais escuro)' Guinea-Bissau: 'Guiné-Bissau' Gulf of Alaska: Golfo do Alasca Gulf of California: 'Golfo da Califórnia' @@ -207,27 +191,20 @@ translations: Hudson Bay: 'Baía de Hudson' Hungary: Hungria Iceland: 'Islândia' - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Islândia (fundo azul, cruz vermelha e branca) e Noruega (fundo vermelho, cruz azul e branca)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Islândia (fundo azul, cruz vermelha) e Ilhas Faroé (fundo branco, cruz vermelha e azul)' Independent state claimed by Georgia.: 'Estado independente reivindicado pela Geórgia.' Independent state claimed by Moldova.: 'Estado independente reivindicado pela Moldávia.' Independent state claimed by Somalia.: 'Estado independente reivindicado pela Somália.' India: 'Índia' Indian Ocean: 'Oceano Índico' Indonesia: 'Indonésia' - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonésia (branco e vermelho invertidos, vermelho mais brilhante) e Mônaco (branco e vermelho invertidos, mais estreita)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonésia (mais larga, vermelho mais brilhante) e Polônia (mais larga, vermelho e branco invertidos)' Iran: 'Irã (BR), Irão (PT)' Iraq: Iraque - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Iraque (texto em vez de emblema) e Iêmen (sem emblema)' Ireland: Irlanda - 'Ireland (orange and green flipped, wider)': 'Irlanda (laranja e verde invertidos, mais larga)' Islamabad: Islamabade Island of Indonesia.: 'Ilha da Indonésia.' Isle of Man: Ilha de Man Italy: 'Itália' Ivory Coast: Costa do Marfim - 'Ivory Coast (green and orange flipped, narrower)': 'Costa do Marfim (verde e laranja invertidos, mais estreita)' Jakarta: Jacarta Japan: 'Japão' Jersey: 'Jérsia' @@ -252,7 +229,6 @@ translations: Laayoune: Laiune Labrador Sea: Mar de Labrador Latvia: 'Letônia (BR), Letónia (PT)' - 'Latvia (darker red, narrower white band)': 'Letônia (vermelho mais escuro, faixa branca mais estreita)' Lebanon: 'Líbano' Lesotho: Lesoto Liberia: 'Libéria' @@ -265,13 +241,11 @@ translations: London: Londres Lusaka: Lusaca Luxembourg: Luxemburgo - 'Luxembourg (lighter blue)': 'do Luxemburgo (azul mais claro)' Luxembourg City: Luxemburgo Madrid: 'Madri (BR), Madrid (PT)' Malawi: 'Maláui' Malaysia: 'Malásia' Maldives: Maldivas - 'Mali (red and green flipped, slightly brighter green)': 'Mali (vermelho e verde invertidos, verde ligeiramente mais brilhante)' Managua: 'Manágua' Mariehamn: Marianhamina Marshall Islands: Ilhas Marshall @@ -288,9 +262,7 @@ translations: Minsk: Minsque Mogadishu: 'Mogadíscio' Moldova: 'Moldávia' - 'Moldova (wider, coat of arms with eagle)': 'Moldávia (mais larga, brasão com águia)' Monaco: 'Mônaco (BR), Mónaco (PT)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Mônaco (mais estreita, vermelho mais escuro) e Polônia (vermelho e branco invertidos, vermelho mais escuro)' Mongolia: 'Mongólia' Monrovia: 'Monróvia' Montevideo: 'Montevidéu (BR), Montevideu (PT)' @@ -302,18 +274,14 @@ translations: 'N''Djamena': Jamena Nairobi: 'Nairóbi' Namibia: 'Namíbia' - 'Nauru (single star below yellow band)': 'Nauru (estrela única abaixo da faixa amarela)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru não tem capital oficial; o distrito de Iarém é apenas considerada a capital.' Naypyidaw: 'Nepiedó' Netherlands: 'Países Baixos' - 'Netherlands (darker blue)': 'Países Baixos (azul mais escuro)' New Caledonia: 'Nova Caledônia' New Delhi: Nova Deli New Zealand: 'Nova Zelândia' - 'New Zealand (red stars, two fewer stars)': 'Nova Zelândia (estrelas vermelhas, duas estrelas a menos)' Niamey: Niamei Nicaragua: 'Nicarágua' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicarágua (brasão diferente, azul ligeiramente mais claro)' Nicosia: 'Nicósia' Niger: 'Níger' Nigeria: 'Nigéria' @@ -327,7 +295,6 @@ translations: Northern Ireland: Irlanda do Norte Northern Mariana Islands: Ilhas Marianas do Norte Norway: Noruega - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Noruega (fundo vermelho, cruz azul) e Ilhas Faroé (fundo branco, cruz vermelha e azul)' Norwegian Sea: Mar da Noruega Not a sovereign country: 'Não é um Estado soberano' Nouakchott: Nuaquexote @@ -348,8 +315,6 @@ translations: Pacific Ocean: 'Oceano Pacífico' Pakistan: 'Paquistão' Palestine: Palestina - 'Palestine (black/white/green, red arrow)': 'Palestina (preto/branco/verde, seta vermelha)' - 'Palestine (no symbol)': 'Palestina (sem símbolo)' Palikir: Paliquir Panama: 'Panamá' Panama City: 'Cidade do Panamá' @@ -374,10 +339,8 @@ translations: Prague: Praga Pretoria, Cape Town, Bloemfontein: 'Pretória, Cidade do Cabo, Blumefontaina' Puerto Rico: Porto Rico - 'Puerto Rico (blue triangle, red stripes)': 'Porto Rico (triângulo azul, listras vermelhas)' Pyongyang: Pionguiangue Qatar: Catar - 'Qatar (wider, more serrated edges, maroon)': 'Catar (mais larga, bordas serrilhadas, marrom)' Rabat: Rabate Red Sea: Mar Vermelho Region of France.: 'Região da França.' @@ -385,10 +348,8 @@ translations: 'Reykjavík': Reiquiavique Riyadh: Riade Romania: 'Romênia (BR), Roménia (PT)' - 'Romania (slightly lighter blue)': 'Romênia (azul ligeiramente mais claro)' Rome: Roma Russia: 'Rússia' - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Rússia (sem brasão) e Eslovênia (brasão mais largo e menor)' Rwanda: Ruanda 'Réunion': 'Reunião' Sahrawi Arab Democratic Republic: 'República Árabe Saaraui Democrática (RASD)' @@ -410,7 +371,6 @@ translations: Sea of Japan: 'Mar do Japão' Sea of Okhotsk: Mar de Okhotsk Semi-autonomous region of Tanzania.: 'Região semi-autônoma da Tanzânia.' - 'Senegal (green/yellow/red, green star)': 'Senegal (verde/amarelo/vermelho, estrela verde)' Seoul: Seul Serbia: 'Sérvia' Seychelles: Seicheles @@ -420,8 +380,6 @@ translations: Sint Maarten: 'São Martinho (Países Baixos)' Skopje: 'Escópia' Slovakia: 'Eslováquia' - 'Slovakia (narrower, bigger coat of arms)': 'Eslováquia (mais estreita, brasão maior)' - 'Slovakia (with coat of arms)': 'Eslováquia (com brasão)' Slovenia: 'Eslovênia (BR), Eslovénia (PT)' Sofia: 'Sófia' Solomon Islands: 'Ilhas Salomão' @@ -449,7 +407,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Sub-região da Oceania que compreende milhares de pequenas ilhas no oeste do Oceano Pacífico.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Sub-região da Oceania que inclui Vanuatu, Ilhas Salomão, Fiji e Papua-Nova Guiné.' Sudan: 'Sudão' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudão (vermelho/branco/preto, seta verde) e República Árabe Saaraui Democrática (com estrela e crescente)' Sukhumi: Sucumi Svalbard: Esvalbarda Sweden: 'Suécia' @@ -519,27 +476,111 @@ translations: Zagreb: Zagrebe Zambia: 'Zâmbia' Zimbabwe: 'Zimbábue' + black/white/green, red arrow: preto/branco/verde, seta vermelha + blue background, red and white cross: fundo azul, cruz vermelha e branca + blue background, red cross: fundo azul, cruz vermelha + blue triangle, red stripes: 'triângulo azul, listras vermelhas' + brighter red, wider white band: vermelho mais brilhante, faixa branca mais larga + coat of arms instead of star: 'brasão de armas em vez de estrela' + darker blue: azul mais escuro + darker red, narrower white band: vermelho mais escuro, faixa branca mais estreita + different coat of arms, slightly darker blue: 'brasão diferente, azul ligeiramente mais escuro' + different coat of arms, slightly lighter blue: 'brasão diferente, azul ligeiramente mais claro' + emblem instead of text: emblema em vez de texto + green and orange flipped, narrower: verde e laranja invertidos, mais estreita + green and red flipped, slightly darker green: verde e vermelho invertidos, verde ligeiramente mais escuro + green/red/yellow, yellow star: verde/vermelho/amarelo, estrela amarela + green/yellow/red, green star: verde/amarelo/vermelho, estrela verde + lighter blue: azul mais claro + narrower, bigger coat of arms: 'mais estreita, brasão maior' + narrower, coat of arms with motto: 'mais estreita, brasão com lema' + narrower, darker red: mais estreita, vermelho mais escuro + narrower, fewer serrated edges, red: mais estreita, menos bordas serrilhadas, vermelha + no coat of arms: 'sem brasão' + no emblem: sem emblema + no symbol: 'sem símbolo' + no text: sem texto + orange and green flipped, wider: laranja e verde invertidos, mais larga + red and green flipped, slightly brighter green: vermelho e verde invertidos, verde ligeiramente mais brilhante + red and white flipped, darker red: vermelho e branco invertidos, vermelho mais escuro + red and white flipped, wider: mais larga, vermelho e branco invertidos + red background, blue and white cross: fundo vermelho, cruz azul e branca + red background, blue cross: fundo vermelho, cruz azul + red stars, two fewer stars: estrelas vermelhas, duas estrelas a menos + red triangle, blue stripes: 'triângulo vermelho, listras azuis' + red/white/black, green arrow: vermelho/branco/preto, seta verde + single star below yellow band: 'estrela única abaixo da faixa amarela' + slightly darker blue: azul ligeiramente mais escuro + slightly lighter blue: azul ligeiramente mais claro + star instead of coat of arms: 'estrela em vez de brasão' + text instead of emblem: texto em vez de emblema + two stars in top-left corner: duas estrelas no canto superior esquerdo + white and red flipped, brighter red: branco e vermelho invertidos, vermelho mais brilhante + white and red flipped, narrower: branco e vermelho invertidos, mais estreita + white background, red and blue cross: fundo branco, cruz vermelha e azul + white stars, two more stars: estrelas brancas, duas estrelas a mais + wider, brighter red: mais larga, vermelho mais brilhante + wider, coat of arms with eagle: 'mais larga, brasão com águia' + wider, more serrated edges, maroon: mais larga, bordas serrilhadas, marrom + wider, smaller coat of arms: 'brasão mais largo e menor' + with coat of arms: 'com brasão' + with emblem: com emblema + with star and crescent: com estrela e crescente + with text: com texto + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1} ({description_1}) e {country_2} ({description_2})' 'Åland Islands': Alanda contextual: notes.note: + austria.fields.field.flag-similarity.message.variables.country_1: + Latvia: 'Letônia' azores: Autonomous region of Portugal.: 'Região autônoma de Portugal.' + chad.fields.field.flag-similarity.message.variables.country_1: + Romania: 'Romênia' + egypt.fields.field.flag-similarity.message.variables.country_2: + Yemen: 'Iêmen' french-polynesia: Overseas territory of France.: 'Território ultramarino da França.' + ghana.fields.field.flag-similarity.message.format: + '{country_1} ({description_1})': ' {country_1} ({description_1})' guernsey: Crown dependency of the United Kingdom.: 'Dependência da coroa do Reino Unido.' + indonesia.fields.field.flag-similarity.message.variables.country_1: + Monaco: 'Mônaco' + indonesia.fields.field.flag-similarity.message.variables.country_2: + Poland: 'Polônia' + iraq.fields.field.flag-similarity.message.variables.country_1: + Egypt: Egito + iraq.fields.field.flag-similarity.message.variables.country_2: + Yemen: 'Iêmen' isle-of-man: Crown dependency of the United Kingdom.: Ilha de Man jersey: Crown dependency of the United Kingdom.: 'Dependência da coroa do Reino Unido.' madeira: Autonomous region of Portugal.: 'Região Autônoma de Portugal.' + monaco.fields.field.flag-similarity.message.variables.country_2: + Poland: 'Polônia' + netherlands.fields.field.flag-similarity.message.variables.country_1: + Luxembourg: do Luxemburgo new-caledonia: Overseas territory of France.: 'Território ultramarino da França.' + palestine.fields.field.flag-similarity.message.variables.country_2: + Sahrawi Arab Democratic Republic: 'República Árabe Saaraui Democrática' + poland.fields.field.flag-similarity.message.variables.country_2: + Monaco: 'Mônaco' saint-martin: Overseas territory of France.: 'País constituinte do Reino dos Países Baixos.' + slovakia.fields.field.flag-similarity.message.variables.country_2: + Slovenia: 'Eslovênia' wallis-and-futuna: Overseas territory of France.: 'Território ultramarino da França.' + yemen.fields.field.flag-similarity.message.format: + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1} ({description_1}), e {country_2} ({description_2})' + yemen.fields.field.flag-similarity.message.variables.country_1: + Egypt: Egito + yemen.fields.field.flag-similarity.message.variables.country_2: + Iraq: Iraque no_change: - Abuja - Alofi diff --git a/fixtures/ultimate-geography/overlays/languages/ru.yaml b/fixtures/ultimate-geography/overlays/languages/ru.yaml index a7f4191..7e3ebee 100644 --- a/fixtures/ultimate-geography/overlays/languages/ru.yaml +++ b/fixtures/ultimate-geography/overlays/languages/ru.yaml @@ -31,7 +31,6 @@ translations: Amman: 'Амман' Amsterdam: 'Амстердам' Andorra: 'Андорра' - 'Andorra (narrower, coat of arms with motto)': 'Андорра (уже, герб с девизом)' Andorra la Vella: 'Андорра-ла-Велья' Angola: 'Ангола' Anguilla: 'Ангилья' @@ -54,9 +53,7 @@ translations: Athens: 'Афины' Atlantic Ocean: 'Атлантический океан' Australia: 'Австралия' - 'Australia (white stars, two more stars)': 'Австралия (белые звезды, еще две звезды)' Austria: 'Австрия' - 'Austria (brighter red, wider white band)': 'Австрия (ярче красный, шире белая полоса)' Autonomous community of Spain.: 'Автономная община Испании.' Autonomous province of South Korea.: 'Автономная провинция Южной Кореи.' Autonomous region of Finland.: 'Автономная область Финляндии.' @@ -67,7 +64,6 @@ translations: Azores: 'Азорские острова' Baghdad: 'Багдад' Bahrain: 'Бахрейн' - 'Bahrain (narrower, fewer serrated edges, red)': 'Бахрейн (более узкий, с меньшим количеством зубчатых краев, красный)' Baku: 'Баку' Bali: 'Бали' Balkan Peninsula: 'Балканский полуостров' @@ -103,7 +99,6 @@ translations: Black Sea: 'Чёрное море' 'Bogotá': 'Богота' Bolivia: 'Боливия' - 'Bolivia (coat of arms instead of star)': 'Боливия (герб вместо звезды)' Bosnia and Herzegovina: 'Босния и Герцеговина' Botswana: 'Ботсвана' Bougainville: 'Бугенвиль' @@ -124,7 +119,6 @@ translations: Cairo: 'Каир' Cambodia: 'Камбоджа' Cameroon: 'Камерун' - 'Cameroon (green/red/yellow, yellow star)': 'Камерун (зеленый/красный/желтый, желтая звезда)' Canada: 'Канада' Canary Islands: 'Канарские Острова' Canberra: 'Канберра' @@ -140,7 +134,6 @@ translations: Central African Republic: 'Центральноафриканская Республика' Cetinje is an honorary capital.: 'Цетине - почетная столица.' Chad: 'Чад' - 'Chad (slightly darker blue)': 'Чад (чуть более темно-синий)' Charlotte Amalie: 'Шарлотта-Амалия' Chile: 'Чили' China: 'Китай' @@ -150,7 +143,6 @@ translations: Claimed and controlled: 'Заявленные и контролируемые' Claimed but not controlled: 'Заявлено, но не контролируется' Colombia: 'Колумбия' - 'Colombia (no coat of arms)': 'Колумбия (без герба)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Коломбо часто называют столицей, но Шри-Джаяварденепура-Котте, пригород Коломбо, является официальной, законодательной столицей.' Comoros: 'Коморские острова' Conakry: 'Конакри' @@ -163,9 +155,7 @@ translations: Costa Rica: 'Коста-Рика' Croatia: 'Хорватия' Cuba: 'Куба' - 'Cuba (red triangle, blue stripes)': 'Куба (Красный треугольник, синие полосы)' 'Curaçao': 'Кюрасао' - 'Curaçao (two stars in top-left corner)': 'Кюрасао (две звезды в левом верхнем углу)' Cyprus: 'Кипр' Czech Republic: 'Чешская Республика' Dakar: 'Дакар' @@ -188,13 +178,9 @@ translations: East China Sea: 'Восточно-Китайское море' East Siberian Sea: 'Восточно-Сибирское море' Ecuador: 'Эквадор' - 'Ecuador (with coat of arms)': 'Эквадор (с гербом)' Edinburgh: 'Эдинбург' Egypt: 'Египет' - 'Egypt (emblem instead of text), Yemen (no text)': 'Египет (эмблема вместо текста), Йемен (без текста)' - 'Egypt (with emblem), Iraq (with text)': 'Египет (с эмблемой), Ирак (с текстом)' El Salvador: 'Сальвадор' - 'El Salvador (different coat of arms, slightly darker blue)': 'Сальвадор (другой герб, чуть более темно-синий)' England: 'Англия' English Channel: 'Ла-Манш' Equatorial Guinea: 'Экваториальная Гвинея' @@ -222,7 +208,6 @@ translations: Georgia: 'Грузия' Germany: 'Германия' Ghana: 'Гана' - 'Ghana (star instead of coat of arms)': 'Гана (звезда вместо герба)' Gibraltar: 'Гибралтар' Gitega: 'Гитега' Greece: 'Греция' @@ -234,7 +219,6 @@ translations: Guatemala City: 'Гватемала' Guernsey: 'Гернси' Guinea: 'Гвинея' - 'Guinea (green and red flipped, slightly darker green)': 'Гвинея (зеленая и красная перевернутая, чуть более темно-зеленая)' Guinea-Bissau: 'Гвинея-Бисау' Gulf of Alaska: 'Залив Аляска' Gulf of California: 'Калифорнийский залив' @@ -258,28 +242,21 @@ translations: Hudson Bay: 'Гудзонов залив' Hungary: 'Венгрия' Iceland: 'Исландия' - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Исландия (синий фон, красный и белый крест), Норвегия (красный фон, синий и белый крест)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Исландия (синий фон, красный крест), Фарерские острова (Белый фон, красный и синий крест)' Independent state claimed by Georgia.: 'Независимое государство, на которое претендует Грузия.' Independent state claimed by Moldova.: 'Независимое государство, на которое претендует Молдова.' Independent state claimed by Somalia.: 'Независимое государство, на которое претендует Сомали.' India: 'Индия' Indian Ocean: 'Индийский океан' Indonesia: 'Индонезия' - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Индонезия (белый и красный перевернутый, более яркий красный), Монако (белый и красный перевернутый, более узкий)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Индонезия (шире, ярче красного), Польша (красно-белый перевернутый, шире)' Iran: 'Иран' Iraq: 'Ирак' - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Ирак (текст вместо эмблемы), Йемен (без эмблемы)' Ireland: 'Ирландия' - 'Ireland (orange and green flipped, wider)': 'Ирландия (оранжево - зеленый перевернутый, шире)' Islamabad: 'Исламабад' Island of Indonesia.: 'Остров Индонезии.' Isle of Man: 'Остров Мэн' Israel: 'Израиль' Italy: 'Италия' Ivory Coast: 'Кот-д’Ивуар' - 'Ivory Coast (green and orange flipped, narrower)': 'Кот-д''Ивуар (зеленый и оранжевый перевернутый, более узкий)' Jakarta: 'Джакарта' Jamaica: 'Ямайка' Japan: 'Япония' @@ -313,7 +290,6 @@ translations: Labrador Sea: 'Море Лабрадор' Laos: 'Лаос' Latvia: 'Латвия' - 'Latvia (darker red, narrower white band)': 'Латвия (более темная красная, более узкая белая полоса)' Lebanon: 'Ливан' Lesotho: 'Лесото' Liberia: 'Либерия' @@ -330,7 +306,6 @@ translations: Luanda: 'Луанда' Lusaka: 'Лусака' Luxembourg: 'Люксембург' - 'Luxembourg (lighter blue)': 'Люксембург (светло-голубой)' Luxembourg City: 'город Люксембург' Macau: 'Макао' Madagascar: 'Мадагаскар' @@ -341,7 +316,6 @@ translations: Malaysia: 'Малайзия' Maldives: 'Мальдивы' Mali: 'Мали' - 'Mali (red and green flipped, slightly brighter green)': 'Мали (красный и зеленый перевернуты, немного ярче зеленого)' Malta: 'Мальта' 'Malé': 'Мале' Managua: 'Манагуа' @@ -364,9 +338,7 @@ translations: Minsk: 'Минск' Mogadishu: 'Могадишо' Moldova: 'Молдова' - 'Moldova (wider, coat of arms with eagle)': 'Молдова (шире, герб с орлом)' Monaco: 'Монако' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Монако (более узкий, темно-красный), Польша (красно-белый перевернутый, темно-красный)' Mongolia: 'Монголия' Monrovia: 'Монровия' Montenegro: 'Черногория' @@ -382,20 +354,16 @@ translations: Namibia: 'Намибия' Nassau: 'Нассау' Nauru: 'Науру' - 'Nauru (single star below yellow band)': 'Науру (одиночная звезда под желтой полосой)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Науру не имеет официальной столицы; округ Ярен является фактической столицей.' Naypyidaw: 'Нейпьидо' Nepal: 'Непал' Netherlands: 'Нидерланды' - 'Netherlands (darker blue)': 'Нидерланды (темно-синий)' New Caledonia: 'Новая Каледония' New Delhi: 'Нью-Дели' New Zealand: 'Новая Зеландия' - 'New Zealand (red stars, two fewer stars)': 'Новая Зеландия (красные звезды, на две звезды меньше)' Ngerulmud: 'Нгерулмуд' Niamey: 'Ниамей' Nicaragua: 'Никарагуа' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Никарагуа (другой герб, немного более светлый синий)' Nicosia: 'Никосия' Niger: 'Нигер' Nigeria: 'Нигерия' @@ -409,7 +377,6 @@ translations: Northern Ireland: 'Северная Ирландия' Northern Mariana Islands: 'Северные Марианские Острова' Norway: 'Норвегия' - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Норвегия (красный фон, синий крест), Фарерские острова (Белый фон, красный и синий крест)' Norwegian Sea: 'Норвежское море' Not a sovereign country: 'Не суверенная страна' Nouakchott: 'Нуакшот' @@ -433,8 +400,6 @@ translations: Pakistan: 'Пакистан' Palau: 'Палау' Palestine: 'Палестина' - 'Palestine (black/white/green, red arrow)': 'Палестина (черный/белый/зеленый, красная стрелка)' - 'Palestine (no symbol)': 'Палестина (без символа)' Palikir: 'Паликир' Panama: 'Панама' Panama City: 'Панама-Сити' @@ -466,10 +431,8 @@ translations: Pretoria, Cape Town, Bloemfontein: 'Претория, Кейптаун, Блумфонтейн' Pristina: 'Приштина' Puerto Rico: 'Пуэрто-Рико' - 'Puerto Rico (blue triangle, red stripes)': 'Пуэрто-Рико (синий треугольник, красные полосы)' Pyongyang: 'Пхеньян' Qatar: 'Катар' - 'Qatar (wider, more serrated edges, maroon)': 'Катар (более широкий, с более зазубренными краями, темно-бордовый)' Quito: 'Кито' Rabat: 'Рабат' Red Sea: 'Красное море' @@ -479,11 +442,9 @@ translations: Riga: 'Рига' Riyadh: 'Эр-Рияд' Romania: 'Румыния' - 'Romania (slightly lighter blue)': 'Румыния (немного светлее синего цвета)' Rome: 'Рим' Roseau: 'Розо' Russia: 'Россия' - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Россия (без герба), Словения (шире, меньший герб)' Rwanda: 'Руанда' 'Réunion': 'Реюньон' Sahrawi Arab Democratic Republic: 'Сахарская Арабская Демократическая Республика' @@ -509,7 +470,6 @@ translations: Sea of Okhotsk: 'Охотское море' Semi-autonomous region of Tanzania.: 'Полуавтономный регион Танзании.' Senegal: 'Сенегал' - 'Senegal (green/yellow/red, green star)': 'Сенегал (зеленый/желтый/красный, зеленая звезда)' Seoul: 'Сеул' Serbia: 'Сербия' Seychelles: 'Сейшельские острова' @@ -519,8 +479,6 @@ translations: Sint Maarten: 'Синт-Мартен' Skopje: 'Скопье' Slovakia: 'Словакия' - 'Slovakia (narrower, bigger coat of arms)': 'Словакия (уже, больший герб)' - 'Slovakia (with coat of arms)': 'Словакия (с гербом)' Slovenia: 'Словения' Sofia: 'София' Solomon Islands: 'Соломоновы острова' @@ -550,7 +508,6 @@ translations: Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Субрегион Океании, в который входят Вануату, Соломоновы Острова, Папуа-Новая Гвинея и Фиджи.' Sucre: 'Сукре' Sudan: 'Судан' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Судан (красный/белый/черный, зеленая стрелка), Сахарская Арабская Демократическая Республика (со звезда и полумесяц)' Sukhumi: 'Сухуми' Sumatra: 'Суматра' Suriname: 'Суринам' @@ -641,6 +598,57 @@ translations: Zambia: 'Замбия' Zanzibar: 'Занзибар' Zimbabwe: 'Зимбабве' + black/white/green, red arrow: 'черный/белый/зеленый, красная стрелка' + blue background, red and white cross: 'синий фон, красный и белый крест' + blue background, red cross: 'синий фон, красный крест' + blue triangle, red stripes: 'синий треугольник, красные полосы' + brighter red, wider white band: 'ярче красный, шире белая полоса' + coat of arms instead of star: 'герб вместо звезды' + darker blue: 'темно-синий' + darker red, narrower white band: 'более темная красная, более узкая белая полоса' + different coat of arms, slightly darker blue: 'другой герб, чуть более темно-синий' + different coat of arms, slightly lighter blue: 'другой герб, немного более светлый синий' + emblem instead of text: 'эмблема вместо текста' + green and orange flipped, narrower: 'зеленый и оранжевый перевернутый, более узкий' + green and red flipped, slightly darker green: 'зеленая и красная перевернутая, чуть более темно-зеленая' + green/red/yellow, yellow star: 'зеленый/красный/желтый, желтая звезда' + green/yellow/red, green star: 'зеленый/желтый/красный, зеленая звезда' + lighter blue: 'светло-голубой' + narrower, bigger coat of arms: 'уже, больший герб' + narrower, coat of arms with motto: 'уже, герб с девизом' + narrower, darker red: 'более узкий, темно-красный' + narrower, fewer serrated edges, red: 'более узкий, с меньшим количеством зубчатых краев, красный' + no coat of arms: 'без герба' + no emblem: 'без эмблемы' + no symbol: 'без символа' + no text: 'без текста' + orange and green flipped, wider: 'оранжево - зеленый перевернутый, шире' + red and green flipped, slightly brighter green: 'красный и зеленый перевернуты, немного ярче зеленого' + red and white flipped, darker red: 'красно-белый перевернутый, темно-красный' + red and white flipped, wider: 'красно-белый перевернутый, шире' + red background, blue and white cross: 'красный фон, синий и белый крест' + red background, blue cross: 'красный фон, синий крест' + red stars, two fewer stars: 'красные звезды, на две звезды меньше' + red triangle, blue stripes: 'Красный треугольник, синие полосы' + red/white/black, green arrow: 'красный/белый/черный, зеленая стрелка' + single star below yellow band: 'одиночная звезда под желтой полосой' + slightly darker blue: 'чуть более темно-синий' + slightly lighter blue: 'немного светлее синего цвета' + star instead of coat of arms: 'звезда вместо герба' + text instead of emblem: 'текст вместо эмблемы' + two stars in top-left corner: 'две звезды в левом верхнем углу' + white and red flipped, brighter red: 'белый и красный перевернутый, более яркий красный' + white and red flipped, narrower: 'белый и красный перевернутый, более узкий' + white background, red and blue cross: 'Белый фон, красный и синий крест' + white stars, two more stars: 'белые звезды, еще две звезды' + wider, brighter red: 'шире, ярче красного' + wider, coat of arms with eagle: 'шире, герб с орлом' + wider, more serrated edges, maroon: 'более широкий, с более зазубренными краями, темно-бордовый' + wider, smaller coat of arms: 'шире, меньший герб' + with coat of arms: 'с гербом' + with emblem: 'с эмблемой' + with star and crescent: 'со звезда и полумесяц' + with text: 'с текстом' 'Åland Islands': 'Аландские острова' contextual: notes.note: @@ -664,6 +672,8 @@ translations: Overseas territory of the United Kingdom.: 'Заморская территория Великобритании.' guernsey: Crown dependency of the United Kingdom.: 'Зависимость короны от Соединенного Королевства.' + ireland.fields.field.flag-similarity.message.variables.country_1: + Ivory Coast: 'Кот-д''Ивуар' isle-of-man: Crown dependency of the United Kingdom.: 'Зависимость Соединенного Королевства от короны.' jersey: diff --git a/fixtures/ultimate-geography/overlays/languages/sv.yaml b/fixtures/ultimate-geography/overlays/languages/sv.yaml index 3748404..79aa951 100644 --- a/fixtures/ultimate-geography/overlays/languages/sv.yaml +++ b/fixtures/ultimate-geography/overlays/languages/sv.yaml @@ -22,7 +22,6 @@ translations: Also known as the Sea of Cortez.: 'Även känt som Cortez hav.' 'Also spelled as Sana''a.': '' American Samoa: Amerikanska Samoa - 'Andorra (narrower, coat of arms with motto)': 'Andorra (smalare, vapensköld med valspråk)' Antarctica: Antarktis Antigua and Barbuda: Antigua och Barbuda Arabian Sea: Arabiska havet @@ -34,9 +33,7 @@ translations: Athens: Aten Atlantic Ocean: Atlanten Australia: Australien - 'Australia (white stars, two more stars)': 'Australien (vita stjärnor, två fler stjärnor)' Austria: 'Österrike' - 'Austria (brighter red, wider white band)': 'Österrike (ljusare röd, bredare vitt band)' Autonomous community of Spain.: Spansk autonom region. Autonomous province of South Korea.: 'Autonom region i Sydkorea; tidigare känd som Quelpart.' Autonomous region of Finland.: 'Självstyrande del av Finland.' @@ -45,7 +42,6 @@ translations: Azerbaijan: Azerbajdzjan Azores: Azorerna Baghdad: Bagdad - 'Bahrain (narrower, fewer serrated edges, red)': 'Bahrain (smalare, färre tänder, röd)' Balkan Peninsula: 'Balkanhalvön' Baltic Sea: 'Östersjön' Banda Sea: 'Bandasjön' @@ -58,7 +54,6 @@ translations: Bering Strait: Berings sund Bishkek: Bisjkek Black Sea: Svarta havet - 'Bolivia (coat of arms instead of star)': 'Bolivia (vapensköld istället för stjärna)' Bosnia and Herzegovina: Bosnien och Hercegovina Brazil: Brasilien British Virgin Islands: 'Brittiska Jungfruöarna' @@ -68,7 +63,6 @@ translations: Cairo: Kairo Cambodia: Kambodja Cameroon: Kamerun - 'Cameroon (green/red/yellow, yellow star)': 'Kamerun (grön/röd/gul, gul stjärna)' Canada: Kanada Canary Islands: 'Kanarieöarna' Cape Verde: Kap Verde @@ -80,12 +74,10 @@ translations: Central African Republic: Centralafrikanska republiken Cetinje is an honorary capital.: 'Cetinje är hedershuvudstad.' Chad: Tchad - 'Chad (slightly darker blue)': 'Tchad (aningen mörkare blå)' China: Kina City of San Marino: San Marino Claimed and controlled: 'Hävdas och kontrolleras' Claimed but not controlled: 'Påstått men inte kontrollerat' - 'Colombia (no coat of arms)': 'Colombia (ingen vapensköld)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: 'Kallas även Kotte eller Sri Jayawardenapura Kotte; Colombo refereras ofta till som huvudstad, men förorten Sri Jayawardenapura är officiell huvudstad.' Comoros: Komorerna Constituent country in the Kingdom of Denmark.: Riksdel av Danmark. @@ -98,8 +90,6 @@ translations: Croatia: Kroatien Crown dependency of the United Kingdom.: Brittisk kronbesittning. Cuba: Kuba - 'Cuba (red triangle, blue stripes)': 'Kuba (röd triangel, blå ränder)' - 'Curaçao (two stars in top-left corner)': 'Curaçao (två stjärnor i övre vänstra hörnet)' Cyprus: Cypern Czech Republic: Tjeckien Damascus: Damaskus @@ -113,11 +103,7 @@ translations: Dushanbe: Dusjanbe East China Sea: 'Östkinesiska havet' East Siberian Sea: 'Östsibiriska havet' - 'Ecuador (with coat of arms)': 'Ecuador (med vapensköld)' Egypt: Egypten - 'Egypt (emblem instead of text), Yemen (no text)': 'Egypten (statsvapen istället för text), Jemen (ingen text)' - 'Egypt (with emblem), Iraq (with text)': 'Egypten (med emblem), Irak (med text)' - 'El Salvador (different coat of arms, slightly darker blue)': 'El Salvador (annan vapensköld, aningen mörkare blå)' English Channel: Engelska kanalen Equatorial Guinea: Ekvatorialguinea Estonia: Estland @@ -135,10 +121,8 @@ translations: French Polynesia: Franska Polynesien Georgia: Georgien Germany: Tyskland - 'Ghana (star instead of coat of arms)': 'Ghana (stjärnor istället för vapensköld)' Greece: Grekland Greenland: 'Grönland' - 'Guinea (green and red flipped, slightly darker green)': 'Guinea (grön och röd växlade, aningen mörkare grön)' Gulf of Alaska: Alaskagolfen Gulf of California: Californiaviken Gulf of Carpentaria: Carpentariaviken @@ -151,24 +135,17 @@ translations: Hong Kong: Hongkong Hungary: Ungern Iceland: Island - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': 'Island (blå bakgrund, rött och vitt kors), Norge (röd bakgrund, blått och vitt kors)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': 'Island (blå bakgrund, rött kors), Färöarna (vit bakgrund, rött och blått kors)' Independent state claimed by Georgia.: 'Delvis erkänd stat, görs anspråk på av Georgien.' Independent state claimed by Moldova.: 'Självutropad stat, görs anspråk på av Moldavien.' Independent state claimed by Somalia.: 'Självutropad stat, görs anspråk på av Somalien.' India: Indien Indian Ocean: Indiska oceanen Indonesia: Indonesien - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': 'Indonesien (vit och röd växlade, ljusare röd), Monaco (vit och röd växlade, smalare)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': 'Indonesien (bredare, ljusare röd), Polen (röd och vit växlade, bredare)' Iraq: Irak - 'Iraq (text instead of emblem), Yemen (no emblem)': 'Irak (text istället för statsvapen), Jemen (inget statsvapen)' Ireland: Irland - 'Ireland (orange and green flipped, wider)': 'Irland (orange och grön växlade, bredare)' Island of Indonesia.: 'Ö i Indonesien.' Italy: Italien Ivory Coast: Elfenbenskusten - 'Ivory Coast (green and orange flipped, narrower)': 'Elfenbenskusten (grön och orange växlade, smalare)' Jordan: Jordanien Kaliningrad Oblast: Kaliningrad oblast Kathmandu: Katmandu @@ -181,18 +158,15 @@ translations: Laayoune: al-Ayun Labrador Sea: Labradorhavet Latvia: Lettland - 'Latvia (darker red, narrower white band)': 'Lettland (mörkare röd, smalare vitt band)' Lebanon: Libanon Libya: Libyen Lisbon: Lissabon Lithuania: Litauen Luxembourg: Luxemburg - 'Luxembourg (lighter blue)': 'Luxemburg (ljusare blå)' Luxembourg City: Luxemburg Macau: Macao Madagascar: Madagaskar Maldives: Maldiverna - 'Mali (red and green flipped, slightly brighter green)': 'Mali (röd och grön växlade, aningen ljusare grön)' Marshall Islands: 'Marshallöarna' Mauritania: Mauretanien Mediterranean Sea: Medelhavet @@ -200,21 +174,15 @@ translations: Mexico: Mexiko Micronesia: Mikronesien Moldova: Moldavien - 'Moldova (wider, coat of arms with eagle)': 'Moldavien (bredare, vapensköld med örn)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': 'Monaco (smalare, mörkare röd), Polen (röd och vit växlade, mörkare röd)' Mongolia: Mongoliet Morocco: Marocko Moscow: Moskva Mozambique: 'Moçambique' Muscat: Muskat - 'Nauru (single star below yellow band)': 'Nauru (ensam stjärna under den gula randen)' 'Nauru has no official capital; the Yaren District is the de facto capital.': 'Nauru saknar städer, Yaren är de facto huvudort.' Netherlands: 'Nederländerna' - 'Netherlands (darker blue)': 'Nederländerna (mörkare blå)' New Caledonia: Nya Kaledonien New Zealand: Nya Zeeland - 'New Zealand (red stars, two fewer stars)': 'Nya Zeeland (röda stjärnor, två färre stjärnor)' - 'Nicaragua (different coat of arms, slightly lighter blue)': 'Nicaragua (annan vapensköld, aningen ljusare blå)' North America: Nordamerika North Korea: Nordkorea North Macedonia: Nordmakedonien @@ -224,7 +192,6 @@ translations: Northern Ireland: Nordirland Northern Mariana Islands: Nordmarianerna Norway: Norge - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': 'Norge (röd bakgrund, blått kors), Färöarna (vit bakgrund, rött och blått kors)' Norwegian Sea: Norska havet Not a sovereign country: 'Ej suverän stat' 'Nukuʻalofa': 'Nuku''alofa' @@ -236,8 +203,6 @@ translations: Officially Luxembourg.: '' Pacific Ocean: Stilla havet Palestine: Palestina - 'Palestine (black/white/green, red arrow)': 'Palestina (svart/vit/grön, röd triangel)' - 'Palestine (no symbol)': 'Palestina (ingen symbol)' Papua New Guinea: Papua Nya Guinea Partially recognised state claimed by China.: 'Delvis erkänd stat, görs anspråk på av Kina.' Partially recognised state claimed by Morocco. Also known as Western Sahara.: 'Även känt som Västsahariska republiken.' @@ -249,17 +214,13 @@ translations: Polynesia: Polynesien Prague: Prag Pretoria, Cape Town, Bloemfontein: Pretoria, Kapstaden, Bloemfontein - 'Puerto Rico (blue triangle, red stripes)': 'Puerto Rico (blå triangel, röda ränder)' - 'Qatar (wider, more serrated edges, maroon)': 'Qatar (bredare, fler tänder, rödbrun)' Red Sea: 'Röda havet' Region of France.: Fransk region. Republic of the Congo: Kongo-Brazzaville 'Reykjavík': Reykjavik Romania: 'Rumänien' - 'Romania (slightly lighter blue)': 'Rumänien (aningen ljusare blå)' Rome: Rom Russia: Ryssland - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': 'Ryssland (ingen vapensköld), Slovenien (bredare, mindre vapensköld)' Sahrawi Arab Democratic Republic: Sahariska arabiska demokratiska republiken Saint Kitts and Nevis: Saint Kitts och Nevis Saint Vincent and the Grenadines: Saint Vincent och Grenadinerna @@ -272,13 +233,10 @@ translations: Sea of Japan: Japanska havet Sea of Okhotsk: Ochotska havet Semi-autonomous region of Tanzania.: 'Delvis självstyrande örike i union med Tanzania.' - 'Senegal (green/yellow/red, green star)': 'Senegal (grön/gul/röd, grön stjärna)' Serbia: Serbien Seychelles: Seychellerna Sicily: Sicilien Slovakia: Slovakien - 'Slovakia (narrower, bigger coat of arms)': 'Slovakien (smalare, större vapensköld)' - 'Slovakia (with coat of arms)': 'Slovakien (med vapensköld)' Slovenia: Slovenien Solomon Islands: 'Salomonöarna' South Africa: Sydafrika @@ -299,7 +257,6 @@ translations: Subregion of Oceania comprising thousands of small islands in the central and southern Pacific Ocean.: 'Delregion av Oceanien bestående av tusentals småöar i centrala och södra Stilla havet.' Subregion of Oceania comprising thousands of small islands in the western Pacific Ocean.: 'Delregion av Oceanien bestående av tusentals småöar i västra Stilla havet.' Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: 'Delregion av Oceanien bestående av bland andra Vanuatu, Salomonöarna, Fiji och Papua Nya Guinea.' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': 'Sudan (röd/vit/svart, grön triangel), Sahariska arabiska demokratiska republiken (med stjärna och halvmåne)' Sukhumi: Suchumi Suriname: Surinam Sweden: Sverige @@ -344,6 +301,57 @@ translations: Yellow Sea: Gula havet Yemen: Jemen Yerevan: Jerevan + black/white/green, red arrow: 'svart/vit/grön, röd triangel' + blue background, red and white cross: 'blå bakgrund, rött och vitt kors' + blue background, red cross: 'blå bakgrund, rött kors' + blue triangle, red stripes: 'blå triangel, röda ränder' + brighter red, wider white band: 'ljusare röd, bredare vitt band' + coat of arms instead of star: 'vapensköld istället för stjärna' + darker blue: 'mörkare blå' + darker red, narrower white band: 'mörkare röd, smalare vitt band' + different coat of arms, slightly darker blue: 'annan vapensköld, aningen mörkare blå' + different coat of arms, slightly lighter blue: 'annan vapensköld, aningen ljusare blå' + emblem instead of text: 'statsvapen istället för text' + green and orange flipped, narrower: 'grön och orange växlade, smalare' + green and red flipped, slightly darker green: 'grön och röd växlade, aningen mörkare grön' + green/red/yellow, yellow star: 'grön/röd/gul, gul stjärna' + green/yellow/red, green star: 'grön/gul/röd, grön stjärna' + lighter blue: 'ljusare blå' + narrower, bigger coat of arms: 'smalare, större vapensköld' + narrower, coat of arms with motto: 'smalare, vapensköld med valspråk' + narrower, darker red: 'smalare, mörkare röd' + narrower, fewer serrated edges, red: 'smalare, färre tänder, röd' + no coat of arms: 'ingen vapensköld' + no emblem: inget statsvapen + no symbol: ingen symbol + no text: ingen text + orange and green flipped, wider: 'orange och grön växlade, bredare' + red and green flipped, slightly brighter green: 'röd och grön växlade, aningen ljusare grön' + red and white flipped, darker red: 'röd och vit växlade, mörkare röd' + red and white flipped, wider: 'röd och vit växlade, bredare' + red background, blue and white cross: 'röd bakgrund, blått och vitt kors' + red background, blue cross: 'röd bakgrund, blått kors' + red stars, two fewer stars: 'röda stjärnor, två färre stjärnor' + red triangle, blue stripes: 'röd triangel, blå ränder' + red/white/black, green arrow: 'röd/vit/svart, grön triangel' + single star below yellow band: 'ensam stjärna under den gula randen' + slightly darker blue: 'aningen mörkare blå' + slightly lighter blue: 'aningen ljusare blå' + star instead of coat of arms: 'stjärnor istället för vapensköld' + text instead of emblem: 'text istället för statsvapen' + two stars in top-left corner: 'två stjärnor i övre vänstra hörnet' + white and red flipped, brighter red: 'vit och röd växlade, ljusare röd' + white and red flipped, narrower: 'vit och röd växlade, smalare' + white background, red and blue cross: 'vit bakgrund, rött och blått kors' + white stars, two more stars: 'vita stjärnor, två fler stjärnor' + wider, brighter red: 'bredare, ljusare röd' + wider, coat of arms with eagle: 'bredare, vapensköld med örn' + wider, more serrated edges, maroon: 'bredare, fler tänder, rödbrun' + wider, smaller coat of arms: 'bredare, mindre vapensköld' + with coat of arms: 'med vapensköld' + with emblem: med emblem + with star and crescent: 'med stjärna och halvmåne' + with text: med text 'Åland Islands': 'Åland' contextual: notes.note: diff --git a/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml b/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml index f7cdf0e..5abc530 100644 --- a/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml +++ b/fixtures/ultimate-geography/overlays/languages/zh-tw.yaml @@ -31,7 +31,6 @@ translations: Amman: '安曼 (Amman)' Amsterdam: '阿姆斯特丹 (Amsterdam)' Andorra: '安道爾 (Andorra)' - 'Andorra (narrower, coat of arms with motto)': '安道爾(更窄、徽章)' Andorra la Vella: '安道爾城 (Andorra la Vella)' Angola: '安哥拉 (Angola)' Anguilla: '安奎拉 (Anguilla)' @@ -54,9 +53,7 @@ translations: Athens: '雅典 (Athens)' Atlantic Ocean: '大西洋 (Atlantic Ocean)' Australia: '澳大利亞 (Australia)' - 'Australia (white stars, two more stars)': '澳洲(白色星星、多兩顆星星)' Austria: '奧地利 (Austria)' - 'Austria (brighter red, wider white band)': '奧地利(更淺紅色、白線較寬)' Autonomous community of Spain.: '西班牙的自治區。' Autonomous province of South Korea.: '南韓的自治區。' Autonomous region of Finland.: '芬蘭的自治區。' @@ -68,7 +65,6 @@ translations: Azores: '亞速群島 (Azores)' Baghdad: '巴格達 (Baghdad)' Bahrain: '巴林 (Bahrain)' - 'Bahrain (narrower, fewer serrated edges, red)': '巴林(更窄、更少鋸齒狀邊緣、紅色)' Baku: '巴庫 (Baku)' Bali: '峇里島 (Bali)' Balkan Peninsula: '巴爾幹半島 (Balkan Peninsula)' @@ -104,7 +100,6 @@ translations: Black Sea: '黑海 (Black Sea)' 'Bogotá': '波哥大 (Bogotá)' Bolivia: '玻利維亞 (Bolivia)' - 'Bolivia (coat of arms instead of star)': '波利維亞(徽章、非星星)' Bosnia and Herzegovina: '波士尼亞與赫塞哥維納 (Bosnia and Herzegovina)' Botswana: '波札那 (Botswana)' Bougainville: '布干維爾島 (Bougainville)' @@ -125,7 +120,6 @@ translations: Cairo: '開羅 (Cairo)' Cambodia: '柬埔寨 (Cambodia)' Cameroon: '喀麥隆共和國 (Cameroon)' - 'Cameroon (green/red/yellow, yellow star)': '喀麥隆(綠紅黃、黃色星星)' Canada: '加拿大 (Canada)' Canary Islands: '加納利群島 (Canary Islands)' Canberra: '坎培拉 (Canberra)' @@ -141,7 +135,6 @@ translations: Central African Republic: '中非共和國 (Central African Republic)' Cetinje is an honorary capital.: '策提涅是歷史首都。' Chad: '查德 (Chad)' - 'Chad (slightly darker blue)': '查德(稍更深藍)' Charlotte Amalie: '夏洛特阿馬利亞 (Charlotte Amalie)' Chile: '智利 (Chile)' China: '中國 (China)' @@ -151,7 +144,6 @@ translations: Claimed and controlled: '宣稱擁有主權並有實際控制權' Claimed but not controlled: '宣稱擁有主權但沒有實際控制權' Colombia: '哥倫比亞 (Colombia)' - 'Colombia (no coat of arms)': '哥倫比亞(無徽章)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: '雖然可倫坡被稱為首都,但"斯里賈亞瓦德納普拉科特"是正式與立法的首都。' Comoros: '葛摩聯盟 (Comoros)' Conakry: '柯那克里 (Conakry)' @@ -166,9 +158,7 @@ translations: Croatia: '克羅埃西亞 (Croatia)' Crown dependency of the United Kingdom.: '英國皇家屬地。' Cuba: '古巴 (Cuba)' - 'Cuba (red triangle, blue stripes)': '古巴(紅色三角、紅條紋)' 'Curaçao': '庫拉索 (Curaçao)' - 'Curaçao (two stars in top-left corner)': '庫拉索(左上角有兩顆星星)' Cyprus: '賽普勒斯 (Cyprus)' Czech Republic: '捷克 (Czech Republic)' Dakar: '達卡 (Dakar)' @@ -190,13 +180,9 @@ translations: East China Sea: '東海 (East China Sea)' East Siberian Sea: '東西伯利亞海 (East Siberian Sea)' Ecuador: '厄瓜多 (Ecuador)' - 'Ecuador (with coat of arms)': '厄瓜多(有徽章)' Edinburgh: '愛丁堡 (Edinburgh)' Egypt: '埃及 (Egypt)' - 'Egypt (emblem instead of text), Yemen (no text)': '埃及(徽章、非文字)、葉門(無文字)' - 'Egypt (with emblem), Iraq (with text)': '埃及(徽章)、伊拉克(文字)' El Salvador: '薩爾瓦多 (El Salvador)' - 'El Salvador (different coat of arms, slightly darker blue)': '薩爾瓦多(不同徽章、稍深藍)' England: '英格蘭 (England)' English Channel: '英吉利海峽 (English Channel)' Equatorial Guinea: '赤道幾內亞 (Equatorial Guinea)' @@ -224,7 +210,6 @@ translations: Georgia: '喬治亞 (Georgia)' Germany: '格國 (Germany)' Ghana: '迦納 (Ghana)' - 'Ghana (star instead of coat of arms)': '迦納(星星、非徽章)' Gibraltar: '直布羅陀 (Gibraltar)' Gitega: '吉特加 (Gitega)' Greece: '希臘 (Greece)' @@ -236,7 +221,6 @@ translations: Guatemala City: '瓜地馬拉市 (Guatemala City)' Guernsey: '根西 (Guernsey)' Guinea: '幾內亞 (Guinea)' - 'Guinea (green and red flipped, slightly darker green)': '圭亞那(綠紅對調、稍微深綠)' Guinea-Bissau: '幾內亞比索共和國 (Guinea-Bissau)' Gulf of Alaska: '阿拉斯加灣 (Gulf of Alaska)' Gulf of California: '加利福尼亞灣 (Gulf of California)' @@ -260,27 +244,20 @@ translations: Hudson Bay: '哈德遜灣 (Hudson Bay)' Hungary: '匈牙利 (Hungary)' Iceland: '冰島 (Iceland)' - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': '冰島(藍底、紅白十字)、挪威(紅底、藍白十字)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': '冰島(藍底、紅十字)、法羅群島(白底、紅藍十字)' Independent state claimed by Moldova.: '摩爾多瓦宣稱擁有該地區主權,但目前是獨立狀態。' Independent state claimed by Somalia.: '索馬利亞宣稱擁有該地區主權,但目前是獨立狀態。' India: '印度 (India)' Indian Ocean: '印度洋 (Indian Ocean)' Indonesia: '印度尼西亞(印尼) (Indonesia)' - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': '印尼(白紅對調、更多紅色)、摩納哥(白紅對調、更窄)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': '印尼(更寬、更淺紅)、波蘭(紅白對調、更寬)' Iran: '伊朗 (Iran)' Iraq: '伊拉克 (Iraq)' - 'Iraq (text instead of emblem), Yemen (no emblem)': '伊拉克(文字、無徽章)、葉門(無徽章)' Ireland: '愛爾蘭 (Ireland)' - 'Ireland (orange and green flipped, wider)': '愛爾蘭(橘綠對調、更寬)' Islamabad: '伊斯蘭馬巴德 (Islamabad)' Island of Indonesia.: '印尼的島嶼。' Isle of Man: '曼島 (Isle of Man)' Israel: '以色列 (Israel)' Italy: '義大利 (Italy)' Ivory Coast: '象牙海岸 (Ivory Coast)' - 'Ivory Coast (green and orange flipped, narrower)': '象牙海岸(綠橘對調、更窄)' Jakarta: '雅加達 (Jakarta)' Jamaica: '牙買加 (Jamaica)' Japan: '日本 (Japan)' @@ -314,7 +291,6 @@ translations: Labrador Sea: '拉布拉多海 (Labrador Sea)' Laos: '寮國 (Laos)' Latvia: '拉脫維亞 (Latvia)' - 'Latvia (darker red, narrower white band)': '拉脫維亞(更深紅、窄白線)' Lebanon: '黎巴嫩 (Lebanon)' Lesotho: '賴索托 (Lesotho)' Liberia: '賴比瑞亞共和國 (Liberia)' @@ -331,7 +307,6 @@ translations: Luanda: '盧安達 (Luanda)' Lusaka: '路沙卡 (Lusaka)' Luxembourg: '盧森堡 (Luxembourg)' - 'Luxembourg (lighter blue)': '盧森堡(更淺藍)' Luxembourg City: '盧森堡市 (Luxembourg City)' Macau: '澳門 (Macau)' Madagascar: '馬達加斯加 (Madagascar)' @@ -342,7 +317,6 @@ translations: Malaysia: '馬來西亞 (Malaysia)' Maldives: '馬爾地夫 (Maldives)' Mali: '馬利 (Mali)' - 'Mali (red and green flipped, slightly brighter green)': '馬利(紅綠對調、更淺綠)' Malta: '馬爾他 (Malta)' 'Malé': '馬列市 (Malé)' Managua: '馬納瓜 (Managua)' @@ -365,9 +339,7 @@ translations: Minsk: '明斯克 (Minsk)' Mogadishu: '摩加迪休 (Mogadishu)' Moldova: '摩爾多瓦 (Moldova)' - 'Moldova (wider, coat of arms with eagle)': '摩爾多瓦(更寬、老鷹)' Monaco: '摩納哥 (Monaco)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': '摩納哥(更窄、更深紅)、波蘭(紅白對調、更深紅)' Mongolia: '蒙古 (Mongolia)' Monrovia: '蒙羅維亞 (Monrovia)' Montenegro: '蒙特內哥羅 (Montenegro)' @@ -383,20 +355,16 @@ translations: Namibia: '納米比亞共和國 (Namibia)' Nassau: '拿騷 (Nassau)' Nauru: '諾魯共和國 (Nauru)' - 'Nauru (single star below yellow band)': '諾魯(僅一顆星星於黃線下)' 'Nauru has no official capital; the Yaren District is the de facto capital.': '諾魯沒有法定首都,但亞倫是實際首都。' Naypyidaw: '奈比都 (Naypyidaw)' Nepal: '尼泊爾 (Nepal)' Netherlands: '荷蘭 (Netherlands)' - 'Netherlands (darker blue)': '荷蘭(更深藍)' New Caledonia: '新喀里多尼亞 (New Caledonia)' New Delhi: '新德里 (New Delhi)' New Zealand: '紐西蘭 (New Zealand)' - 'New Zealand (red stars, two fewer stars)': '紐西蘭(紅色星星、少兩顆星星)' Ngerulmud: '恩吉魯穆德 (Ngerulmud)' Niamey: '尼阿美 (Niamey)' Nicaragua: '尼加拉瓜 (Nicaragua)' - 'Nicaragua (different coat of arms, slightly lighter blue)': '尼加拉瓜(不同徽章、稍淺藍)' Nicosia: '尼克西亞 (Nicosia)' Niger: '尼日 (Niger)' Nigeria: '奈及利亞 (Nigeria)' @@ -410,7 +378,6 @@ translations: Northern Ireland: '北愛爾蘭 (Northern Ireland)' Northern Mariana Islands: '北馬里亞納群島 (Northern Mariana Islands)' Norway: '挪威 (Norway)' - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': '挪威(紅底、藍十字)' Norwegian Sea: '挪威海 (Norwegian Sea)' Not a sovereign country: '不是主權國家' Nouakchott: '諾克少 (Nouakchott)' @@ -435,8 +402,6 @@ translations: Pakistan: '巴基斯坦 (Pakistan)' Palau: '帛琉 (Palau)' Palestine: '巴勒斯坦 (Palestine)' - 'Palestine (black/white/green, red arrow)': '巴勒斯坦(黑白綠、紅色箭頭)' - 'Palestine (no symbol)': '巴勒斯坦(無標誌)' Palikir: '帕里克爾 (Palikir)' Panama: '巴拿馬 (Panama)' Panama City: '巴拿馬城 (Panama City)' @@ -468,10 +433,8 @@ translations: Pretoria, Cape Town, Bloemfontein: '普利托利亞、開普敦、布隆泉 (Pretoria, Cape Town, Bloemfontein)' Pristina: '普里斯提納 (Pristina)' Puerto Rico: '波多黎各 (Puerto Rico)' - 'Puerto Rico (blue triangle, red stripes)': '波多黎各(藍三角、紅條紋)' Pyongyang: '平壤 (Pyongyang)' Qatar: '卡達 (Qatar)' - 'Qatar (wider, more serrated edges, maroon)': '卡達(更寬、更多鋸齒狀邊緣、栗紅色)' Quito: '基多市 (Quito)' Rabat: '拉巴特 (Rabat)' Red Sea: '紅海 (Red Sea)' @@ -481,11 +444,9 @@ translations: Riga: '里加 (Riga)' Riyadh: '利雅德 (Riyadh)' Romania: '羅馬尼亞 (Romania)' - 'Romania (slightly lighter blue)': '羅馬尼亞(稍微淺藍)' Rome: '羅馬 (Rome)' Roseau: '羅索 (Roseau)' Russia: '俄羅斯 (Russia)' - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': '俄羅斯(沒徽章)、斯洛維尼亞(更寬、更小的徽章)' Rwanda: '盧安達 (Rwanda)' 'Réunion': '留尼旺島 (Réunion)' Sahrawi Arab Democratic Republic: '撒拉威阿拉伯民主共和國(西薩哈拉) (Sahrawi Arab Democratic Republic)' @@ -511,7 +472,6 @@ translations: Sea of Okhotsk: '鄂霍次克海 (Sea of Okhotsk)' Semi-autonomous region of Tanzania.: '坦尚尼亞的半自治區。' Senegal: '塞內加爾 (Senegal)' - 'Senegal (green/yellow/red, green star)': '塞內加爾(綠黃紅、綠色星星)' Seoul: '首爾 (Seoul)' Serbia: '塞爾維亞 (Serbia)' Seychelles: '塞席爾共和國 (Seychelles)' @@ -521,8 +481,6 @@ translations: Sint Maarten: '荷屬聖馬丁 (Sint Maarten)' Skopje: '斯科普里 (Skopje)' Slovakia: '斯洛伐克 (Slovakia)' - 'Slovakia (narrower, bigger coat of arms)': '斯洛伐克(更窄、更大的徽章)' - 'Slovakia (with coat of arms)': '斯洛伐克(有徽章)' Slovenia: '斯洛維尼亞 (Slovenia)' Sofia: '索菲亞 (Sofia)' Solomon Islands: '索羅門群島 (Solomon Islands)' @@ -552,7 +510,6 @@ translations: Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: '大洋洲次區域,包括萬那杜、所羅門群島、斐濟和巴布亞紐幾內亞。' Sucre: '蘇克瑞 (Sucre)' Sudan: '蘇丹 (Sudan)' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': '蘇丹(紅白黑、綠色箭頭)、西撒哈拉(星星與新月)' Sukhumi: '蘇呼米 (Sukhumi)' Sumatra: '蘇門答臘 (Sumatra)' Suriname: '蘇利南 (Suriname)' @@ -643,17 +600,180 @@ translations: Zambia: '桑比亞 (Zambia)' Zanzibar: '尚吉巴 (Zanzibar)' Zimbabwe: '辛巴威 (Zimbabwe)' + black/white/green, red arrow: '黑白綠、紅色箭頭' + blue background, red and white cross: '藍底、紅白十字' + blue background, red cross: '藍底、紅十字' + blue triangle, red stripes: '藍三角、紅條紋' + brighter red, wider white band: '更淺紅色、白線較寬' + coat of arms instead of star: '徽章、非星星' + darker blue: '更深藍' + darker red, narrower white band: '更深紅、窄白線' + different coat of arms, slightly darker blue: '不同徽章、稍深藍' + different coat of arms, slightly lighter blue: '不同徽章、稍淺藍' + emblem instead of text: '徽章、非文字' + green and orange flipped, narrower: '綠橘對調、更窄' + green and red flipped, slightly darker green: '綠紅對調、稍微深綠' + green/red/yellow, yellow star: '綠紅黃、黃色星星' + green/yellow/red, green star: '綠黃紅、綠色星星' + lighter blue: '更淺藍' + narrower, bigger coat of arms: '更窄、更大的徽章' + narrower, coat of arms with motto: '更窄、徽章' + narrower, darker red: '更窄、更深紅' + narrower, fewer serrated edges, red: '更窄、更少鋸齒狀邊緣、紅色' + no coat of arms: '無徽章' + no emblem: '無徽章' + no symbol: '無標誌' + no text: '無文字' + orange and green flipped, wider: '橘綠對調、更寬' + red and green flipped, slightly brighter green: '紅綠對調、更淺綠' + red and white flipped, darker red: '紅白對調、更深紅' + red and white flipped, wider: '紅白對調、更寬' + red background, blue and white cross: '紅底、藍白十字' + red background, blue cross: '紅底、藍十字' + red stars, two fewer stars: '紅色星星、少兩顆星星' + red triangle, blue stripes: '紅色三角、紅條紋' + red/white/black, green arrow: '紅白黑、綠色箭頭' + single star below yellow band: '僅一顆星星於黃線下' + slightly darker blue: '稍更深藍' + slightly lighter blue: '稍微淺藍' + star instead of coat of arms: '星星、非徽章' + text instead of emblem: '文字、無徽章' + two stars in top-left corner: '左上角有兩顆星星' + white and red flipped, brighter red: '白紅對調、更多紅色' + white and red flipped, narrower: '白紅對調、更窄' + white background, red and blue cross: '白底、紅藍十字' + white stars, two more stars: '白色星星、多兩顆星星' + wider, brighter red: '更寬、更淺紅' + wider, coat of arms with eagle: '更寬、老鷹' + wider, more serrated edges, maroon: '更寬、更多鋸齒狀邊緣、栗紅色' + wider, smaller coat of arms: '更寬、更小的徽章' + with coat of arms: '有徽章' + with emblem: '徽章' + with star and crescent: '星星與新月' + with text: '文字' + '{country_1} ({description_1})': '{country_1}({description_1})' + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1}({description_1})、{country_2}({description_2})' 'Åland Islands': '奧蘭群島 (Åland Islands)' contextual: notes.note: abkhazia: Independent state claimed by Georgia.: '喬治亞宣稱擁有該地區主權,但目前是獨立狀態。' - djibouti.fields.field.capital: - Djibouti: '吉布地 (Djibouti)' + andorra.fields.field.flag-similarity.message.variables.country_1: + Moldova: '摩爾多瓦' + australia.fields.field.flag-similarity.message.variables.country_1: + New Zealand: '紐西蘭' + austria.fields.field.flag-similarity.message.variables.country_1: + Latvia: '拉脫維亞' + bahrain.fields.field.flag-similarity.message.variables.country_1: + Qatar: '卡達' + bolivia.fields.field.flag-similarity.message.variables.country_1: + Ghana: '迦納' + cameroon.fields.field.flag-similarity.message.variables.country_1: + Senegal: '塞內加爾' + chad.fields.field.flag-similarity.message.variables.country_1: + Romania: '羅馬尼亞' + colombia.fields.field.flag-similarity.message.variables.country_1: + Ecuador: '厄瓜多' + cuba.fields.field.flag-similarity.message.variables.country_1: + Puerto Rico: '波多黎各' + cura-ao.fields.field.flag-similarity.message.variables.country_1: + Nauru: '諾魯' djibouti: Djibouti: '吉布地共和國 (Djibouti)' + djibouti.fields.field.capital: + Djibouti: '吉布地 (Djibouti)' + ecuador.fields.field.flag-similarity.message.variables.country_1: + Colombia: '哥倫比亞' + egypt.fields.field.flag-similarity.message.variables.country_1: + Iraq: '伊拉克' + egypt.fields.field.flag-similarity.message.variables.country_2: + Yemen: '葉門' + el-salvador.fields.field.flag-similarity.message.variables.country_1: + Nicaragua: '尼加拉瓜' + faroe-islands.fields.field.flag-similarity.message.variables.country_1: + Iceland: '冰島' + faroe-islands.fields.field.flag-similarity.message.variables.country_2: + Norway: '挪威' + ghana.fields.field.flag-similarity.message.variables.country_1: + Bolivia: '波利維亞' + guinea.fields.field.flag-similarity.message.variables.country_1: + Mali: '馬利' + iceland.fields.field.flag-similarity.message.format: + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1}({description_1})' + iceland.fields.field.flag-similarity.message.variables.country_1: + Norway: '挪威' + indonesia.fields.field.flag-similarity.message.variables.country_1: + Monaco: '摩納哥' + indonesia.fields.field.flag-similarity.message.variables.country_2: + Poland: '波蘭' + iraq.fields.field.flag-similarity.message.variables.country_1: + Egypt: '埃及' + iraq.fields.field.flag-similarity.message.variables.country_2: + Yemen: '葉門' + ireland.fields.field.flag-similarity.message.variables.country_1: + Ivory Coast: '象牙海岸' + ivory-coast.fields.field.flag-similarity.message.variables.country_1: + Ireland: '愛爾蘭' + latvia.fields.field.flag-similarity.message.variables.country_1: + Austria: '奧地利' + luxembourg.fields.field.flag-similarity.message.variables.country_1: + Netherlands: '荷蘭' + mali.fields.field.flag-similarity.message.variables.country_1: + Guinea: '圭亞那' + moldova.fields.field.flag-similarity.message.variables.country_1: + Andorra: '安道爾' + monaco.fields.field.flag-similarity.message.variables.country_1: + Indonesia: '印尼' + monaco.fields.field.flag-similarity.message.variables.country_2: + Poland: '波蘭' + nauru.fields.field.flag-similarity.message.variables.country_1: + 'Curaçao': '庫拉索' + netherlands.fields.field.flag-similarity.message.variables.country_1: + Luxembourg: '盧森堡' + new-zealand.fields.field.flag-similarity.message.variables.country_1: + Australia: '澳洲' + nicaragua.fields.field.flag-similarity.message.variables.country_1: + El Salvador: '薩爾瓦多' + norway.fields.field.flag-similarity.message.variables.country_1: + Iceland: '冰島' + norway.fields.field.flag-similarity.message.variables.country_2: + Faroe Islands: '法羅群島' + palestine.fields.field.flag-similarity.message.variables.country_1: + Sudan: '蘇丹' + palestine.fields.field.flag-similarity.message.variables.country_2: + Sahrawi Arab Democratic Republic: '西撒哈拉' + poland.fields.field.flag-similarity.message.variables.country_1: + Indonesia: '印尼' + poland.fields.field.flag-similarity.message.variables.country_2: + Monaco: '摩納哥' + puerto-rico.fields.field.flag-similarity.message.variables.country_1: + Cuba: '古巴' + qatar.fields.field.flag-similarity.message.variables.country_1: + Bahrain: '巴林' + romania.fields.field.flag-similarity.message.variables.country_1: + Chad: '查德' + russia.fields.field.flag-similarity.message.variables.country_1: + Slovakia: '斯洛伐克' + sahrawi-arab-democratic-republic.fields.field.flag-similarity.message.variables.country_1: + Palestine: '巴勒斯坦' + senegal.fields.field.flag-similarity.message.variables.country_1: + Cameroon: '喀麥隆' + slovakia.fields.field.flag-similarity.message.variables.country_1: + Russia: '俄羅斯' + slovakia.fields.field.flag-similarity.message.variables.country_2: + Slovenia: '斯洛維尼亞' + slovakia.fields.field.flag-similarity.message.variables.description_1: + no coat of arms: '沒徽章' + slovenia.fields.field.flag-similarity.message.variables.country_1: + Slovakia: '斯洛伐克' south-ossetia: Independent state claimed by Georgia.: '從喬治亞獨立。' + sudan.fields.field.flag-similarity.message.variables.country_1: + Palestine: '巴勒斯坦' + yemen.fields.field.flag-similarity.message.variables.country_1: + Egypt: '埃及' + yemen.fields.field.flag-similarity.message.variables.country_2: + Iraq: '伊拉克' target_additions: notes.note.kiribati.fields.field.capital-info: '拉巴斯正在建造來取代馬拉波成為首都。' variables: diff --git a/fixtures/ultimate-geography/overlays/languages/zh.yaml b/fixtures/ultimate-geography/overlays/languages/zh.yaml index 7644fee..076bd3e 100644 --- a/fixtures/ultimate-geography/overlays/languages/zh.yaml +++ b/fixtures/ultimate-geography/overlays/languages/zh.yaml @@ -31,7 +31,6 @@ translations: Amman: '安曼(Amman)' Amsterdam: '阿姆斯特丹(Amsterdam)' Andorra: '安道尔(Andorra)' - 'Andorra (narrower, coat of arms with motto)': '安道尔(更窄,带有座右铭的纹章)' Andorra la Vella: '安道尔城(Andorra la Vella)' Angola: '安哥拉(Angola)' Anguilla: '安圭拉(Anguilla)' @@ -54,9 +53,7 @@ translations: Athens: '雅典(Athens)' Atlantic Ocean: '大西洋(Atlantic Ocean)' Australia: '澳大利亚(Australia)' - 'Australia (white stars, two more stars)': '澳大利亚(白星,多两颗星)' Austria: '奥地利(Austria)' - 'Austria (brighter red, wider white band)': '奥地利(更亮的红色,更宽的白色带)' Autonomous community of Spain.: '西班牙(Spain)自治区。' Autonomous province of South Korea.: '韩国(South Korea)自治省。' Autonomous region of Finland.: '芬兰(Finland)自治区。' @@ -68,7 +65,6 @@ translations: Azores: '亚速尔群岛(Azores)' Baghdad: '巴格达(Baghdad)' Bahrain: '巴林(Bahrain)' - 'Bahrain (narrower, fewer serrated edges, red)': '巴林(更窄,锯齿边缘更少,红色)' Baku: '巴库(Baku)' Bali: '巴厘岛(Bali)' Balkan Peninsula: '巴尔干半岛(Balkan Peninsula)' @@ -104,7 +100,6 @@ translations: Black Sea: '黑海(Black Sea)' 'Bogotá': '波哥大(Bogotá)' Bolivia: '玻利维亚(Bolivia)' - 'Bolivia (coat of arms instead of star)': '玻利维亚(徽章而不是星形)' Bosnia and Herzegovina: '波斯尼亚和黑塞哥维那(Bosnia and Herzegovina),简称“波黑”' Botswana: '博茨瓦纳(Botswana)' Bougainville: '布干维尔(Bougainville)' @@ -125,7 +120,6 @@ translations: Cairo: '开罗(Cairo)' Cambodia: '柬埔寨(Cambodia)' Cameroon: '喀麦隆(Cameroon)' - 'Cameroon (green/red/yellow, yellow star)': '喀麦隆(绿/红/黄,黄星)' Canada: '加拿大(Canada)' Canary Islands: '加那利群岛(Canary Islands)' Canberra: '堪培拉(Canberra)' @@ -141,7 +135,6 @@ translations: Central African Republic: '中非共和国(Central African Republic)' Cetinje is an honorary capital.: '采蒂涅(Cetinje),黑山古都和宗教、历史中心。' Chad: '乍得(Chad)' - 'Chad (slightly darker blue)': '乍得(略深蓝色)' Charlotte Amalie: '夏洛特阿马利亚(Charlotte Amalie)' Chile: '智利(Chile)' China: '中国(China)' @@ -151,7 +144,6 @@ translations: Claimed and controlled: '声称对其拥有主权并拥有实际控制权' Claimed but not controlled: '声称对其拥有主权但没有实际控制权' Colombia: '哥伦比亚(Colombia)' - 'Colombia (no coat of arms)': '哥伦比亚(无国徽)' Colombo is often referred to as the capital but Sri Jayawardenepura Kotte, a suburb of Colombo, is the official, legislative capital.: '科伦坡(Colombo)通常被称为首都,但科伦坡郊区的斯里贾亚瓦德纳普拉科特(Sri Jayawardenepura Kotte)是官方的立法首都。' Comoros: '科摩罗(Comoros)' Conakry: '科纳克里(Conakry)' @@ -166,9 +158,7 @@ translations: Croatia: '克罗地亚(Croatia)' Crown dependency of the United Kingdom.: '英国(U.K.)的皇家属地。' Cuba: '古巴(Cuba)' - 'Cuba (red triangle, blue stripes)': '古巴(红三角,蓝条纹)' 'Curaçao': '库拉索(Curaçao)' - 'Curaçao (two stars in top-left corner)': '库拉索岛(左上角的两颗星)' Cyprus: '塞浦路斯(Cyprus)' Czech Republic: '捷克共和国(Czech Republic)' Dakar: '达喀尔(Dakar)' @@ -191,13 +181,9 @@ translations: East China Sea: '东海(East China Sea)' East Siberian Sea: '东西伯利亚海(East Siberian Sea)' Ecuador: '厄瓜多尔(Ecuador)' - 'Ecuador (with coat of arms)': '厄瓜多尔(有国徽)' Edinburgh: '爱丁堡(Edinburgh)' Egypt: '埃及(Egypt)' - 'Egypt (emblem instead of text), Yemen (no text)': '埃及(标志代替文字)、也门(无文字)' - 'Egypt (with emblem), Iraq (with text)': '埃及(带徽章)、伊拉克(带文字)' El Salvador: '萨尔瓦多(El Salvador)' - 'El Salvador (different coat of arms, slightly darker blue)': '萨尔瓦多(不同的纹章,略深的蓝色)' England: '英格兰(England)' English Channel: '英吉利海峡(English Channel)' Equatorial Guinea: '赤道几内亚(Equatorial Guinea)' @@ -225,7 +211,6 @@ translations: Georgia: '格鲁吉亚(Georgia)' Germany: '德国(Germany)' Ghana: '加纳(Ghana)' - 'Ghana (star instead of coat of arms)': '加纳(星而不是国徽)' Gibraltar: '直布罗陀(Gibraltar)' Gitega: '基特加(Gitega)' Greece: '希腊(Greece)' @@ -237,7 +222,6 @@ translations: Guatemala City: '危地马拉市(Guatemala City)' Guernsey: '根西岛(Guernsey)' Guinea: '几内亚(Guinea)' - 'Guinea (green and red flipped, slightly darker green)': '几内亚(绿色和红色翻转,绿色略深)' Guinea-Bissau: '几内亚比绍(Guinea-Bissau)' Gulf of Alaska: '阿拉斯加湾(Gulf of Alaska)' Gulf of California: '加利福尼亚湾(Gulf of California)' @@ -261,28 +245,21 @@ translations: Hudson Bay: '哈德逊湾(Hudson Bay)' Hungary: '匈牙利(Hungary)' Iceland: '冰岛(Iceland)' - 'Iceland (blue background, red and white cross), Norway (red background, blue and white cross)': '冰岛(蓝底,红白交叉),挪威(红底,蓝白交叉)' - 'Iceland (blue background, red cross), Faroe Islands (white background, red and blue cross)': '冰岛(蓝底,红十字),法罗群岛(白底,红蓝交叉)' Independent state claimed by Georgia.: '格鲁吉亚(Georgia)宣称拥有主权的独立国家。' Independent state claimed by Moldova.: '摩尔多瓦(Moldova)宣称拥有主权的独立国家。' Independent state claimed by Somalia.: '索马里(Somalia)宣称拥有主权的独立国家。' India: '印度(India)' Indian Ocean: '印度洋(Indian Ocean)' Indonesia: '印度尼西亚(Indonesia)' - 'Indonesia (white and red flipped, brighter red), Monaco (white and red flipped, narrower)': '印度尼西亚(白色和红色翻转,红色更亮),摩纳哥(白色和红色翻转,更窄)' - 'Indonesia (wider, brighter red), Poland (red and white flipped, wider)': '印度尼西亚(更宽,红色更亮),波兰(红色和白色翻转,更宽)' Iran: '伊朗(Iran)' Iraq: '伊拉克(Iraq)' - 'Iraq (text instead of emblem), Yemen (no emblem)': '伊拉克(文字代替徽记)、也门(无徽记)' Ireland: '爱尔兰(Ireland)' - 'Ireland (orange and green flipped, wider)': '爱尔兰(橙色和绿色翻转,更宽)' Islamabad: '伊斯兰堡(Islamabad)' Island of Indonesia.: '印度尼西亚(Indonesia)的岛屿之一。' Isle of Man: '马恩岛(Isle of Man)' Israel: '以色列(Israel)' Italy: '意大利(Italy)' Ivory Coast: '科特迪瓦(Ivory Coast)' - 'Ivory Coast (green and orange flipped, narrower)': '科特迪瓦(绿色和橙色翻转,更窄)' Jakarta: '雅加达(Jakarta)' Jamaica: '牙买加(Jamaica)' Japan: '日本(Japan)' @@ -316,7 +293,6 @@ translations: Labrador Sea: '拉布拉多海(Labrador Sea)' Laos: '老挝(Laos)' Latvia: '拉脱维亚(Latvia)' - 'Latvia (darker red, narrower white band)': '拉脱维亚(较深的红色,较窄的白色带)' Lebanon: '黎巴嫩(Lebanon)' Lesotho: '莱索托(Lesotho)' Liberia: '利比里亚(Liberia)' @@ -333,7 +309,6 @@ translations: Luanda: '罗安达(Luanda)' Lusaka: '卢萨卡(Lusaka)' Luxembourg: '卢森堡(Luxembourg)' - 'Luxembourg (lighter blue)': '卢森堡(浅蓝色)' Luxembourg City: '卢森堡市(Luxembourg City)' Macau: '澳门(Macau)' Madagascar: '马达加斯加(Madagascar)' @@ -344,7 +319,6 @@ translations: Malaysia: '马来西亚(Malaysia)' Maldives: '马尔代夫(Maldives)' Mali: '马里(Mali)' - 'Mali (red and green flipped, slightly brighter green)': '马里(红绿翻转,绿色略亮)' Malta: '马耳他(Malta)' 'Malé': '马累(Malé)' Managua: '马那瓜(Managua)' @@ -367,9 +341,7 @@ translations: Minsk: '明斯克(Minsk)' Mogadishu: '摩加迪沙(Mogadishu)' Moldova: '摩尔多瓦(Moldova)' - 'Moldova (wider, coat of arms with eagle)': '摩尔多瓦(更宽,鹰纹)' Monaco: '摩纳哥(Monaco)' - 'Monaco (narrower, darker red), Poland (red and white flipped, darker red)': '摩纳哥(更窄,红色更深),波兰(红白翻转,红色更深)' Mongolia: '蒙古(Mongolia)' Monrovia: '蒙罗维亚(Monrovia)' Montenegro: '黑山(Montenegro)' @@ -385,20 +357,16 @@ translations: Namibia: '纳米比亚(Namibia)' Nassau: '拿骚(Nassau)' Nauru: '瑙鲁(Nauru)' - 'Nauru (single star below yellow band)': '瑙鲁(黄带下方的单星)' 'Nauru has no official capital; the Yaren District is the de facto capital.': '瑙鲁(Nauru)无正式首都,政府机关设在亚伦区(the Yaren District)。' Naypyidaw: '内比都(Naypyidaw)' Nepal: '尼泊尔(Nepal)' Netherlands: '荷兰(Netherlands)' - 'Netherlands (darker blue)': '荷兰(深蓝色)' New Caledonia: '新喀里多尼亚(New Caledonia)' New Delhi: '新德里(New Delhi)' New Zealand: '新西兰(New Zealand)' - 'New Zealand (red stars, two fewer stars)': '新西兰(红星,少两颗星)' Ngerulmud: '恩格鲁穆德(Ngerulmud)' Niamey: '尼亚美(Niamey)' Nicaragua: '尼加拉瓜(Nicaragua)' - 'Nicaragua (different coat of arms, slightly lighter blue)': '尼加拉瓜(不同的纹章,略浅的蓝色)' Nicosia: '尼科西亚(Nicosia)' Niger: '尼日尔(Niger)' Nigeria: '尼日利亚(Nigeria)' @@ -412,7 +380,6 @@ translations: Northern Ireland: '北爱尔兰(Northern Ireland)' Northern Mariana Islands: '北马里亚纳群岛(Northern Mariana Islands)' Norway: '挪威(Norway)' - 'Norway (red background, blue cross), Faroe Islands (white background, red and blue cross)': '挪威(红底蓝十字)、法罗群岛(白底红蓝十字)' Norwegian Sea: '挪威海(Norwegian Sea)' Not a sovereign country: '不是主权国家' Nouakchott: '努瓦克肖特(Nouakchott)' @@ -437,8 +404,6 @@ translations: Pakistan: '巴基斯坦(Pakistan)' Palau: '帕劳(Palau)' Palestine: '巴勒斯坦(Palestine)' - 'Palestine (black/white/green, red arrow)': '巴勒斯坦(黑/白/绿,红色箭头)' - 'Palestine (no symbol)': '巴勒斯坦(无符号)' Palikir: '帕利基尔(Palikir)' Panama: '巴拿马(Panama)' Panama City: '巴拿马城(Panama City)' @@ -470,10 +435,8 @@ translations: Pretoria, Cape Town, Bloemfontein: '比勒陀利亚(Pretoria)、开普敦(Cape Town)、布隆方丹(Bloemfontein)' Pristina: '普里什蒂纳(Pristina)' Puerto Rico: '波多黎各(Puerto Rico)' - 'Puerto Rico (blue triangle, red stripes)': '波多黎各(蓝色三角形,红色条纹)' Pyongyang: '平壤(Pyongyang)' Qatar: '卡塔尔(Qatar)' - 'Qatar (wider, more serrated edges, maroon)': '卡塔尔(更宽,锯齿状边缘,栗色)' Quito: '基多(Quito)' Rabat: '拉巴特(Rabat)' Red Sea: '红海(Red Sea)' @@ -483,11 +446,9 @@ translations: Riga: '里加(Riga)' Riyadh: '利雅得(Riyadh)' Romania: '罗马尼亚(Romania)' - 'Romania (slightly lighter blue)': '罗马尼亚(略浅蓝色)' Rome: '罗马(Rome)' Roseau: '罗索(Roseau)' Russia: '俄罗斯(Russia)' - 'Russia (no coat of arms), Slovenia (wider, smaller coat of arms)': '俄罗斯(无国徽)、斯洛文尼亚(更宽、更小的国徽)' Rwanda: '卢旺达(Rwanda)' 'Réunion': '留尼汪岛(Réunion)' Sahrawi Arab Democratic Republic: '阿拉伯撒哈拉民主共和国(Sahrawi Arab Democratic Republic)' @@ -513,7 +474,6 @@ translations: Sea of Okhotsk: '鄂霍次克海(Sea of Okhotsk)' Semi-autonomous region of Tanzania.: '坦桑尼亚(Tanzania)的半自治区。' Senegal: '塞内加尔(Senegal)' - 'Senegal (green/yellow/red, green star)': '塞内加尔(绿/黄/红,绿星)' Seoul: '首尔(Seoul)' Serbia: '塞尔维亚(Serbia)' Seychelles: '塞舌尔(Seychelles)' @@ -523,8 +483,6 @@ translations: Sint Maarten: '圣马丁岛(Sint Maarten)' Skopje: '斯科普里(Skopje)' Slovakia: '斯洛伐克(Slovakia)' - 'Slovakia (narrower, bigger coat of arms)': '斯洛伐克(更窄、更大的国徽)' - 'Slovakia (with coat of arms)': '斯洛伐克(有国徽)' Slovenia: '斯洛文尼亚(Slovenia)' Sofia: '索菲亚(Sofia)' Solomon Islands: '所罗门群岛(Solomon Islands)' @@ -554,7 +512,6 @@ translations: Subregion of Oceania, which includes Vanuatu, the Solomon Islands, Fiji, and Papua New Guinea.: '大洋洲(Oceania)的一个区域,包括瓦努阿图(Vanuatu)、所罗门群岛(Solomon Islands)、斐济(Fiji)和巴布亚新几内亚(Papua New Guinea)。' Sucre: '苏克雷(Sucre)' Sudan: '苏丹(Sudan)' - 'Sudan (red/white/black, green arrow), Sahrawi Arab Democratic Republic (with star and crescent)': '苏丹(红/白/黑、绿箭头)、阿拉伯撒哈拉民主共和国(有星形和新月形)' Sukhumi: '苏呼米(Sukhumi)' Sumatra: '苏门答腊岛(Sumatra)' Suriname: '苏里南(Suriname)' @@ -644,13 +601,186 @@ translations: Zambia: '赞比亚(Zambia)' Zanzibar: '桑给巴尔(Zanzibar)' Zimbabwe: '津巴布韦(Zimbabwe)' + black/white/green, red arrow: '黑/白/绿,红色箭头' + blue background, red and white cross: '蓝底,红白交叉' + blue background, red cross: '蓝底,红十字' + blue triangle, red stripes: '蓝色三角形,红色条纹' + brighter red, wider white band: '更亮的红色,更宽的白色带' + coat of arms instead of star: '徽章而不是星形' + darker blue: '深蓝色' + darker red, narrower white band: '较深的红色,较窄的白色带' + different coat of arms, slightly darker blue: '不同的纹章,略深的蓝色' + different coat of arms, slightly lighter blue: '不同的纹章,略浅的蓝色' + emblem instead of text: '标志代替文字' + green and orange flipped, narrower: '绿色和橙色翻转,更窄' + green and red flipped, slightly darker green: '绿色和红色翻转,绿色略深' + green/red/yellow, yellow star: '绿/红/黄,黄星' + green/yellow/red, green star: '绿/黄/红,绿星' + lighter blue: '浅蓝色' + narrower, bigger coat of arms: '更窄、更大的国徽' + narrower, coat of arms with motto: '更窄,带有座右铭的纹章' + narrower, darker red: '更窄,红色更深' + narrower, fewer serrated edges, red: '更窄,锯齿边缘更少,红色' + no coat of arms: '无国徽' + no emblem: '无徽记' + no symbol: '无符号' + no text: '无文字' + orange and green flipped, wider: '橙色和绿色翻转,更宽' + red and green flipped, slightly brighter green: '红绿翻转,绿色略亮' + red and white flipped, darker red: '红白翻转,红色更深' + red and white flipped, wider: '红色和白色翻转,更宽' + red background, blue and white cross: '红底,蓝白交叉' + red background, blue cross: '红底蓝十字' + red stars, two fewer stars: '红星,少两颗星' + red triangle, blue stripes: '红三角,蓝条纹' + red/white/black, green arrow: '红/白/黑、绿箭头' + single star below yellow band: '黄带下方的单星' + slightly darker blue: '略深蓝色' + slightly lighter blue: '略浅蓝色' + star instead of coat of arms: '星而不是国徽' + text instead of emblem: '文字代替徽记' + two stars in top-left corner: '左上角的两颗星' + white and red flipped, brighter red: '白色和红色翻转,红色更亮' + white and red flipped, narrower: '白色和红色翻转,更窄' + white background, red and blue cross: '白底,红蓝交叉' + white stars, two more stars: '白星,多两颗星' + wider, brighter red: '更宽,红色更亮' + wider, coat of arms with eagle: '更宽,鹰纹' + wider, more serrated edges, maroon: '更宽,锯齿状边缘,栗色' + wider, smaller coat of arms: '更宽、更小的国徽' + with coat of arms: '有国徽' + with emblem: '带徽章' + with star and crescent: '有星形和新月形' + with text: '带文字' + '{country_1} ({description_1})': '{country_1}({description_1})' + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1}({description_1})、{country_2}({description_2})' 'Åland Islands': '奥兰群岛(Åland Islands)' contextual: notes.note: - vatican-city.fields.field.capital: - Vatican City: '梵蒂冈城(Vatican City)' + andorra.fields.field.flag-similarity.message.variables.country_1: + Moldova: '摩尔多瓦' + australia.fields.field.flag-similarity.message.variables.country_1: + New Zealand: '新西兰' + austria.fields.field.flag-similarity.message.variables.country_1: + Latvia: '拉脱维亚' + bahrain.fields.field.flag-similarity.message.variables.country_1: + Qatar: '卡塔尔' + bolivia.fields.field.flag-similarity.message.variables.country_1: + Ghana: '加纳' + cameroon.fields.field.flag-similarity.message.variables.country_1: + Senegal: '塞内加尔' + chad.fields.field.flag-similarity.message.variables.country_1: + Romania: '罗马尼亚' + colombia.fields.field.flag-similarity.message.variables.country_1: + Ecuador: '厄瓜多尔' + cuba.fields.field.flag-similarity.message.variables.country_1: + Puerto Rico: '波多黎各' + cura-ao.fields.field.flag-similarity.message.variables.country_1: + Nauru: '瑙鲁' + ecuador.fields.field.flag-similarity.message.variables.country_1: + Colombia: '哥伦比亚' + egypt.fields.field.flag-similarity.message.variables.country_1: + Iraq: '伊拉克' + egypt.fields.field.flag-similarity.message.variables.country_2: + Yemen: '也门' + el-salvador.fields.field.flag-similarity.message.variables.country_1: + Nicaragua: '尼加拉瓜' + faroe-islands.fields.field.flag-similarity.message.format: + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1}({description_1}),{country_2}({description_2})' + faroe-islands.fields.field.flag-similarity.message.variables.country_1: + Iceland: '冰岛' + faroe-islands.fields.field.flag-similarity.message.variables.country_2: + Norway: '挪威' + ghana.fields.field.flag-similarity.message.variables.country_1: + Bolivia: '玻利维亚' + guinea.fields.field.flag-similarity.message.variables.country_1: + Mali: '马里' + iceland.fields.field.flag-similarity.message.variables.country_1: + Norway: '挪威' + iceland.fields.field.flag-similarity.message.variables.country_2: + Faroe Islands: '法罗群岛' + iceland.fields.field.flag-similarity.message.variables.description_2: + white background, red and blue cross: '白底红蓝十字' + indonesia.fields.field.flag-similarity.message.format: + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1}({description_1}),{country_2}({description_2})' + indonesia.fields.field.flag-similarity.message.variables.country_1: + Monaco: '摩纳哥' + indonesia.fields.field.flag-similarity.message.variables.country_2: + Poland: '波兰' + iraq.fields.field.flag-similarity.message.variables.country_1: + Egypt: '埃及' + iraq.fields.field.flag-similarity.message.variables.country_2: + Yemen: '也门' + ireland.fields.field.flag-similarity.message.variables.country_1: + Ivory Coast: '科特迪瓦' + ivory-coast.fields.field.flag-similarity.message.variables.country_1: + Ireland: '爱尔兰' + latvia.fields.field.flag-similarity.message.variables.country_1: + Austria: '奥地利' + luxembourg.fields.field.flag-similarity.message.variables.country_1: + Netherlands: '荷兰' + mali.fields.field.flag-similarity.message.variables.country_1: + Guinea: '几内亚' + moldova.fields.field.flag-similarity.message.variables.country_1: + Andorra: '安道尔' + monaco.fields.field.flag-similarity.message.format: + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1}({description_1}),{country_2}({description_2})' + monaco.fields.field.flag-similarity.message.variables.country_1: + Indonesia: '印度尼西亚' + monaco.fields.field.flag-similarity.message.variables.country_2: + Poland: '波兰' + nauru.fields.field.flag-similarity.message.variables.country_1: + 'Curaçao': '库拉索岛' + netherlands.fields.field.flag-similarity.message.variables.country_1: + Luxembourg: '卢森堡' + new-zealand.fields.field.flag-similarity.message.variables.country_1: + Australia: '澳大利亚' + nicaragua.fields.field.flag-similarity.message.variables.country_1: + El Salvador: '萨尔瓦多' + norway.fields.field.flag-similarity.message.format: + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1}({description_1}),{country_2}({description_2})' + norway.fields.field.flag-similarity.message.variables.country_1: + Iceland: '冰岛' + norway.fields.field.flag-similarity.message.variables.country_2: + Faroe Islands: '法罗群岛' + palestine.fields.field.flag-similarity.message.variables.country_1: + Sudan: '苏丹' + palestine.fields.field.flag-similarity.message.variables.country_2: + Sahrawi Arab Democratic Republic: '阿拉伯撒哈拉民主共和国' + poland.fields.field.flag-similarity.message.format: + '{country_1} ({description_1}), {country_2} ({description_2})': '{country_1}({description_1}),{country_2}({description_2})' + poland.fields.field.flag-similarity.message.variables.country_1: + Indonesia: '印度尼西亚' + poland.fields.field.flag-similarity.message.variables.country_2: + Monaco: '摩纳哥' + puerto-rico.fields.field.flag-similarity.message.variables.country_1: + Cuba: '古巴' + qatar.fields.field.flag-similarity.message.variables.country_1: + Bahrain: '巴林' + romania.fields.field.flag-similarity.message.variables.country_1: + Chad: '乍得' + russia.fields.field.flag-similarity.message.variables.country_1: + Slovakia: '斯洛伐克' + sahrawi-arab-democratic-republic.fields.field.flag-similarity.message.variables.country_1: + Palestine: '巴勒斯坦' + senegal.fields.field.flag-similarity.message.variables.country_1: + Cameroon: '喀麦隆' + slovakia.fields.field.flag-similarity.message.variables.country_1: + Russia: '俄罗斯' + slovakia.fields.field.flag-similarity.message.variables.country_2: + Slovenia: '斯洛文尼亚' + slovenia.fields.field.flag-similarity.message.variables.country_1: + Slovakia: '斯洛伐克' + sudan.fields.field.flag-similarity.message.variables.country_1: + Palestine: '巴勒斯坦' vatican-city: Vatican City: '梵蒂冈(Vatican City)' + vatican-city.fields.field.capital: + Vatican City: '梵蒂冈城(Vatican City)' + yemen.fields.field.flag-similarity.message.variables.country_1: + Egypt: '埃及' + yemen.fields.field.flag-similarity.message.variables.country_2: + Iraq: '伊拉克' target_additions: notes.note.myanmar.fields.field.capital-info: '缅甸于2005年11月6日首都从仰光(Yangon)迁往内比都(Naypyidaw)。' notes.note.netherlands.fields.field.country-info: '现名尼德兰(Netherlands),中文通译为荷兰(Holanda)' From 0d3992b877f8719b7dbf5a4109747aee3cae8f43 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Tue, 23 Jun 2026 11:28:51 +0200 Subject: [PATCH 037/142] Document Deck Workbench direction --- CONTEXT.md | 28 +++++- .../src/commands/translations.rs | 2 + crates/brain-brew-cli/src/help.rs | 2 +- crates/brain-brew-cli/tests/cli.rs | 59 ++++++++++++- crates/brain-brew-core/src/lib.rs | 6 ++ documentation/docs/authoring/translations.md | 2 +- documentation/docs/reference/cli.md | 10 +++ ...deck-workbench-server-with-iced-wasm-ui.md | 51 +++++++++++ ...anguage-catalog-and-translation-profile.md | 85 +++++++++++++++++++ ...slation-records-for-source-text-changes.md | 59 +++++++++++++ ...ire-workbench-api-and-browser-e2e-tests.md | 45 ++++++++++ .../docs/reference/decisions/README.md | 4 + 12 files changed, 349 insertions(+), 4 deletions(-) create mode 100644 documentation/docs/reference/decisions/0011-use-a-local-deck-workbench-server-with-iced-wasm-ui.md create mode 100644 documentation/docs/reference/decisions/0012-add-manifest-language-catalog-and-translation-profile.md create mode 100644 documentation/docs/reference/decisions/0013-use-stale-translation-records-for-source-text-changes.md create mode 100644 documentation/docs/reference/decisions/0014-require-workbench-api-and-browser-e2e-tests.md diff --git a/CONTEXT.md b/CONTEXT.md index 1096017..9bb2b8f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -16,6 +16,10 @@ _Avoid_: note list, CSV file, Anki export A person responsible for evolving and publishing a shared flashcard deck. _Avoid_: learner, reviewer, end user +**Deck Workbench**: +An ergonomic authoring and review interface for working with a Federated Deck workspace without treating the interface as canonical storage. +_Avoid_: web app source of truth, generated HTML report, translator-only app + **Learner**: A person who studies with a shared deck and may have private changes to preserve. _Avoid_: deck maintainer, publisher @@ -88,6 +92,14 @@ _Avoid_: note ID, canonical ID A bounded set of changes applied to a base deck without replacing the base deck. _Avoid_: fork, duplicate deck +**Source Language**: +The language of the base deck text used as the reference for translation work. +_Avoid_: hard-coded English assumption, target language + +**Target Language**: +A language produced from the source language by a Translation Overlay. +_Avoid_: separate translated deck, independent deck language + **Translation Overlay**: An overlay that changes deck language or localized text. _Avoid_: separate translated deck @@ -113,9 +125,13 @@ A named text value defined on a deck, note type, card template, or note and refe _Avoid_: recipe variable, runtime Anki field **Translation Dictionary**: -A translation overlay section mapping direct source text, contextual source text, explicit no-change decisions for reviewed unchanged text, target-language additions for blank source fields, source variables, and adapter IDs, with non-blank source keys acting as implicit expected bases. +A translation overlay section mapping direct source text, contextual source text, explicit no-change decisions for reviewed unchanged text, target-language additions for blank source fields, source variables, adapter IDs, and stale translation records, with non-blank source keys acting as implicit expected bases. _Avoid_: CSV importer, global localization database +**Stale Translation Record**: +A Translation Dictionary review item that applies a prior target text to changed source text while warning that the translation needs review. +_Avoid_: stale key only, automatic silent migration + **Field Fill**: An overlay shorthand for filling existing blank note fields with new content while requiring the upstream field to still be blank. _Avoid_: translation addition, field definition addition @@ -140,6 +156,10 @@ _Avoid_: unordered overlay set, dependency graph A named collection of overlays available in a Federated Deck. _Avoid_: raw file list, package solver +**Language Catalog**: +A named collection of source and target languages available in a Federated Deck, connecting target languages to their Translation Overlays and Build Targets. +_Avoid_: target naming convention, hard-coded language list + **Overlay Dependency**: A requirement that one overlay include and apply another overlay before itself. _Avoid_: implicit conflict resolution, automatic content merge @@ -181,6 +201,7 @@ _Avoid_: import only, export only, one-way conversion - **Brain Brew** primarily serves **Deck Maintainers**. - A **Deck Maintainer** publishes one or more **Shared Decks**. - A **Deck Maintainer** may publish a **Federated Deck** as a composable shared-deck source package. +- A **Deck Workbench** helps people review or author a **Federated Deck** while preserving source files as canonical storage. - A **Federated Deck** may contribute a base **Deck**, **Overlays**, or both. - **Federated Decks** compose through **Deck Federation** to produce **Resolved Decks**. - A **Learner** studies a **Shared Deck** and may have a **Personal Overlay**. @@ -200,12 +221,15 @@ _Avoid_: import only, export only, one-way conversion - **Deck Federation** combines one base **Deck** with zero or more **Overlays**. - An **Overlay** contains an **Overlay Fragment**. - An **Overlay** may use a **Translation Dictionary** to translate extracted source text without repeating per-field replacement boilerplate. +- A **Translation Dictionary** may contain **Stale Translation Records** when source text changes invalidate existing target-language decisions. - An **Overlay** may use **Field Fills** to add content to existing blank note fields without misclassifying that content as a translation. - A **Source Variable** lets shared card template structure refer to phrase values translated by a **Translation Dictionary**. - An **Overlay** uses **Change Intents** to change **Deck Entities** by **Stable ID**. - Replace, remove, and override **Change Intents** require an **Expected Base**. - A remove **Change Intent** creates a **Tombstone**. - An **Overlay Catalog** names the overlays available in a **Federated Deck**. +- A **Language Catalog** names the **Source Language** and **Target Languages** available in a **Federated Deck**. +- A **Target Language** is produced by applying a **Translation Overlay** to source-language content. - An **Overlay Dependency** constrains the order of an **Overlay Stack**. - An **Overlay Stack** applies overlays in declared or dependency-expanded order. - A **Build Target** selects overlays from an **Overlay Catalog** for composition. @@ -237,3 +261,5 @@ _Avoid_: import only, export only, one-way conversion - "translated deck identity" could mean a separate stable identity per language; resolved: translations use language-neutral **Stable IDs** for the same conceptual **Deck Entities** and language-specific external identities remain **Adapter IDs**. - "Ultimate Geography support" could mean product-specific application behavior; resolved: Ultimate Geography is a demanding case study and parity fixture for general Brain Brew federation behavior, not a special-purpose application feature. - "migration import" could mean Brain Brew should convert every legacy source layout; resolved: initial migration means refactoring into **Canonical Deck Files** and proving output parity, not building public legacy source importers. +- "webview" and "Iced server" were used for an interactive editing surface; resolved: the product concept is a **Deck Workbench**, and it must not become a separate source of truth. +- "language" could mean either source text language or translated output language; resolved: use **Source Language** for the base text and **Target Language** for languages produced by **Translation Overlays**. diff --git a/crates/brain-brew-cli/src/commands/translations.rs b/crates/brain-brew-cli/src/commands/translations.rs index af4df49..3692f59 100644 --- a/crates/brain-brew-cli/src/commands/translations.rs +++ b/crates/brain-brew-cli/src/commands/translations.rs @@ -1784,6 +1784,8 @@ fn context_unit_json(unit: &TranslationContextUnit) -> serde_json::Value { "template_id": card.template_id.as_str(), "template_name": card.template_name, "sides": card.sides.iter().map(|side| side.as_str()).collect::>(), + "question_format": card.question_format, + "answer_format": card.answer_format, })).collect::>(), }) } diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index d5a7ca9..03a523f 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -13,7 +13,7 @@ pub(crate) fn general() -> String { " import Import CrowdAnki into CanonicalDeck YAML\n", " lock Update or verify locked federated package inputs\n", " targets List manifest targets\n", - " translations Report/apply translation coverage stubs (aliases: translate, translation)\n", + " translations Report/apply translation coverage (aliases: translate, translation)\n", " verify Run manifest formatting, composition, validation, media, and golden checks\n", " explain Explain a manifest target and its overlay stack\n", " diff Compare decks semantically, or emit an overlay draft\n\n", diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index 4d9093d..addba03 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -956,6 +956,46 @@ fn translations_missing_manifest_lists_nearby_manifests() { assert!(err.contains("brainbrew translations --manifest decks/sample/brainbrew.yaml")); } +#[test] +fn translations_help_does_not_advertise_rejected_static_editor_flags() { + let output = run(["translations", "--help"]); + + assert!(output.status.success(), "stderr: {}", stderr(&output)); + let out = stdout(&output); + assert!(!out.contains("--web-editor")); + assert!(!out.contains("--apply-editor-edits")); + assert!(!out.contains("translator editor")); +} + +#[test] +fn translations_rejects_rejected_static_editor_flags() { + let dir = temp_dir("translations-rejected-static-editor-flags"); + write_translation_workspace(&dir); + + let web = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--target", + "da-standard", + "--web-editor", + "--out", + dir.join("editor.html").to_str().unwrap(), + ]); + assert!(!web.status.success()); + assert!(stderr(&web).contains("unexpected translations argument \"--web-editor\"")); + + let apply = run([ + "translations", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--apply-editor-edits", + dir.join("edits.json").to_str().unwrap(), + ]); + assert!(!apply.status.success()); + assert!(stderr(&apply).contains("unexpected translations argument \"--apply-editor-edits\"")); +} + #[test] fn translations_reports_when_selected_target_has_no_dictionary_overlays() { let manifest = workspace_root().join("fixtures/ug-style/brainbrew.yaml"); @@ -1089,7 +1129,24 @@ fn translations_context_view_json_exposes_reusable_context_model() { assert_eq!(unit["note_fields"][0]["translated"], "Canarias"); assert_eq!(unit["source"], "Autonomous community of Spain."); assert_eq!(unit["translated"], "Comunidad autónoma de España."); - assert!(unit["card_templates"].as_array().unwrap().len() >= 4); + let card_templates = unit["card_templates"].as_array().unwrap(); + assert!(card_templates.len() >= 4); + let country_capital = card_templates + .iter() + .find(|card| card["template_id"] == "template.country-capital") + .expect("country-capital card context is present"); + assert!( + country_capital["question_format"] + .as_str() + .unwrap() + .contains("{{Country info}}") + ); + assert!( + country_capital["answer_format"] + .as_str() + .unwrap() + .contains("{{Capital}}") + ); } #[test] diff --git a/crates/brain-brew-core/src/lib.rs b/crates/brain-brew-core/src/lib.rs index 19541d4..8607b0e 100644 --- a/crates/brain-brew-core/src/lib.rs +++ b/crates/brain-brew-core/src/lib.rs @@ -1505,6 +1505,8 @@ fn card_contexts_for_field(note_type: &NoteType, field_name: &str) -> Vec, + /// Near-rendered card front template snippet where this string may appear. + pub question_format: String, + /// Near-rendered card back template snippet where this string may appear. + pub answer_format: String, } /// Card side where a translated field appears. diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index 0ba12ff..fe44d0a 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -228,7 +228,7 @@ brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json brainbrew translations --manifest brainbrew.yaml --target de-standard --full ``` -Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Use `--context` for the first translator-in-context terminal view: it shows source English and target text together with translation status, note id, field id/name, note type, and card templates where the field appears. Filter or navigate the context view with `--language`, `--note`, `--field`, `--source`, `--duplicates`, `--status missing|stale|translated|direct|contextual|no-change`, and `--path-prefix`. Repeated source strings are shown as duplicate source groups so translators can see whether a reusable `direct`/`no_change` entry is enough or a path-specific `contextual` override is needed. YAML remains the canonical storage format; the terminal context view is the first ergonomic translator interface over that canonical data, and `--context --json` exposes the same note/field/card context model for later Iced or web UIs. Use `--summary` for compact per-language/per-overlay counts; summary mode de-duplicates identical reports across target variants and includes direct translations, contextual overrides, no-change entries, target-language additions, variables, adapter IDs, raw untranslated fallbacks, actionable missing text translations, ignored entries, and stale/invalid keys. Human summary output uses narrow aligned columns by default; add `--summary --full` to include overlay/file columns, or `--summary --json` for complete machine-readable metadata. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. +Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Use `--context` for the first translator-in-context terminal view: it shows source English and target text together with translation status, note id, field id/name, note type, and card templates where the field appears. Filter or navigate the context view with `--language`, `--note`, `--field`, `--source`, `--duplicates`, `--status missing|stale|translated|direct|contextual|no-change`, and `--path-prefix`. Repeated source strings are shown as duplicate source groups so translators can see whether a reusable `direct`/`no_change` entry is enough or a path-specific `contextual` override is needed. YAML remains the canonical storage format; the terminal context view is the first ergonomic translator interface over that canonical data, and `--context --json` exposes the same note/field/card context model intended for the Deck Workbench. Use `--summary` for compact per-language/per-overlay counts; summary mode de-duplicates identical reports across target variants and includes direct translations, contextual overrides, no-change entries, target-language additions, variables, adapter IDs, raw untranslated fallbacks, actionable missing text translations, ignored entries, and stale/invalid keys. Human summary output uses narrow aligned columns by default; add `--summary --full` to include overlay/file columns, or `--summary --json` for complete machine-readable metadata. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. To seed translator work after adding English notes or fields, run apply explicitly: diff --git a/documentation/docs/reference/cli.md b/documentation/docs/reference/cli.md index 794fdb4..bc39f1e 100644 --- a/documentation/docs/reference/cli.md +++ b/documentation/docs/reference/cli.md @@ -79,6 +79,16 @@ brainbrew explain --manifest brainbrew.yaml --target en-standard --json Shows expanded overlay stack and resulting changes. +## `translations` / `translate` + +```bash +brainbrew translations --manifest brainbrew.yaml --target da-standard +brainbrew translations --manifest brainbrew.yaml --target da-standard --context +brainbrew translations --manifest brainbrew.yaml --all-targets --summary +``` + +Reports translation coverage, shows terminal note/card context, summarizes translation state, or applies reviewed translation stubs back to canonical translation overlay YAML. + ## `verify` ```bash diff --git a/documentation/docs/reference/decisions/0011-use-a-local-deck-workbench-server-with-iced-wasm-ui.md b/documentation/docs/reference/decisions/0011-use-a-local-deck-workbench-server-with-iced-wasm-ui.md new file mode 100644 index 0000000..0756b81 --- /dev/null +++ b/documentation/docs/reference/decisions/0011-use-a-local-deck-workbench-server-with-iced-wasm-ui.md @@ -0,0 +1,51 @@ +# ADR-011: Use a Local Deck Workbench Server with an Iced/WASM UI + +**Date**: 2026-06-23 +**Status**: Accepted +**Deciders**: Project Lead + +## Context + +Deck maintainers and translators need an ergonomic interface for reviewing and editing deck source in context. The interface must show notes, produced cards, source-language text, target-language text, translation status, and pending changes without making a web app or generated artifact the source of truth. + +Brain Brew remains local-first: Canonical Deck files, manifests, and overlays are the canonical storage. A previous static HTML export was rejected because it was not an interactive workbench and did not match the intended workflow. + +## Decision + +Brain Brew will provide a **Deck Workbench** launched by `brainbrew workbench serve --manifest brainbrew.yaml`. + +The command runs a local Rust server bound to `127.0.0.1` on an available port by default, opens the browser unless `--no-open` is supplied, serves an Iced/WASM frontend, and exposes JSON HTTP APIs for workspace data and apply operations. The browser UI is not allowed to write source files directly. All file access, validation, and YAML mutation happen server-side through Brain Brew semantics. + +The first durable workbench capability centers on translation and source note-field editing in context. It supports language-first selection, note/card/source-string/card pivots, near-Anki previews, staged browser-local edits, apply previews, explicit Apply to canonical Translation Overlay YAML, and direct Canonical Deck File updates for existing source-language note field text. + +## Rationale + +**Pros:** + +- Preserves Brain Brew source files as canonical storage. +- Gives translators a real interactive application instead of static generated HTML. +- Keeps local file write permissions in the Rust process, not browser code. +- Allows the UI to use Rust/Iced state architecture while still running in a browser. +- Keeps room for future Deck Workbench modes beyond translation and note-field text editing. + +**Cons:** + +- Adds a WASM frontend build and serving pipeline. +- Iced web/WASM support is a more experimental frontend choice than a conventional TypeScript framework. +- The server/frontend API must be versioned and tested. +- Browser local storage drafts require careful stale-file detection before Apply. + +## Alternatives Considered + +- **Static self-contained HTML export**: rejected because it is not an interactive file-oriented workbench and creates a clumsy draft/apply flow. +- **Native Iced desktop app first**: rejected for now because the desired workflow is a browser/webview-style local UI and because the browser is better suited to card-like previews and local storage. +- **Conventional Svelte/TypeScript frontend**: rejected by preference in favor of Iced/WASM, while retaining a Rust server API boundary. +- **Web app as source of truth**: rejected because Brain Brew source remains YAML/overlay-based canonical storage. + +## Implications + +- `brainbrew workbench serve` must be treated as a local file-editing process and bind locally by default. +- The Iced/WASM UI should live in a separate workspace crate from the CLI server. +- Workbench APIs should be JSON HTTP first, with file fingerprints/polling for stale-session detection. +- Apply operations must show a validation/preview step before writing source files. +- Direct Canonical Deck File editing should start with existing note field text only; structural deck authoring remains out of scope until separately designed. diff --git a/documentation/docs/reference/decisions/0012-add-manifest-language-catalog-and-translation-profile.md b/documentation/docs/reference/decisions/0012-add-manifest-language-catalog-and-translation-profile.md new file mode 100644 index 0000000..f2a4ef7 --- /dev/null +++ b/documentation/docs/reference/decisions/0012-add-manifest-language-catalog-and-translation-profile.md @@ -0,0 +1,85 @@ +# ADR-012: Add Manifest Language Catalog and Translation Profile + +**Date**: 2026-06-23 +**Status**: Accepted +**Deciders**: Project Lead + +## Context + +Translation work should be language-first: translators choose a source or target language, see progress, select a primary target for previews, switch among target labels such as standard or extended, and create new languages without hand-editing every manifest target. Existing manifests have overlays and build targets, but no explicit language catalog or translation profile. + +Naming conventions such as `da-standard` are useful defaults, but relying on them alone would make the Deck Workbench infer too much from package-specific target names. + +## Decision + +Brain Brew manifests will gain top-level `languages:` metadata and a top-level `translation_profile:`. + +`languages:` is a map keyed by language code. Each entry includes `display_name`, `source`, `primary_target`, and `targets`, where `targets` maps friendly target labels to Build Target IDs. Source language entries set `source: true` and omit `translation_overlays`. Target language entries name their `translation_overlays` as a labeled map because one language may translate the base deck plus extension-specific content overlays. + +Example: + +```yaml +languages: + en: + display_name: English + source: true + primary_target: standard + targets: + standard: en-standard + extended: en-extended + da: + display_name: Danish + translation_overlays: + base: overlay.translation.da + hardcore: overlay.translation.hardcore.da + primary_target: standard + targets: + standard: da-standard + extended: da-extended +``` + +`translation_profile:` classifies translation work for UI progress and review. The initial shape includes structural note fields and optional path globs: + +```yaml +translation_profile: + structural_fields: + - field.flag + - field.map + optional_paths: + - deck.* + - note_types.* + - notes.*.tags.* +``` + +Main language completion counts non-structural note field text. Optional metadata such as deck description, note type names, field labels, and card templates appears in a separate optional checklist and does not block main completion. + +## Rationale + +**Pros:** + +- Makes language-first workflows explicit and reviewable. +- Avoids hard-coding target naming conventions into the workbench. +- Supports source-language entries without pretending the source language is a Translation Overlay. +- Lets new-language scaffolding mirror an existing/template language while previewing IDs and paths before writing. +- Handles packages where multiple extension/content overlays have separate translations for the same target language. +- Keeps translator-facing progress focused on deck content instead of structural/media/template noise. + +**Cons:** + +- Extends the manifest schema. +- Existing packages need metadata before the workbench can provide first-class language navigation. +- Progress semantics become distinct from strict raw translation coverage unless documented carefully. + +## Alternatives Considered + +- **Infer languages and primary targets from target names**: rejected because package naming conventions are not reliable enough for a durable UI. +- **Put all metadata under `workbench:`**: rejected because languages are broader than one UI and should be usable by CLI/reporting workflows too. +- **Use `variants:` inside language entries**: rejected because “Variant” is package/UG terminology; Brain Brew’s formal concrete composition concept is **Build Target**. +- **Classify structural fields in every Translation Overlay**: rejected because field roles are source/workspace facts, not per-language translation decisions. + +## Implications + +- Manifest parsing/formatting must preserve the new metadata deterministically. +- Workbench language dashboards should use `languages:` instead of target-name inference. +- New-language creation should default to a labeled base translation overlay such as `overlay.translation.`, `overlays/languages/.yaml`, extension-specific translation overlays that mirror the selected template language, and target IDs based on the selected template, with an editable preview before writing. +- Verification/reporting may expose both main deck-content completion and optional metadata status. diff --git a/documentation/docs/reference/decisions/0013-use-stale-translation-records-for-source-text-changes.md b/documentation/docs/reference/decisions/0013-use-stale-translation-records-for-source-text-changes.md new file mode 100644 index 0000000..1942949 --- /dev/null +++ b/documentation/docs/reference/decisions/0013-use-stale-translation-records-for-source-text-changes.md @@ -0,0 +1,59 @@ +# ADR-013: Use Stale Translation Records for Source Text Changes + +**Date**: 2026-06-23 +**Status**: Accepted +**Deciders**: Project Lead + +## Context + +Translation Dictionary keys are source text expected bases. When source-language note field text changes, existing target-language decisions keyed by the old source text no longer match, while the new source text appears missing. Some source edits are typo or wording fixes where the existing target text remains acceptable; others require a translator to review or retranslate. + +A source-language maintainer may not be able to update every target language immediately. Brain Brew needs a way to keep translated decks usable while making review debt explicit in source. + +## Decision + +Translation Dictionaries will support persisted **Stale Translation Records** under `translations.stale_records`. + +A stale record stores the old source, new source, prior target text, and optional context path when needed: + +```yaml +translations: + stale_records: + - old_source: Autonomous community of Spain. + new_source: Autonomous region of Spain. + target: Selvstyrende region af Spanien. + context: notes.note.canary-islands.fields.field.country-info +``` + +During compose/export, a stale record applies `target` to `new_source` but emits a warning that the translation needs review. Stale records do not block the main language completion percentage by default. `brainbrew verify` should warn by default and support an optional strict policy that fails when stale records remain. + +When the Deck Workbench applies source-language edits, affected translations default to creating stale records. Maintainers may explicitly choose to migrate an old source key to the new source key while preserving target text when they know the existing translation is still correct. Resolving a stale record creates or updates the normal direct/contextual translation for `new_source` and removes the stale record. + +## Rationale + +**Pros:** + +- Keeps translated decks usable after source edits. +- Preserves review debt in the repo for the appropriate translation maintainer. +- Avoids silent key migration when source meaning may have changed. +- Lets source-language maintainers safely update source text without being responsible for every target language. + +**Cons:** + +- Extends the Translation Dictionary schema. +- Compose/export can produce output from a known-stale translation unless strict policy is enabled. +- Tooling must surface warnings clearly so stale records are not forgotten. + +## Alternatives Considered + +- **Transient stale reports only**: rejected because review debt would not be preserved clearly in source after a source edit. +- **Always migrate keys automatically**: rejected because source changes can invalidate target text semantically. +- **Always fail until all translations are updated**: rejected because source maintainers may not know every target language and translated decks should remain usable with warnings. +- **Do not apply stale target text**: rejected because it would regress translated decks to source-language fallback immediately after source edits. + +## Implications + +- Translation coverage and context reports must include stale records as warning/review items. +- Workbench Apply preview for source edits must show affected target translations and default them to stale records. +- Release workflows can opt into strict stale-record failure. +- Documentation must explain the difference between stale records, missing translations, direct translations, contextual overrides, and global no-change decisions. diff --git a/documentation/docs/reference/decisions/0014-require-workbench-api-and-browser-e2e-tests.md b/documentation/docs/reference/decisions/0014-require-workbench-api-and-browser-e2e-tests.md new file mode 100644 index 0000000..1ef6c70 --- /dev/null +++ b/documentation/docs/reference/decisions/0014-require-workbench-api-and-browser-e2e-tests.md @@ -0,0 +1,45 @@ +# ADR-014: Require Workbench API and Browser E2E Tests + +**Date**: 2026-06-23 +**Status**: Accepted +**Deciders**: Project Lead + +## Context + +The Deck Workbench is a local file-editing application with a Rust server, Iced/WASM browser UI, browser-local staged edits, and Apply operations that mutate Canonical Deck files and Translation Overlays. Unit tests and service-level tests are not enough to prove that the intended workflow works end to end. + +## Decision + +Every user-visible Deck Workbench slice must have both API integration tests and browser E2E tests before it is considered done. + +API integration tests start the real workbench server on loopback with a temp workspace, call JSON HTTP endpoints, and inspect resulting source files. Browser E2E tests use Rust WebDriver tooling with `thirtyfour`, a devenv-provided Chromium/chromedriver, and the real Iced/WASM UI. Browser tests must exercise visible UI state, staged browser-local edits, Apply preview/validation, and resulting file changes. They should save screenshots/artifacts on failure, but visual snapshot diffing is not required initially. + +Workbench E2E tests live in a dedicated workspace test crate, `crates/brain-brew-workbench-e2e`, to keep browser automation dependencies out of production crates. The standard browser/API E2E gate is `devenv shell e2e`, and CI must run it. Purpose-built small fixtures should cover most scenarios, with at least one UG-like smoke path for real-world package shape. + +## Rationale + +**Pros:** + +- Proves the actual local-server/browser/file-editing workflow. +- Catches regressions in WASM UI wiring, browser local storage, HTTP APIs, and YAML mutation. +- Keeps slower browser automation out of the default Rust test crate dependencies. +- Makes workbench slices demonstrably usable before they are marked complete. + +**Cons:** + +- Adds Chromium/chromedriver and WebDriver test infrastructure to devenv/CI. +- Browser tests are slower and can be more fragile than pure Rust tests. +- Test fixtures and failure artifacts need ongoing maintenance. + +## Alternatives Considered + +- **Playwright**: rejected in favor of Rust WebDriver tests to keep the harness in Rust. +- **Rust integration tests only**: rejected because they do not exercise the browser UI, local storage, or real user workflow. +- **Browser E2E only**: rejected because API integration tests provide faster, more precise coverage of server behavior and file mutations. +- **Visual snapshot testing from the start**: deferred because semantic assertions plus failure screenshots provide useful coverage with less flakiness. + +## Implications + +- New workbench features need an E2E acceptance plan before implementation. +- CI must provision browser automation dependencies reproducibly. +- `devenv shell test` can remain the normal Rust test gate, while `devenv shell e2e` becomes the workbench E2E gate. diff --git a/documentation/docs/reference/decisions/README.md b/documentation/docs/reference/decisions/README.md index 426e761..11ec926 100644 --- a/documentation/docs/reference/decisions/README.md +++ b/documentation/docs/reference/decisions/README.md @@ -18,6 +18,10 @@ The ADRs are written in hindsight around the decisions that still shape the Rust | [0008](0008-use-source-variables-and-translation-dictionaries.md) | Use Source Variables and Translation Dictionaries | Accepted | 2026-05-25 | | [0009](0009-use-manifests-targets-and-locks-before-a-recipe-dsl.md) | Use Manifests, Targets, and Locks Before a Recipe DSL | Accepted | 2026-05-25 | | [0010](0010-fail-closed-on-unsupported-adapter-data.md) | Fail Closed on Unsupported Adapter Data | Accepted | 2026-05-25 | +| [0011](0011-use-a-local-deck-workbench-server-with-iced-wasm-ui.md) | Use a Local Deck Workbench Server with an Iced/WASM UI | Accepted | 2026-06-23 | +| [0012](0012-add-manifest-language-catalog-and-translation-profile.md) | Add Manifest Language Catalog and Translation Profile | Accepted | 2026-06-23 | +| [0013](0013-use-stale-translation-records-for-source-text-changes.md) | Use Stale Translation Records for Source Text Changes | Accepted | 2026-06-23 | +| [0014](0014-require-workbench-api-and-browser-e2e-tests.md) | Require Workbench API and Browser E2E Tests | Accepted | 2026-06-23 | ## Process From 1ff68823a575cd815cbfb6cad603ca4f6ad27ace Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Tue, 23 Jun 2026 16:58:10 +0200 Subject: [PATCH 038/142] Add manifest language catalog --- crates/brain-brew-formats/src/manifest.rs | 234 +++++++++++++++- .../brain-brew-formats/tests/manifest_yaml.rs | 261 ++++++++++++++++++ .../docs/authoring/manifests-targets.md | 43 +++ documentation/docs/authoring/translations.md | 2 +- 4 files changed, 537 insertions(+), 3 deletions(-) diff --git a/crates/brain-brew-formats/src/manifest.rs b/crates/brain-brew-formats/src/manifest.rs index af9ed7e..e4a5272 100644 --- a/crates/brain-brew-formats/src/manifest.rs +++ b/crates/brain-brew-formats/src/manifest.rs @@ -11,6 +11,8 @@ pub struct FederatedDeckManifest { pub include_roots: Vec, pub overlays: BTreeMap, pub targets: BTreeMap, + pub languages: BTreeMap, + pub translation_profile: TranslationProfile, } impl FederatedDeckManifest { @@ -36,6 +38,51 @@ impl FederatedDeckManifest { }) } + fn validate_language_catalog(&self) -> Result<(), ManifestError> { + for (code, language) in &self.languages { + if language.source && !language.translation_overlays.is_empty() { + return Err(ManifestError::SourceLanguageHasTranslationOverlays( + code.clone(), + )); + } + if !language.targets.contains_key(&language.primary_target) { + return Err(ManifestError::MissingLanguagePrimaryTargetLabel { + language: code.clone(), + primary_target: language.primary_target.clone(), + }); + } + for (label, target) in &language.targets { + if !self.targets.contains_key(target) { + return Err(ManifestError::MissingLanguageTarget { + language: code.clone(), + label: label.clone(), + target: target.clone(), + }); + } + } + for (label, overlay) in &language.translation_overlays { + let Some(entry) = self.overlays.get(overlay) else { + return Err(ManifestError::MissingLanguageTranslationOverlay { + language: code.clone(), + label: label.clone(), + overlay: overlay.clone(), + }); + }; + if let Some(kind) = &entry.kind + && kind != "translation" + { + return Err(ManifestError::LanguageTranslationOverlayHasWrongKind { + language: code.clone(), + label: label.clone(), + overlay: overlay.clone(), + kind: kind.clone(), + }); + } + } + } + Ok(()) + } + fn visit_overlay( &self, overlay: &str, @@ -107,6 +154,29 @@ pub struct BuildTarget { pub exports: TargetExports, } +/// One source or target language available in a Federated Deck workspace. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LanguageManifestEntry { + pub display_name: String, + pub source: bool, + pub translation_overlays: BTreeMap, + pub primary_target: String, + pub targets: BTreeMap, +} + +/// Workspace-wide translation classification used by language-first tooling. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TranslationProfile { + pub structural_fields: Vec, + pub optional_paths: Vec, +} + +impl TranslationProfile { + fn is_empty(&self) -> bool { + self.structural_fields.is_empty() && self.optional_paths.is_empty() + } +} + /// Optional reproducibility checks and default outputs for one target. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TargetExports { @@ -245,6 +315,54 @@ pub fn to_string(manifest: &FederatedDeckManifest) -> String { } } } + + if !manifest.languages.is_empty() { + out.push_str("languages:\n"); + for (code, language) in &manifest.languages { + out.push_str(&format!(" {code}:\n")); + out.push_str(&format!( + " display_name: {}\n", + yaml_scalar(&language.display_name) + )); + if language.source { + out.push_str(" source: true\n"); + } + if !language.translation_overlays.is_empty() { + out.push_str(" translation_overlays:\n"); + for (label, overlay) in &language.translation_overlays { + out.push_str(&format!(" {label}: {}\n", yaml_scalar(overlay))); + } + } + out.push_str(&format!( + " primary_target: {}\n", + yaml_scalar(&language.primary_target) + )); + if language.targets.is_empty() { + out.push_str(" targets: {}\n"); + } else { + out.push_str(" targets:\n"); + for (label, target) in &language.targets { + out.push_str(&format!(" {label}: {}\n", yaml_scalar(target))); + } + } + } + } + + if !manifest.translation_profile.is_empty() { + out.push_str("translation_profile:\n"); + if !manifest.translation_profile.structural_fields.is_empty() { + out.push_str(" structural_fields:\n"); + for field in &manifest.translation_profile.structural_fields { + out.push_str(&format!(" - {}\n", yaml_scalar(field))); + } + } + if !manifest.translation_profile.optional_paths.is_empty() { + out.push_str(" optional_paths:\n"); + for path in &manifest.translation_profile.optional_paths { + out.push_str(&format!(" - {}\n", yaml_scalar(path))); + } + } + } out } @@ -295,6 +413,27 @@ pub enum ManifestError { MissingOverlay(String), DependencyCycle(Vec), InvalidTranslationCoveragePolicy(String), + SourceLanguageHasTranslationOverlays(String), + MissingLanguagePrimaryTargetLabel { + language: String, + primary_target: String, + }, + MissingLanguageTarget { + language: String, + label: String, + target: String, + }, + MissingLanguageTranslationOverlay { + language: String, + label: String, + overlay: String, + }, + LanguageTranslationOverlayHasWrongKind { + language: String, + label: String, + overlay: String, + kind: String, + }, } impl fmt::Display for ManifestError { @@ -316,6 +455,42 @@ impl fmt::Display for ManifestError { f, "invalid translation coverage policy {policy:?}; expected lenient or strict" ), + Self::SourceLanguageHasTranslationOverlays(language) => write!( + f, + "manifest source language {language:?} must not declare translation_overlays" + ), + Self::MissingLanguagePrimaryTargetLabel { + language, + primary_target, + } => write!( + f, + "manifest language {language:?} primary_target {primary_target:?} is not present in its targets map" + ), + Self::MissingLanguageTarget { + language, + label, + target, + } => write!( + f, + "manifest language {language:?} target {label:?} references missing build target {target:?}" + ), + Self::MissingLanguageTranslationOverlay { + language, + label, + overlay, + } => write!( + f, + "manifest language {language:?} translation overlay {label:?} references missing overlay {overlay:?}" + ), + Self::LanguageTranslationOverlayHasWrongKind { + language, + label, + overlay, + kind, + } => write!( + f, + "manifest language {language:?} translation overlay {label:?} references overlay {overlay:?} with kind {kind:?}; expected translation" + ), } } } @@ -334,11 +509,15 @@ struct ManifestYaml { overlays: BTreeMap, #[serde(default)] targets: BTreeMap, + #[serde(default)] + languages: BTreeMap, + #[serde(default)] + translation_profile: TranslationProfileYaml, } impl ManifestYaml { fn into_manifest(self) -> Result { - Ok(FederatedDeckManifest { + let manifest = FederatedDeckManifest { package: self.package.map(PackageMetadataYaml::into_metadata), base: self.base, include_roots: self.include_roots, @@ -352,7 +531,15 @@ impl ManifestYaml { .into_iter() .map(|(id, target)| Ok((id, target.into_target()?))) .collect::>()?, - }) + languages: self + .languages + .into_iter() + .map(|(code, language)| (code, language.into_entry())) + .collect(), + translation_profile: self.translation_profile.into_profile(), + }; + manifest.validate_language_catalog()?; + Ok(manifest) } } @@ -428,6 +615,49 @@ impl BuildTargetYaml { } } +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct LanguageManifestEntryYaml { + display_name: String, + #[serde(default)] + source: bool, + #[serde(default)] + translation_overlays: BTreeMap, + primary_target: String, + #[serde(default)] + targets: BTreeMap, +} + +impl LanguageManifestEntryYaml { + fn into_entry(self) -> LanguageManifestEntry { + LanguageManifestEntry { + display_name: self.display_name, + source: self.source, + translation_overlays: self.translation_overlays, + primary_target: self.primary_target, + targets: self.targets, + } + } +} + +#[derive(Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct TranslationProfileYaml { + #[serde(default)] + structural_fields: Vec, + #[serde(default)] + optional_paths: Vec, +} + +impl TranslationProfileYaml { + fn into_profile(self) -> TranslationProfile { + TranslationProfile { + structural_fields: self.structural_fields, + optional_paths: self.optional_paths, + } + } +} + #[derive(Default, Deserialize)] #[serde(deny_unknown_fields)] struct TargetExportsYaml { diff --git a/crates/brain-brew-formats/tests/manifest_yaml.rs b/crates/brain-brew-formats/tests/manifest_yaml.rs index 30cb35f..e4a062f 100644 --- a/crates/brain-brew-formats/tests/manifest_yaml.rs +++ b/crates/brain-brew-formats/tests/manifest_yaml.rs @@ -166,6 +166,267 @@ targets: ); } +#[test] +fn parses_and_formats_language_catalog_and_translation_profile() { + let formatted = manifest::format_str( + r#" +translation_profile: + optional_paths: [note_types.*, deck.*, notes.*.tags.*] + structural_fields: [field.map, field.flag] +languages: + da: + targets: + extended: da-extended + standard: da-standard + primary_target: standard + translation_overlays: + hardcore: overlay.translation.hardcore.da + base: overlay.translation.da + display_name: Danish + en: + targets: + extended: en-extended + standard: en-standard + primary_target: standard + source: true + display_name: English +base: deck.yaml +overlays: + overlay.translation.hardcore.da: + file: overlays/languages/hardcore/da.yaml + kind: translation + overlay.translation.da: + file: overlays/languages/da.yaml + kind: translation +targets: + en-standard: + overlays: [] + en-extended: + overlays: [] + da-standard: + overlays: [overlay.translation.da] + da-extended: + overlays: [overlay.translation.da, overlay.translation.hardcore.da] +"#, + ) + .expect("manifest formats"); + + assert_eq!( + formatted, + r#"base: deck.yaml +overlays: + overlay.translation.da: + file: overlays/languages/da.yaml + kind: translation + overlay.translation.hardcore.da: + file: overlays/languages/hardcore/da.yaml + kind: translation +targets: + da-extended: + overlays: + - overlay.translation.da + - overlay.translation.hardcore.da + da-standard: + overlays: + - overlay.translation.da + en-extended: + overlays: [] + en-standard: + overlays: [] +languages: + da: + display_name: Danish + translation_overlays: + base: overlay.translation.da + hardcore: overlay.translation.hardcore.da + primary_target: standard + targets: + extended: da-extended + standard: da-standard + en: + display_name: English + source: true + primary_target: standard + targets: + extended: en-extended + standard: en-standard +translation_profile: + structural_fields: + - field.map + - field.flag + optional_paths: + - 'note_types.*' + - 'deck.*' + - 'notes.*.tags.*' +"# + ); + + let manifest = manifest::from_str(&formatted).expect("manifest parses"); + let english = &manifest.languages["en"]; + assert_eq!(english.display_name, "English"); + assert!(english.source); + assert!(english.translation_overlays.is_empty()); + assert_eq!(english.primary_target, "standard"); + assert_eq!(english.targets["extended"], "en-extended"); + + let danish = &manifest.languages["da"]; + assert_eq!(danish.display_name, "Danish"); + assert!(!danish.source); + assert_eq!( + danish.translation_overlays["base"], + "overlay.translation.da" + ); + assert_eq!( + danish.translation_overlays["hardcore"], + "overlay.translation.hardcore.da" + ); + assert_eq!(danish.primary_target, "standard"); + assert_eq!(danish.targets["standard"], "da-standard"); + + assert_eq!( + manifest.translation_profile.structural_fields, + vec!["field.map", "field.flag"] + ); + assert_eq!( + manifest.translation_profile.optional_paths, + vec!["note_types.*", "deck.*", "notes.*.tags.*"] + ); +} + +#[test] +fn language_catalog_reports_missing_target_references() { + let error = manifest::from_str( + r#" +base: deck.yaml +targets: {} +languages: + da: + display_name: Danish + primary_target: standard + targets: + standard: da-standard +"#, + ) + .expect_err("missing language target reference is reported"); + + assert_eq!( + error.to_string(), + "manifest language \"da\" target \"standard\" references missing build target \"da-standard\"" + ); +} + +#[test] +fn language_catalog_reports_missing_translation_overlay_references() { + let error = manifest::from_str( + r#" +base: deck.yaml +targets: + da-standard: + overlays: [] +languages: + da: + display_name: Danish + translation_overlays: + base: overlay.translation.da + primary_target: standard + targets: + standard: da-standard +"#, + ) + .expect_err("missing language overlay reference is reported"); + + assert_eq!( + error.to_string(), + "manifest language \"da\" translation overlay \"base\" references missing overlay \"overlay.translation.da\"" + ); +} + +#[test] +fn language_catalog_reports_non_translation_overlay_kind() { + let error = manifest::from_str( + r#" +base: deck.yaml +overlays: + overlay.extension.hardcore: + file: overlays/variants/hardcore.yaml + kind: extension +targets: + da-standard: + overlays: + - overlay.extension.hardcore +languages: + da: + display_name: Danish + translation_overlays: + hardcore: overlay.extension.hardcore + primary_target: standard + targets: + standard: da-standard +"#, + ) + .expect_err("translation overlay kind is validated when present"); + + assert_eq!( + error.to_string(), + "manifest language \"da\" translation overlay \"hardcore\" references overlay \"overlay.extension.hardcore\" with kind \"extension\"; expected translation" + ); +} + +#[test] +fn source_languages_reject_translation_overlays() { + let error = manifest::from_str( + r#" +base: deck.yaml +overlays: + overlay.translation.en: + file: overlays/languages/en.yaml + kind: translation +targets: + en-standard: + overlays: [] +languages: + en: + display_name: English + source: true + translation_overlays: + base: overlay.translation.en + primary_target: standard + targets: + standard: en-standard +"#, + ) + .expect_err("source language translation overlays are rejected"); + + assert_eq!( + error.to_string(), + "manifest source language \"en\" must not declare translation_overlays" + ); +} + +#[test] +fn language_catalog_reports_missing_primary_target_label() { + let error = manifest::from_str( + r#" +base: deck.yaml +targets: + da-standard: + overlays: [] +languages: + da: + display_name: Danish + primary_target: extended + targets: + standard: da-standard +"#, + ) + .expect_err("missing primary target label is reported"); + + assert_eq!( + error.to_string(), + "manifest language \"da\" primary_target \"extended\" is not present in its targets map" + ); +} + #[test] fn parses_and_formats_translation_coverage_policy() { let formatted = manifest::format_str( diff --git a/documentation/docs/authoring/manifests-targets.md b/documentation/docs/authoring/manifests-targets.md index 97d4b43..2378578 100644 --- a/documentation/docs/authoring/manifests-targets.md +++ b/documentation/docs/authoring/manifests-targets.md @@ -87,6 +87,49 @@ targets: Users and CI select targets instead of memorizing overlay paths. +## Language catalog + +A manifest can declare source and target languages explicitly so language-first tools such as the Deck Workbench do not infer meaning from target names. + +```yaml +languages: + en: + display_name: English + source: true + primary_target: standard + targets: + standard: en-standard + extended: en-extended + da: + display_name: Danish + translation_overlays: + base: overlay.translation.da + hardcore: overlay.translation.hardcore.da + primary_target: standard + targets: + standard: da-standard + extended: da-extended +``` + +`languages` is keyed by language code. A source language sets `source: true` and omits `translation_overlays`. A target language uses labeled `translation_overlays` so one language can translate base content plus extension-specific content, and its `targets` map connects friendly labels such as `standard` or `extended` to concrete build target IDs. + +## Translation profile + +`translation_profile` classifies progress for language-first review. Main completion focuses on non-structural note field text; optional metadata belongs in a separate checklist. + +```yaml +translation_profile: + structural_fields: + - field.flag + - field.map + optional_paths: + - deck.* + - note_types.* + - notes.*.tags.* +``` + +`structural_fields` excludes source fields such as flags or maps from main text completion. `optional_paths` marks deck metadata, note type/template metadata, tags, or other paths as optional review work instead of main translation completion. + ## Translation coverage policy Translated targets can choose how strictly `brainbrew verify` treats untranslated fallbacks: diff --git a/documentation/docs/authoring/translations.md b/documentation/docs/authoring/translations.md index fe44d0a..4320826 100644 --- a/documentation/docs/authoring/translations.md +++ b/documentation/docs/authoring/translations.md @@ -228,7 +228,7 @@ brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json brainbrew translations --manifest brainbrew.yaml --target de-standard --full ``` -Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Use `--context` for the first translator-in-context terminal view: it shows source English and target text together with translation status, note id, field id/name, note type, and card templates where the field appears. Filter or navigate the context view with `--language`, `--note`, `--field`, `--source`, `--duplicates`, `--status missing|stale|translated|direct|contextual|no-change`, and `--path-prefix`. Repeated source strings are shown as duplicate source groups so translators can see whether a reusable `direct`/`no_change` entry is enough or a path-specific `contextual` override is needed. YAML remains the canonical storage format; the terminal context view is the first ergonomic translator interface over that canonical data, and `--context --json` exposes the same note/field/card context model intended for the Deck Workbench. Use `--summary` for compact per-language/per-overlay counts; summary mode de-duplicates identical reports across target variants and includes direct translations, contextual overrides, no-change entries, target-language additions, variables, adapter IDs, raw untranslated fallbacks, actionable missing text translations, ignored entries, and stale/invalid keys. Human summary output uses narrow aligned columns by default; add `--summary --full` to include overlay/file columns, or `--summary --json` for complete machine-readable metadata. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. +Report mode is the default and never modifies files. The human report is translator-focused by default: it shows missing note-field text translations and summarizes structural/media/tag values separately so flag HTML, map HTML, tags, deck metadata, and template names do not drown out translator work. For language-first tools, `brainbrew.yaml` can declare `languages` and `translation_profile` metadata as described in [Manifests and targets](manifests-targets.md). Use `--full` when you intentionally want every scalar fallback. JSON output remains stable and includes all coverage entries. Use `--context` for the first translator-in-context terminal view: it shows source English and target text together with translation status, note id, field id/name, note type, and card templates where the field appears. Filter or navigate the context view with `--language`, `--note`, `--field`, `--source`, `--duplicates`, `--status missing|stale|translated|direct|contextual|no-change`, and `--path-prefix`. Repeated source strings are shown as duplicate source groups so translators can see whether a reusable `direct`/`no_change` entry is enough or a path-specific `contextual` override is needed. YAML remains the canonical storage format; the terminal context view is the first ergonomic translator interface over that canonical data, and `--context --json` exposes the same note/field/card context model intended for the Deck Workbench. Use `--summary` for compact per-language/per-overlay counts; summary mode de-duplicates identical reports across target variants and includes direct translations, contextual overrides, no-change entries, target-language additions, variables, adapter IDs, raw untranslated fallbacks, actionable missing text translations, ignored entries, and stale/invalid keys. Human summary output uses narrow aligned columns by default; add `--summary --full` to include overlay/file columns, or `--summary --json` for complete machine-readable metadata. Missing fallbacks are source strings that would currently pass through unchanged in a translated target. To seed translator work after adding English notes or fields, run apply explicitly: From 2b4c6f8252b6e1f2d318cc2c05ab2ef860828964 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Tue, 23 Jun 2026 17:17:27 +0200 Subject: [PATCH 039/142] Add workbench server scaffold --- Cargo.lock | 383 ++++++++++++++++++ crates/brain-brew-cli/Cargo.toml | 3 + crates/brain-brew-cli/src/commands/mod.rs | 1 + .../brain-brew-cli/src/commands/workbench.rs | 344 ++++++++++++++++ crates/brain-brew-cli/src/help.rs | 5 + crates/brain-brew-cli/src/main.rs | 1 + crates/brain-brew-cli/tests/cli.rs | 270 +++++++++++- documentation/docs/reference/cli.md | 10 + 8 files changed, 1015 insertions(+), 2 deletions(-) create mode 100644 crates/brain-brew-cli/src/commands/workbench.rs diff --git a/Cargo.lock b/Cargo.lock index 3a17cd1..581c5b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,64 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.22.1" @@ -54,6 +112,7 @@ dependencies = [ name = "brainbrew" version = "1.0.0-alpha.1" dependencies = [ + "axum", "base64", "brain-brew-core", "brain-brew-formats", @@ -64,9 +123,17 @@ dependencies = [ "sha2", "tar", "tempfile", + "tokio", + "tower-http", "ureq", ] +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + [[package]] name = "camino" version = "1.2.2" @@ -226,6 +293,45 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -281,6 +387,92 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -462,12 +654,34 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -537,6 +751,12 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "potential_utf" version = "0.1.5" @@ -725,6 +945,29 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -792,12 +1035,28 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -827,6 +1086,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "synstructure" version = "0.13.2" @@ -892,12 +1157,130 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "typenum" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/crates/brain-brew-cli/Cargo.toml b/crates/brain-brew-cli/Cargo.toml index 5693dc7..4272d01 100644 --- a/crates/brain-brew-cli/Cargo.toml +++ b/crates/brain-brew-cli/Cargo.toml @@ -18,6 +18,7 @@ name = "brainbrew" path = "src/main.rs" [dependencies] +axum = "0.8" base64 = "0.22" crossterm = "0.28" flate2 = "1" @@ -28,6 +29,8 @@ serde_json = "1" sha2 = "0.10" tar = "0.4" tempfile = "3" +tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] } +tower-http = { version = "0.6", features = ["fs"] } ureq = "2" [lints] diff --git a/crates/brain-brew-cli/src/commands/mod.rs b/crates/brain-brew-cli/src/commands/mod.rs index 41cfb62..b9003bc 100644 --- a/crates/brain-brew-cli/src/commands/mod.rs +++ b/crates/brain-brew-cli/src/commands/mod.rs @@ -9,3 +9,4 @@ pub(crate) mod targets; pub(crate) mod translations; pub(crate) mod validate; pub(crate) mod verify; +pub(crate) mod workbench; diff --git a/crates/brain-brew-cli/src/commands/workbench.rs b/crates/brain-brew-cli/src/commands/workbench.rs new file mode 100644 index 0000000..8283196 --- /dev/null +++ b/crates/brain-brew-cli/src/commands/workbench.rs @@ -0,0 +1,344 @@ +use std::collections::BTreeMap; +use std::fs; +use std::io::{self, Write}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Arc; + +use axum::extract::State; +use axum::http::StatusCode; +use axum::response::Html; +use axum::routing::get; +use axum::{Json, Router}; +use brain_brew_formats::manifest::{FederatedDeckManifest, LanguageManifestEntry}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use tokio::net::TcpListener; +use tower_http::services::{ServeDir, ServeFile}; + +use crate::help; +use crate::io::{manifest_root, read_manifest}; + +const EMBEDDED_WORKBENCH_INDEX: &str = r#" + + + + + Brain Brew Deck Workbench + + +
+

Brain Brew Deck Workbench

+

The embedded workbench shell is installed. Build the WASM UI or use --dev-assets for a development shell.

+
+ + +"#; + +pub(crate) fn run(args: &[String]) -> Result<(), String> { + if args.len() == 1 && (args[0] == "--help" || args[0] == "-h") { + print!( + "{}", + help::command("workbench").expect("workbench help exists") + ); + return Ok(()); + } + + let Some(subcommand) = args.first().map(String::as_str) else { + return Err(help::usage_error( + "workbench", + "usage: brainbrew workbench serve --manifest brainbrew.yaml", + )); + }; + if subcommand == "serve" + && args + .get(1) + .is_some_and(|arg| arg == "--help" || arg == "-h") + { + print!( + "{}", + help::command("workbench").expect("workbench help exists") + ); + return Ok(()); + } + match subcommand { + "serve" => serve_sync(parse_serve_args(&args[1..])?), + "--help" | "-h" => { + print!( + "{}", + help::command("workbench").expect("workbench help exists") + ); + Ok(()) + } + other => Err(format!("unexpected workbench subcommand {other:?}")), + } +} + +#[derive(Clone, Debug)] +struct ServeArgs { + manifest_path: PathBuf, + port: u16, + open_browser: bool, + dev_assets: Option, +} + +fn parse_serve_args(args: &[String]) -> Result { + let mut parsed = ServeArgs { + manifest_path: PathBuf::from("brainbrew.yaml"), + port: 0, + open_browser: true, + dev_assets: None, + }; + let mut index = 0; + while index < args.len() { + match args[index].as_str() { + "--manifest" => { + let Some(path) = args.get(index + 1) else { + return Err("--manifest requires a path".to_owned()); + }; + parsed.manifest_path = PathBuf::from(path); + index += 2; + } + "--port" => { + let Some(port) = args.get(index + 1) else { + return Err("--port requires a port number".to_owned()); + }; + parsed.port = port + .parse::() + .map_err(|_| format!("invalid --port value {port:?}"))?; + index += 2; + } + "--no-open" => { + parsed.open_browser = false; + index += 1; + } + "--dev-assets" => { + let Some(path) = args.get(index + 1) else { + return Err("--dev-assets requires a directory".to_owned()); + }; + parsed.dev_assets = Some(PathBuf::from(path)); + index += 2; + } + other => return Err(format!("unexpected workbench serve argument {other:?}")), + } + } + if let Some(path) = &parsed.dev_assets + && !path.is_dir() + { + return Err(format!( + "--dev-assets directory {} does not exist", + path.display() + )); + } + Ok(parsed) +} + +fn serve_sync(args: ServeArgs) -> Result<(), String> { + let runtime = tokio::runtime::Runtime::new() + .map_err(|error| format!("failed to start Tokio runtime: {error}"))?; + runtime.block_on(serve(args)) +} + +async fn serve(args: ServeArgs) -> Result<(), String> { + let manifest = read_manifest(&args.manifest_path)?; + let metadata = Arc::new(WorkspaceMetadata::load(&args.manifest_path, manifest)); + let listener = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), args.port)) + .await + .map_err(|error| format!("failed to bind workbench server: {error}"))?; + let address = listener + .local_addr() + .map_err(|error| format!("failed to inspect workbench server address: {error}"))?; + let url = format!("http://{address}"); + + println!("Workbench listening at {url}"); + io::stdout() + .flush() + .map_err(|error| format!("failed to flush workbench URL: {error}"))?; + + if args.open_browser { + open_browser(&url); + } + + axum::serve(listener, app(metadata, args.dev_assets)) + .await + .map_err(|error| format!("workbench server failed: {error}")) +} + +fn app(metadata: Arc, dev_assets: Option) -> Router { + let router = Router::new() + .route("/api/health", get(health)) + .route("/api/workspace", get(workspace)) + .with_state(metadata); + + if let Some(assets) = dev_assets { + router.fallback_service( + ServeDir::new(&assets).not_found_service(ServeFile::new(assets.join("index.html"))), + ) + } else { + router + .route("/", get(embedded_index)) + .fallback(get(embedded_index)) + } +} + +async fn health(State(metadata): State>) -> Json { + Json(json!({ + "status": "ok", + "manifest": metadata.manifest_path.display().to_string(), + })) +} + +async fn workspace( + State(metadata): State>, +) -> Result, (StatusCode, String)> { + metadata + .workspace_json() + .map(Json) + .map_err(|error| (StatusCode::INTERNAL_SERVER_ERROR, error)) +} + +async fn embedded_index() -> Html<&'static str> { + Html(EMBEDDED_WORKBENCH_INDEX) +} + +#[derive(Debug)] +struct WorkspaceMetadata { + manifest_path: PathBuf, + manifest_root: PathBuf, + manifest: FederatedDeckManifest, +} + +impl WorkspaceMetadata { + fn load(manifest_path: &Path, manifest: FederatedDeckManifest) -> Self { + Self { + manifest_path: manifest_path.to_path_buf(), + manifest_root: manifest_root(manifest_path), + manifest, + } + } + + fn workspace_json(&self) -> Result { + let fingerprints = + file_fingerprints(&self.manifest_path, &self.manifest_root, &self.manifest)?; + Ok(json!({ + "manifest": self.manifest_path.display().to_string(), + "manifest_root": self.manifest_root.display().to_string(), + "languages": languages_json(&self.manifest), + "target_labels": target_labels_json(&self.manifest), + "targets": targets_json(&self.manifest), + "translation_profile": { + "structural_fields": self.manifest.translation_profile.structural_fields, + "optional_paths": self.manifest.translation_profile.optional_paths, + }, + "fingerprints": fingerprints, + })) + } +} + +fn languages_json(manifest: &FederatedDeckManifest) -> Value { + let languages = manifest + .languages + .iter() + .map(|(code, language)| (code.clone(), language_json(language))) + .collect::>(); + Value::Object(languages) +} + +fn language_json(language: &LanguageManifestEntry) -> Value { + json!({ + "display_name": language.display_name, + "source": language.source, + "translation_overlays": language.translation_overlays, + "primary_target": language.primary_target, + "targets": language.targets, + }) +} + +fn target_labels_json(manifest: &FederatedDeckManifest) -> Value { + let mut labels = BTreeMap::>::new(); + for (language, entry) in &manifest.languages { + for (label, target) in &entry.targets { + labels.entry(target.clone()).or_default().push(json!({ + "language": language, + "label": label, + })); + } + } + json!(labels) +} + +fn targets_json(manifest: &FederatedDeckManifest) -> Value { + let targets = manifest + .targets + .iter() + .map(|(name, target)| { + ( + name.clone(), + json!({ + "extends": target.extends, + "overlays": target.overlays, + }), + ) + }) + .collect::>(); + Value::Object(targets) +} + +fn file_fingerprints( + manifest_path: &Path, + root: &Path, + manifest: &FederatedDeckManifest, +) -> Result, String> { + let mut files = vec![manifest_path.to_path_buf(), root.join(&manifest.base)]; + for overlay in manifest.overlays.values() { + files.push(root.join(&overlay.file)); + } + files.sort(); + files.dedup(); + files + .into_iter() + .map(|path| file_fingerprint(root, &path)) + .collect() +} + +fn file_fingerprint(root: &Path, path: &Path) -> Result { + let bytes = fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?; + let hash = Sha256::digest(&bytes); + Ok(json!({ + "path": workspace_path(root, path), + "sha256": format!("{hash:x}"), + })) +} + +fn workspace_path(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .unwrap_or(path) + .display() + .to_string() +} + +fn open_browser(url: &str) { + #[cfg(target_os = "windows")] + let mut command = { + let mut command = Command::new("cmd"); + command.args(["/C", "start", "", url]); + command + }; + + #[cfg(target_os = "macos")] + let mut command = { + let mut command = Command::new("open"); + command.arg(url); + command + }; + + #[cfg(all(unix, not(target_os = "macos")))] + let mut command = { + let mut command = Command::new("xdg-open"); + command.arg(url); + command + }; + + let _ = command.spawn(); +} diff --git a/crates/brain-brew-cli/src/help.rs b/crates/brain-brew-cli/src/help.rs index 03a523f..bbfe72c 100644 --- a/crates/brain-brew-cli/src/help.rs +++ b/crates/brain-brew-cli/src/help.rs @@ -14,6 +14,7 @@ pub(crate) fn general() -> String { " lock Update or verify locked federated package inputs\n", " targets List manifest targets\n", " translations Report/apply translation coverage (aliases: translate, translation)\n", + " workbench Serve the local Deck Workbench browser UI and JSON API\n", " verify Run manifest formatting, composition, validation, media, and golden checks\n", " explain Explain a manifest target and its overlay stack\n", " diff Compare decks semantically, or emit an overlay draft\n\n", @@ -25,6 +26,7 @@ pub(crate) fn general() -> String { " brainbrew lock update --package anki-geo.ultimate-geography --path ../ultimate-geography\n", " brainbrew export crowdanki --manifest brainbrew.yaml --target de-extended --media-root media/\n", " brainbrew translate --manifest brainbrew.yaml --target de-standard\n", + " brainbrew workbench serve --manifest brainbrew.yaml\n", " brainbrew verify --manifest brainbrew.yaml --all-targets\n\n", "Run `brainbrew --help` for command-specific examples.\n", ), @@ -58,6 +60,9 @@ pub(crate) fn command(name: &str) -> Option<&'static str> { "translations" | "translate" | "translation" => Some( "Usage:\n brainbrew translate [--manifest brainbrew.yaml] [--target | --all-targets] [--language ] [--overlay ] [--note ] [--field ] [--source ] [--duplicates] [--status ] [--path-prefix ] [--apply | --summary | --context] [--full] [--interactive | --no-interactive] [--json]\n\nInteractive examples:\n brainbrew translate\n brainbrew translate --manifest fixtures/ultimate-geography/brainbrew.yaml\n brainbrew translate --interactive\n\nScriptable examples:\n brainbrew translations --manifest brainbrew.yaml --target da-standard\n brainbrew translations --manifest brainbrew.yaml --all-targets --language de\n brainbrew translations --manifest brainbrew.yaml --target de-standard --note note.finland --field field.country\n brainbrew translations --manifest brainbrew.yaml --target de-standard --context --source Georgia\n brainbrew translations --manifest brainbrew.yaml --target da-standard --apply\n brainbrew translations --manifest brainbrew.yaml --all-targets --summary --json\n brainbrew translations --manifest brainbrew.yaml --target da-standard --json\n\nReport mode is the default and never edits files. In an interactive terminal, missing choices open arrow-key selectors for target, scope, and mode; language and overlay selectors appear only when they disambiguate the selected targets. The human report is translator-focused by default: structural/media/tag fallbacks are hidden and summarized. Use --full to include every scalar fallback. --context shows source and target strings in note/field/card context; combine it with --note, --field, --source, --duplicates, --status, and --language to navigate the translator context view. --summary exports compact per-language/per-overlay counts and de-duplicates identical reports across target variants; human summary output uses narrow aligned columns by default, and --summary --full adds overlay/file columns. --apply inserts source->source translation stubs for missing text fallbacks in non-interactive mode, while interactive apply lets you select rows with Space/Enter and choose one action for all selected rows (mark no-change, direct/contextual stub, ignore-path, skip) or decide per row. Use --path-prefix with paths from a diff to scope a changed-base review.\n", ), + "workbench" => Some( + "Usage:\n brainbrew workbench serve [--manifest brainbrew.yaml] [--port ] [--no-open] [--dev-assets ]\n\nExamples:\n brainbrew workbench serve --manifest brainbrew.yaml\n brainbrew workbench serve --manifest brainbrew.yaml --port 0 --no-open\n brainbrew workbench serve --manifest brainbrew.yaml --dev-assets build/workbench\n\nStarts a localhost Deck Workbench server bound to 127.0.0.1. Port 0 selects an available port. Release builds serve embedded UI assets; --dev-assets serves a development asset directory. The JSON API exposes health and workspace metadata for the browser UI.\n", + ), "verify" => Some( "Usage:\n brainbrew verify [--manifest brainbrew.yaml] (--all-targets | --target ) [--include package/brainbrew.yaml ...] [--package-root packages/] [--media-root media/] [--translation-coverage lenient|strict]\n\nExamples:\n brainbrew verify --manifest brainbrew.yaml --all-targets\n brainbrew verify --manifest brainbrew.yaml --target da-standard\n brainbrew verify --manifest america/brainbrew.yaml --include ultimate-geography/brainbrew.yaml --target en-america\n brainbrew verify --manifest brainbrew.yaml --all-targets --media-root media/\n brainbrew verify --manifest brainbrew.yaml --target de-release --translation-coverage strict\n", ), diff --git a/crates/brain-brew-cli/src/main.rs b/crates/brain-brew-cli/src/main.rs index 34a09fd..00ebdfc 100644 --- a/crates/brain-brew-cli/src/main.rs +++ b/crates/brain-brew-cli/src/main.rs @@ -47,6 +47,7 @@ fn run() -> Result<(), String> { "targets" => commands::targets::run(&args[1..]), "translations" => commands::translations::run(&args[1..]), "verify" => commands::verify::run(&args[1..]), + "workbench" => commands::workbench::run(&args[1..]), "explain" => commands::explain::run(&args[1..]), "diff" => commands::diff::run(&args[1..]), "--help" | "-h" => { diff --git a/crates/brain-brew-cli/tests/cli.rs b/crates/brain-brew-cli/tests/cli.rs index addba03..fcd9728 100644 --- a/crates/brain-brew-cli/tests/cli.rs +++ b/crates/brain-brew-cli/tests/cli.rs @@ -1,7 +1,7 @@ use std::fs; -use std::io::Write; +use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; +use std::process::{Child, ChildStdout, Command, Stdio}; use std::time::{SystemTime, UNIX_EPOCH}; use flate2::Compression; @@ -34,6 +34,177 @@ fn command_help_includes_focused_examples() { )); } +#[test] +fn workbench_help_includes_serve_entrypoint() { + let top = run(["--help"]); + assert!(top.status.success(), "stderr: {}", stderr(&top)); + assert!(stdout(&top).contains("workbench")); + + let command = run(["workbench", "--help"]); + assert!(command.status.success(), "stderr: {}", stderr(&command)); + let out = stdout(&command); + assert!(out.contains("brainbrew workbench serve --manifest brainbrew.yaml")); + assert!(out.contains("--port")); + assert!(out.contains("--no-open")); + assert!(out.contains("--dev-assets")); + + let serve = run(["workbench", "serve", "--help"]); + assert!(serve.status.success(), "stderr: {}", stderr(&serve)); + assert!(stdout(&serve).contains("brainbrew workbench serve --manifest brainbrew.yaml")); +} + +#[test] +fn workbench_serve_exposes_workspace_metadata_api() { + let dir = temp_dir("workbench-api"); + write_workbench_workspace(&dir); + let server = spawn_workbench_server([ + "workbench", + "serve", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--port", + "0", + "--no-open", + ]); + + let health = get_json(&server.url("/api/health")); + assert_eq!(health["status"], "ok"); + assert_eq!( + health["manifest"], + dir.join("brainbrew.yaml").display().to_string() + ); + + let workspace = get_json(&server.url("/api/workspace")); + assert_eq!( + workspace["manifest"], + dir.join("brainbrew.yaml").display().to_string() + ); + assert_eq!(workspace["languages"]["en"]["display_name"], "English"); + assert_eq!(workspace["languages"]["en"]["source"], true); + assert_eq!( + workspace["languages"]["da"]["translation_overlays"]["base"], + "overlay.translation.da" + ); + assert_eq!( + workspace["languages"]["da"]["targets"]["standard"], + "da-standard" + ); + assert_eq!( + workspace["target_labels"]["da-standard"][0]["language"], + "da" + ); + assert_eq!( + workspace["target_labels"]["da-standard"][0]["label"], + "standard" + ); + assert_eq!( + workspace["translation_profile"]["structural_fields"][0], + "field.flag" + ); + let fingerprints = workspace["fingerprints"].as_array().unwrap(); + assert!( + fingerprints + .iter() + .any(|entry| entry["path"] == "brainbrew.yaml") + ); + assert!( + fingerprints + .iter() + .any(|entry| entry["path"] == "deck.yaml") + ); + assert!(fingerprints.iter().any(|entry| entry["path"] == "da.yaml")); + assert!( + fingerprints + .iter() + .all(|entry| entry["sha256"].as_str().unwrap().len() == 64) + ); + let original_da_fingerprint = fingerprints + .iter() + .find(|entry| entry["path"] == "da.yaml") + .unwrap()["sha256"] + .as_str() + .unwrap() + .to_owned(); + + fs::write( + dir.join("da.yaml"), + r#"id: overlay.translation.da +kind: translation +translations: + direct: + Finland: Suomi +"#, + ) + .unwrap(); + let updated = get_json(&server.url("/api/workspace")); + let updated_da_fingerprint = updated["fingerprints"] + .as_array() + .unwrap() + .iter() + .find(|entry| entry["path"] == "da.yaml") + .unwrap()["sha256"] + .as_str() + .unwrap(); + assert_ne!(updated_da_fingerprint, original_da_fingerprint); +} + +#[test] +fn workbench_serve_uses_available_port_and_embedded_assets_by_default() { + let dir = temp_dir("workbench-default-assets"); + write_workbench_workspace(&dir); + + let server = spawn_workbench_server([ + "workbench", + "serve", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--no-open", + ]); + + let response = ureq::get(&server.url("/")).call().expect("GET / succeeds"); + assert_eq!(response.status(), 200); + assert!( + response + .into_string() + .unwrap() + .contains("Brain Brew Deck Workbench") + ); +} + +#[test] +fn workbench_serve_can_use_dev_asset_directory() { + let dir = temp_dir("workbench-dev-assets"); + write_workbench_workspace(&dir); + let assets = dir.join("assets"); + fs::create_dir_all(&assets).unwrap(); + fs::write( + assets.join("index.html"), + "
dev workbench shell
", + ) + .unwrap(); + + let server = spawn_workbench_server([ + "workbench", + "serve", + "--manifest", + dir.join("brainbrew.yaml").to_str().unwrap(), + "--port", + "0", + "--no-open", + "--dev-assets", + assets.to_str().unwrap(), + ]); + + let response = ureq::get(&server.url("/")).call().expect("GET / succeeds"); + assert_eq!(response.status(), 200); + assert!( + response + .into_string() + .unwrap() + .contains("dev workbench shell") + ); +} + #[test] fn validate_without_args_shows_usage_examples() { let output = run(["validate"]); @@ -2484,6 +2655,52 @@ fn run_with_cache(args: [&str; N], cache: &Path) -> std::process .expect("command runs") } +struct RunningWorkbenchServer { + child: Child, + base_url: String, + _stdout: BufReader, +} + +impl RunningWorkbenchServer { + fn url(&self, path: &str) -> String { + format!("{}{}", self.base_url, path) + } +} + +impl Drop for RunningWorkbenchServer { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} + +fn spawn_workbench_server(args: [&str; N]) -> RunningWorkbenchServer { + let mut child = Command::new(env!("CARGO_BIN_EXE_brainbrew")) + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("workbench server spawns"); + let stdout = child.stdout.take().expect("server stdout is piped"); + let mut reader = BufReader::new(stdout); + let mut line = String::new(); + reader.read_line(&mut line).expect("server prints URL"); + let Some(base_url) = line.strip_prefix("Workbench listening at ") else { + panic!("unexpected workbench server line: {line:?}"); + }; + RunningWorkbenchServer { + child, + base_url: base_url.trim().to_owned(), + _stdout: reader, + } +} + +fn get_json(url: &str) -> serde_json::Value { + let response = ureq::get(url).call().expect("GET succeeds"); + assert_eq!(response.status(), 200); + serde_json::from_str(&response.into_string().unwrap()).expect("response is JSON") +} + fn workspace_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() @@ -2499,6 +2716,55 @@ fn write_manifest_workspace(dir: &Path) { fs::write(dir.join("brainbrew.yaml"), MANIFEST_YAML).unwrap(); } +fn write_workbench_workspace(dir: &Path) { + fs::write(dir.join("deck.yaml"), SAMPLE_CANONICAL_YAML).unwrap(); + fs::write( + dir.join("da.yaml"), + r#"id: overlay.translation.da +kind: translation +translations: + direct: + Finland: Finland +"#, + ) + .unwrap(); + fs::write( + dir.join("brainbrew.yaml"), + r#"base: deck.yaml +overlays: + overlay.translation.da: + file: da.yaml + kind: translation +targets: + da-standard: + overlays: + - overlay.translation.da + en-standard: + overlays: [] +languages: + da: + display_name: Danish + translation_overlays: + base: overlay.translation.da + primary_target: standard + targets: + standard: da-standard + en: + display_name: English + source: true + primary_target: standard + targets: + standard: en-standard +translation_profile: + structural_fields: + - field.flag + optional_paths: + - deck.* +"#, + ) + .unwrap(); +} + fn write_translation_workspace(dir: &Path) { let deck = SAMPLE_CANONICAL_YAML .replace("field.flag: ''", "field.flag: ''") diff --git a/documentation/docs/reference/cli.md b/documentation/docs/reference/cli.md index bc39f1e..6c7746e 100644 --- a/documentation/docs/reference/cli.md +++ b/documentation/docs/reference/cli.md @@ -89,6 +89,16 @@ brainbrew translations --manifest brainbrew.yaml --all-targets --summary Reports translation coverage, shows terminal note/card context, summarizes translation state, or applies reviewed translation stubs back to canonical translation overlay YAML. +## `workbench` + +```bash +brainbrew workbench serve --manifest brainbrew.yaml +brainbrew workbench serve --manifest brainbrew.yaml --port 0 --no-open +brainbrew workbench serve --manifest brainbrew.yaml --dev-assets build/workbench +``` + +Starts the local Deck Workbench server on `127.0.0.1`, serving the browser UI plus JSON APIs for health and workspace metadata. + ## `verify` ```bash From aa711b37103cb796b5bee16cfaf3fb9e3b120391 Mon Sep 17 00:00:00 2001 From: Jordan Munch O'Hare Date: Tue, 23 Jun 2026 17:42:25 +0200 Subject: [PATCH 040/142] Add workbench Iced WASM scaffold --- Cargo.lock | 4892 ++++++++++++++--- Cargo.toml | 1 + crates/brain-brew-cli/Cargo.toml | 1 + ...rain_brew_workbench_ui-9575fc3c29e6b0e6.js | 1186 ++++ ...brew_workbench_ui-9575fc3c29e6b0e6_bg.wasm | Bin 0 -> 3405849 bytes .../assets/workbench/index.html | 24 + .../workbench/workbench-dc5d231e5bc3af58.css | 11 + .../brain-brew-cli/src/commands/workbench.rs | 66 +- crates/brain-brew-cli/src/help.rs | 2 +- crates/brain-brew-cli/tests/cli.rs | 56 +- crates/brain-brew-workbench-ui/Cargo.toml | 27 + crates/brain-brew-workbench-ui/index.html | 13 + crates/brain-brew-workbench-ui/src/lib.rs | 184 + crates/brain-brew-workbench-ui/src/main.rs | 3 + .../static/workbench.css | 11 + .../tests/workspace_summary.rs | 17 + devenv.nix | 19 + documentation/docs/reference/cli.md | 4 +- documentation/docs/reference/workbench.md | 36 + documentation/sidebars.js | 1 + 20 files changed, 5812 insertions(+), 742 deletions(-) create mode 100644 crates/brain-brew-cli/assets/workbench/brain_brew_workbench_ui-9575fc3c29e6b0e6.js create mode 100644 crates/brain-brew-cli/assets/workbench/brain_brew_workbench_ui-9575fc3c29e6b0e6_bg.wasm create mode 100644 crates/brain-brew-cli/assets/workbench/index.html create mode 100644 crates/brain-brew-cli/assets/workbench/workbench-dc5d231e5bc3af58.css create mode 100644 crates/brain-brew-workbench-ui/Cargo.toml create mode 100644 crates/brain-brew-workbench-ui/index.html create mode 100644 crates/brain-brew-workbench-ui/src/lib.rs create mode 100644 crates/brain-brew-workbench-ui/src/main.rs create mode 100644 crates/brain-brew-workbench-ui/static/workbench.css create mode 100644 crates/brain-brew-workbench-ui/tests/workspace_summary.rs create mode 100644 documentation/docs/reference/workbench.md diff --git a/Cargo.lock b/Cargo.lock index 581c5b1..51e4c1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,24 +2,260 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + [[package]] name = "adler2" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.11.1", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-build" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fc9904ad2ad097c3c1cfe2eacaaf0fc24710936fa9ed941cb310b7c6ed2ab7" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + [[package]] name = "axum" version = "0.8.9" @@ -78,12 +314,39 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -93,6 +356,37 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brain-brew-core" version = "1.0.0-alpha.1" @@ -108,6 +402,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "brain-brew-workbench-ui" +version = "1.0.0-alpha.1" +dependencies = [ + "gloo-net", + "iced", + "serde_json", +] + [[package]] name = "brainbrew" version = "1.0.0-alpha.1" @@ -118,6 +421,7 @@ dependencies = [ "brain-brew-formats", "crossterm", "flate2", + "include_dir", "nix-nar", "serde_json", "sha2", @@ -128,12 +432,89 @@ dependencies = [ "ureq", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bytes" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.11.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.11.1", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + [[package]] name = "camino" version = "1.2.2" @@ -147,6 +528,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -157,1297 +540,4114 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ - "libc", + "error-code", ] [[package]] -name = "crc32fast" -version = "1.5.0" +name = "clipboard_macos" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" dependencies = [ - "cfg-if", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] -name = "crossterm" -version = "0.28.1" +name = "clipboard_wayland" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" dependencies = [ - "bitflags", - "crossterm_winapi", - "mio", - "parking_lot", - "rustix 0.38.44", - "signal-hook", - "signal-hook-mio", - "winapi", + "smithay-clipboard", ] [[package]] -name = "crossterm_winapi" -version = "0.9.1" +name = "clipboard_x11" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +checksum = "bd63e33452ffdafd39924c4f05a5dd1e94db646c779c6bd59148a3d95fff5ad4" dependencies = [ - "winapi", + "thiserror 2.0.18", + "x11rb", ] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "codespan-reporting" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ - "generic-array", - "typenum", + "serde", + "termcolor", + "unicode-width", ] [[package]] -name = "digest" -version = "0.10.7" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "block-buffer", - "crypto-common", + "bytes", + "memchr", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ - "proc-macro2", - "quote", - "syn", + "crossbeam-utils", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "errno" -version = "0.3.14" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ + "core-foundation-sys", "libc", - "windows-sys 0.61.2", ] [[package]] -name = "fastrand" -version = "2.4.1" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "filetime" -version = "0.2.29" +name = "core-graphics" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ - "cfg-if", + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", "libc", ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "core-graphics-types" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] [[package]] -name = "flate2" -version = "1.1.9" +name = "core-graphics-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "crc32fast", - "miniz_oxide", + "bitflags 2.11.1", + "core-foundation 0.10.1", + "libc", ] [[package]] -name = "foldhash" -version = "0.1.5" +name = "core_maths" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "cosmic-text" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "173852283a9a57a3cbe365d86e74dc428a09c50421477d5ad6fe9d9509e37737" dependencies = [ - "percent-encoding", + "bitflags 2.11.1", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 1.1.0", + "self_cell", + "skrifa 0.37.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "futures-core", + "libc", ] [[package]] -name = "futures-core" -version = "0.3.32" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] [[package]] -name = "futures-sink" -version = "0.3.32" +name = "crossbeam-utils" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "futures-task" -version = "0.3.32" +name = "crossterm" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] [[package]] -name = "futures-util" -version = "0.3.32" +name = "crossterm_winapi" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", + "winapi", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "cryoglyph" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc795bdbccdbd461736fb163930a009da6597b226d6f6fce33e7a8eb6ec519" dependencies = [ - "typenum", - "version_check", + "cosmic-text", + "etagere", + "lru", + "rustc-hash 2.1.2", + "wgpu", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "cfg-if", - "libc", - "wasi", + "generic-array", + "typenum", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "ctor" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", + "dtor", ] [[package]] -name = "hashbrown" -version = "0.15.5" +name = "cursor-icon" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "foldhash", + "block-buffer", + "crypto-common", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "dispatch" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" [[package]] -name = "heck" -version = "0.5.0" +name = "dispatch2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", +] [[package]] -name = "http" -version = "1.4.2" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "bytes", - "itoa", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "http-body" -version = "1.0.1" +name = "dlib" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "bytes", - "http", + "libloading", ] [[package]] -name = "http-body-util" -version = "0.1.3" +name = "document-features" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", + "litrs", ] [[package]] -name = "http-range-header" -version = "0.4.2" +name = "downcast-rs" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] -name = "httparse" -version = "1.10.1" +name = "dpi" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] -name = "httpdate" -version = "1.0.3" +name = "dtor" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" [[package]] -name = "hyper" -version = "1.10.1" +name = "endi" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", -] +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] -name = "hyper-util" -version = "0.1.20" +name = "enumflags2" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ - "bytes", - "http", - "http-body", - "hyper", - "pin-project-lite", - "tokio", - "tower-service", + "enumflags2_derive", + "serde", ] [[package]] -name = "icu_collections" -version = "2.2.0" +name = "enumflags2_derive" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "icu_locale_core" -version = "2.2.0" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "icu_normalizer" -version = "2.2.0" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "icu_normalizer_data" -version = "2.2.0" +name = "error-code" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] -name = "icu_properties" -version = "2.2.0" +name = "etagere" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", + "euclid", + "svg_fmt", ] [[package]] -name = "icu_properties_data" -version = "2.2.0" +name = "euclid" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "num-traits", ] [[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "idna" -version = "1.1.0" +name = "event-listener" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] -name = "idna_adapter" -version = "1.2.2" +name = "event-listener-strategy" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "icu_normalizer", - "icu_properties", + "event-listener", + "pin-project-lite", ] [[package]] -name = "indexmap" -version = "2.14.0" +name = "fastrand" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] -name = "is_executable" -version = "1.0.5" +name = "filetime" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ - "windows-sys 0.60.2", + "cfg-if", + "libc", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "flate2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] [[package]] -name = "libc" -version = "0.2.186" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "foldhash" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "font-types" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] [[package]] -name = "litemap" -version = "0.8.2" +name = "font-types" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] [[package]] -name = "lock_api" -version = "0.4.14" +name = "fontconfig-parser" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" dependencies = [ - "scopeguard", + "roxmltree", ] [[package]] -name = "log" -version = "0.4.29" +name = "fontdb" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] [[package]] -name = "matchit" -version = "0.8.4" +name = "foreign-types" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] [[package]] -name = "memchr" -version = "2.8.0" +name = "foreign-types-macros" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "mime" -version = "0.3.17" +name = "foreign-types-shared" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" [[package]] -name = "mime_guess" -version = "2.0.5" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "mime", - "unicase", + "percent-encoding", ] [[package]] -name = "miniz_oxide" -version = "0.8.9" +name = "futures" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ - "adler2", - "simd-adler32", + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] -name = "mio" -version = "1.2.1" +name = "futures-channel" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", + "futures-core", + "futures-sink", ] [[package]] -name = "nix-nar" -version = "0.4.0" +name = "futures-core" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a60e6f4acddbfaa0c363f8bdc6e4b2188d0b690c567176212f795fe008a30b3" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ - "camino", - "is_executable", - "symlink", - "thiserror", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "futures-io" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] -name = "parking_lot" -version = "0.12.5" +name = "futures-lite" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ - "lock_api", - "parking_lot_core", + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "futures-macro" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "futures-sink" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "futures-task" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] -name = "potential_utf" -version = "0.1.5" +name = "futures-util" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "zerovec", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "proc-macro2", - "syn", + "typenum", + "version_check", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "gethostname" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "unicode-ident", + "rustix 1.1.4", + "windows-link", ] [[package]] -name = "quote" -version = "1.0.45" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "proc-macro2", + "cfg-if", + "libc", + "wasi", ] [[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "redox_syscall" -version = "0.5.18" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "bitflags", + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", ] [[package]] -name = "ring" -version = "0.17.14" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ - "cc", "cfg-if", - "getrandom 0.2.17", "libc", - "untrusted", - "windows-sys 0.52.0", + "r-efi 6.0.0", + "wasip2", + "wasip3", ] [[package]] -name = "rustix" -version = "0.38.44" +name = "gl_generator" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "khronos_api", + "log", + "xml-rs", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "glam" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "rustls" -version = "0.23.40" +name = "gloo-utils" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.11.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", ] [[package]] -name = "rustls-pki-types" -version = "1.14.1" +name = "gpu-descriptor" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "zeroize", + "bitflags 2.11.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", ] [[package]] -name = "rustls-webpki" -version = "0.103.13" +name = "gpu-descriptor-types" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", + "bitflags 2.11.1", ] [[package]] -name = "ryu" -version = "1.0.23" +name = "guillotiere" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "half" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] [[package]] -name = "semver" -version = "1.0.28" +name = "harfrust" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "core_maths", + "read-fonts 0.35.0", + "smallvec", +] [[package]] -name = "serde" -version = "1.0.228" +name = "hashbrown" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "serde_core", - "serde_derive", + "foldhash 0.1.5", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "serde_derive", + "foldhash 0.2.0", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ - "proc-macro2", - "quote", - "syn", + "bytes", + "itoa", ] [[package]] -name = "serde_json" -version = "1.0.150" +name = "http-body" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", "itoa", - "memchr", - "serde", - "serde_core", - "zmij", + "pin-project-lite", + "smallvec", + "tokio", ] [[package]] -name = "serde_path_to_error" +name = "hyper-util" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "itoa", - "serde", - "serde_core", + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "iced" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "000e01026c93ba643f8357a3db3ada0e6555265a377f6f9291c472f6dd701fb3" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", + "iced_core", + "iced_debug", + "iced_futures", + "iced_renderer", + "iced_runtime", + "iced_widget", + "iced_winit", + "thiserror 2.0.18", ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "iced_core" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "91ab1937d699403e7e69252ae743a902bcee9f4ab2052cc4c9a46fcf34729d85" dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", + "bitflags 2.11.1", + "bytes", + "glam", + "lilt", + "log", + "num-traits", + "rustc-hash 2.1.2", + "smol_str", + "thiserror 2.0.18", + "web-time", ] [[package]] -name = "sha2" -version = "0.10.9" +name = "iced_debug" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +checksum = "25035ab0215a620e53f4103e36fc4e59a1fb2817e4bfc38a30ad27b4202ea0be" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "iced_core", + "iced_futures", + "log", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "iced_futures" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "8c0c85ccad42dfbec7293c36c018af0ea0dbcc52d137a4a9a0b0f6822a3fdf0a" +dependencies = [ + "futures", + "iced_core", + "log", + "rustc-hash 2.1.2", + "wasm-bindgen-futures", + "wasmtimer", +] [[package]] -name = "signal-hook" -version = "0.3.18" +name = "iced_graphics" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +checksum = "234ca1c2cec4155055f68fa5fad1b5242c496ac8238d80a259bca382fb44a102" dependencies = [ - "libc", - "signal-hook-registry", + "bitflags 2.11.1", + "bytemuck", + "cosmic-text", + "half", + "iced_core", + "iced_futures", + "log", + "raw-window-handle", + "rustc-hash 2.1.2", + "thiserror 2.0.18", + "unicode-segmentation", +] + +[[package]] +name = "iced_program" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dfafec2947cda688d8eb00dac337ba11aa60f9ef6335aed343e189d26e4a673" +dependencies = [ + "iced_graphics", + "iced_runtime", +] + +[[package]] +name = "iced_renderer" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250cc0802408e8c077986ec56c7d07c65f423ee658a4b9fd795a1f2aae5dac05" +dependencies = [ + "iced_graphics", + "iced_tiny_skia", + "iced_wgpu", + "log", + "thiserror 2.0.18", +] + +[[package]] +name = "iced_runtime" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1889b819ce4c06674183242e336c8d49465665441396914dc07cc86f44fa8d4" +dependencies = [ + "bytes", + "iced_core", + "iced_futures", + "raw-window-handle", + "thiserror 2.0.18", +] + +[[package]] +name = "iced_tiny_skia" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0acf8b75a3bc914aff5f2329fdffc1b36eeaea29dda0e4bd232f1c62e9cc3d" +dependencies = [ + "bytemuck", + "cosmic-text", + "iced_debug", + "iced_graphics", + "kurbo", + "log", + "rustc-hash 2.1.2", + "softbuffer", + "tiny-skia", +] + +[[package]] +name = "iced_wgpu" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff144a999b0ca0f8a10257934500060240825c42e950ec0ebee9c8ae30561c13" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "cryoglyph", + "futures", + "glam", + "guillotiere", + "iced_debug", + "iced_graphics", + "log", + "rustc-hash 2.1.2", + "thiserror 2.0.18", + "wgpu", +] + +[[package]] +name = "iced_widget" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1596afa0d3109c2618e8bc12bae6c11d3064df8f95c42dfce570397dbe957ab" +dependencies = [ + "iced_renderer", + "log", + "num-traits", + "rustc-hash 2.1.2", + "thiserror 2.0.18", + "unicode-segmentation", +] + +[[package]] +name = "iced_winit" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b7dbedc47562d1de3b9707d939f678b88c382004b7ab5a18f7a7dd723162d75" +dependencies = [ + "iced_debug", + "iced_program", + "log", + "mundy", + "rustc-hash 2.1.2", + "thiserror 2.0.18", + "tracing", + "wasm-bindgen-futures", + "web-sys", + "window_clipboard", + "winit", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_executable" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +dependencies = [ + "arrayvec", + "smallvec", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags 2.11.1", + "libc", + "plain", + "redox_syscall 0.8.1", +] + +[[package]] +name = "lilt" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67562e5eff6b20553fa9be1c503356768420994e28f67e3eafe6f41910e57ad" +dependencies = [ + "web-time", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +dependencies = [ + "bitflags 2.11.1", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mundy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f32eb0db40f2df2bcfb05c93b8f73938d4c26ce9ac8881f1df0c8d3296921a73" +dependencies = [ + "android-build", + "async-io", + "cfg-if", + "dispatch", + "futures-channel", + "futures-lite", + "jni", + "ndk-context", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "pin-project-lite", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.62.2", + "zbus", +] + +[[package]] +name = "naga" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.18", + "unicode-ident", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nix-nar" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a60e6f4acddbfaa0c363f8bdc6e4b2188d0b690c567176212f795fe008a30b3" +dependencies = [ + "camino", + "is_executable", + "symlink", + "thiserror 2.0.18", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.10.1", +] + +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types 0.11.3", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "skrifa" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +dependencies = [ + "bytemuck", + "read-fonts 0.35.0", +] + +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts 0.39.2", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.11.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.11.1", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.4", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "swash" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0811b01ca2c4e8718760713911feaf4675c24f94e50530a015ec646cfb622f7c" +dependencies = [ + "skrifa 0.42.1", + "yazi", + "zeno", +] + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" +dependencies = [ + "as-raw-xcb-connection", + "ctor", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "signal-hook-mio" -version = "0.2.5" +name = "wasm-bindgen-macro-support" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ - "libc", - "mio", - "signal-hook", + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] -name = "signal-hook-registry" -version = "1.4.8" +name = "wasm-bindgen-shared" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ - "errno", - "libc", + "unicode-ident", ] [[package]] -name = "simd-adler32" -version = "0.3.9" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] [[package]] -name = "slab" -version = "0.4.12" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] [[package]] -name = "smallvec" -version = "1.15.1" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] [[package]] -name = "socket2" -version = "0.6.4" +name = "wasmtimer" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" dependencies = [ - "libc", - "windows-sys 0.61.2", + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", ] [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "wayland-backend" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] [[package]] -name = "subtle" -version = "2.6.1" +name = "wayland-client" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] [[package]] -name = "symlink" -version = "0.1.0" +name = "wayland-csd-frame" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.11.1", + "cursor-icon", + "wayland-backend", +] [[package]] -name = "syn" -version = "2.0.117" +name = "wayland-cursor" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "rustix 1.1.4", + "wayland-client", + "xcursor", ] [[package]] -name = "sync_wrapper" -version = "1.0.2" +name = "wayland-protocols" +version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] [[package]] -name = "synstructure" -version = "0.13.2" +name = "wayland-protocols-experimental" +version = "20250721.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" dependencies = [ - "proc-macro2", - "quote", - "syn", + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", ] [[package]] -name = "tar" -version = "0.4.46" +name = "wayland-protocols-misc" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" dependencies = [ - "filetime", - "libc", - "xattr", + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", ] [[package]] -name = "tempfile" -version = "3.27.0" +name = "wayland-protocols-plasma" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix 1.1.4", - "windows-sys 0.61.2", + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", ] [[package]] -name = "thiserror" -version = "2.0.18" +name = "wayland-protocols-wlr" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "thiserror-impl", + "bitflags 2.11.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", ] [[package]] -name = "thiserror-impl" -version = "2.0.18" +name = "wayland-scanner" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", + "quick-xml", "quote", - "syn", ] [[package]] -name = "tinystr" -version = "0.8.3" +name = "wayland-sys" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ - "displaydoc", - "zerovec", + "dlib", + "log", + "once_cell", + "pkg-config", ] [[package]] -name = "tokio" -version = "1.52.3" +name = "web-sys" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "tokio-macros" -version = "2.7.0" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "tokio-util" -version = "0.7.18" +name = "webpki-roots" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", + "webpki-roots 1.0.7", ] [[package]] -name = "tower" -version = "0.5.3" +name = "webpki-roots" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", + "rustls-pki-types", ] [[package]] -name = "tower-http" -version = "0.6.11" +name = "wgpu" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", + "arrayvec", + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", ] [[package]] -name = "tower-layer" -version = "0.3.3" +name = "wgpu-core" +version = "27.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.11.1", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-types", +] [[package]] -name = "tower-service" -version = "0.3.3" +name = "wgpu-core-deps-apple" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +dependencies = [ + "wgpu-hal", +] [[package]] -name = "tracing" -version = "0.1.44" +name = "wgpu-core-deps-emscripten" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" dependencies = [ - "log", - "pin-project-lite", - "tracing-core", + "wgpu-hal", ] [[package]] -name = "tracing-core" -version = "0.1.36" +name = "wgpu-core-deps-wasm" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "9b1027dcf3b027a877e44819df7ceb0e2e98578830f8cd34cd6c3c7c2a7a50b7" dependencies = [ - "once_cell", + "wgpu-hal", ] [[package]] -name = "typenum" -version = "1.20.0" +name = "wgpu-core-deps-windows-linux-android" +version = "27.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +dependencies = [ + "wgpu-hal", +] [[package]] -name = "unicase" -version = "2.9.0" +name = "wgpu-hal" +version = "27.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.11.1", + "block", + "bytemuck", + "cfg-if", + "cfg_aliases", + "core-graphics-types 0.2.0", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys", + "objc", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.18", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", + "windows-core 0.58.0", +] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "wgpu-types" +version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags 2.11.1", + "bytemuck", + "js-sys", + "log", + "thiserror 2.0.18", + "web-sys", +] [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] [[package]] -name = "unsafe-libyaml" -version = "0.2.11" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] [[package]] -name = "untrusted" -version = "0.9.0" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "ureq" -version = "2.12.1" +name = "window_clipboard" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +checksum = "d5654226305eaf2dde8853fb482861d28e5dcecbbd40cb88e8393d94bb80d733" dependencies = [ - "base64", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots 0.26.11", + "clipboard-win", + "clipboard_macos", + "clipboard_wayland", + "clipboard_x11", + "raw-window-handle", + "thiserror 2.0.18", ] [[package]] -name = "url" -version = "2.5.8" +name = "windows" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", + "windows-core 0.58.0", + "windows-targets 0.52.6", ] [[package]] -name = "utf8_iter" -version = "1.0.4" +name = "windows" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core 0.62.2", + "windows-future", + "windows-numerics", +] [[package]] -name = "version_check" -version = "0.9.5" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "windows-core" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] [[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "wit-bindgen 0.57.1", + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +name = "windows-future" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "wit-bindgen 0.51.0", + "windows-core 0.62.2", + "windows-link", + "windows-threading", ] [[package]] -name = "wasm-encoder" -version = "0.244.0" +name = "windows-implement" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ - "leb128fmt", - "wasmparser", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "wasmparser" -version = "0.244.0" +name = "windows-interface" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "webpki-roots" -version = "0.26.11" +name = "windows-interface" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "webpki-roots 1.0.7", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "webpki-roots" -version = "1.0.7" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "rustls-pki-types", + "windows-core 0.62.2", + "windows-link", ] [[package]] -name = "winapi" -version = "0.3.9" +name = "windows-result" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "windows-targets 0.52.6", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] [[package]] -name = "windows-link" -version = "0.2.1" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" @@ -1509,6 +4709,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1605,6 +4814,67 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.11.1", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -1669,7 +4939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -1705,6 +4975,38 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "xattr" version = "1.6.1" @@ -1715,6 +5017,43 @@ dependencies = [ "rustix 1.1.4", ] +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.11.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + [[package]] name = "yoke" version = "0.8.2" @@ -1738,6 +5077,93 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.8" @@ -1803,3 +5229,43 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 413c617..cddc7ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "crates/brain-brew-core", "crates/brain-brew-formats", "crates/brain-brew-cli", + "crates/brain-brew-workbench-ui", ] resolver = "3" diff --git a/crates/brain-brew-cli/Cargo.toml b/crates/brain-brew-cli/Cargo.toml index 4272d01..0b9af80 100644 --- a/crates/brain-brew-cli/Cargo.toml +++ b/crates/brain-brew-cli/Cargo.toml @@ -22,6 +22,7 @@ axum = "0.8" base64 = "0.22" crossterm = "0.28" flate2 = "1" +include_dir = "0.7" nix-nar = "0.4" brain-brew-core.workspace = true brain-brew-formats.workspace = true diff --git a/crates/brain-brew-cli/assets/workbench/brain_brew_workbench_ui-9575fc3c29e6b0e6.js b/crates/brain-brew-cli/assets/workbench/brain_brew_workbench_ui-9575fc3c29e6b0e6.js new file mode 100644 index 0000000..c143f7c --- /dev/null +++ b/crates/brain-brew-cli/assets/workbench/brain_brew_workbench_ui-9575fc3c29e6b0e6.js @@ -0,0 +1,1186 @@ +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg_Window_c7f91e3f80ae0a0e: function(arg0) { + const ret = arg0.Window; + return ret; + }, + __wbg___wbindgen_debug_string_8a447059637473e2: function(arg0, arg1) { + const ret = debugString(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_is_function_acc5528be2b923f2: function(arg0) { + const ret = typeof(arg0) === 'function'; + return ret; + }, + __wbg___wbindgen_is_undefined_721f8decd50c87a3: function(arg0) { + const ret = arg0 === undefined; + return ret; + }, + __wbg___wbindgen_string_get_71bb4348194e31f0: function(arg0, arg1) { + const obj = arg1; + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_throw_ea4887a5f8f9a9db: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg__wbg_cb_unref_33c39e13d73b25f6: function(arg0) { + arg0._wbg_cb_unref(); + }, + __wbg_abort_6e6ea7d259504afc: function(arg0) { + arg0.abort(); + }, + __wbg_activeElement_39e967783d67360b: function(arg0) { + const ret = arg0.activeElement; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_addEventListener_ea90bc131475777e: function() { return handleError(function (arg0, arg1, arg2, arg3) { + arg0.addEventListener(getStringFromWasm0(arg1, arg2), arg3); + }, arguments); }, + __wbg_addListener_ddaa585636995443: function() { return handleError(function (arg0, arg1) { + arg0.addListener(arg1); + }, arguments); }, + __wbg_altKey_7412dd64f8c921c7: function(arg0) { + const ret = arg0.altKey; + return ret; + }, + __wbg_altKey_bd27feb84dabed76: function(arg0) { + const ret = arg0.altKey; + return ret; + }, + __wbg_animate_8f41e2f47c7d04ab: function(arg0, arg1, arg2) { + const ret = arg0.animate(arg1, arg2); + return ret; + }, + __wbg_appendChild_acb7691406591783: function() { return handleError(function (arg0, arg1) { + const ret = arg0.appendChild(arg1); + return ret; + }, arguments); }, + __wbg_blockSize_ec8ab846ac2b2395: function(arg0) { + const ret = arg0.blockSize; + return ret; + }, + __wbg_body_c94f9310613c153b: function(arg0) { + const ret = arg0.body; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_brand_3bc196a43eceb8af: function(arg0, arg1) { + const ret = arg1.brand; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_brands_b7dcf262485c3e7c: function(arg0) { + const ret = arg0.brands; + return ret; + }, + __wbg_button_bb3285fcdf965178: function(arg0) { + const ret = arg0.button; + return ret; + }, + __wbg_buttons_c15069bb056ecbe0: function(arg0) { + const ret = arg0.buttons; + return ret; + }, + __wbg_cancelAnimationFrame_32c0ef91e4077228: function() { return handleError(function (arg0, arg1) { + arg0.cancelAnimationFrame(arg1); + }, arguments); }, + __wbg_cancelIdleCallback_d2ed70e82f4d3a29: function(arg0, arg1) { + arg0.cancelIdleCallback(arg1 >>> 0); + }, + __wbg_cancel_65f38182e2eeac5c: function(arg0) { + arg0.cancel(); + }, + __wbg_catch_2bb4b6f2fafb00e7: function(arg0, arg1) { + const ret = arg0.catch(arg1); + return ret; + }, + __wbg_clearTimeout_ef73c3ecbb0ece69: function(arg0, arg1) { + arg0.clearTimeout(arg1); + }, + __wbg_close_38d22e2ee9732188: function(arg0) { + arg0.close(); + }, + __wbg_code_87bb99fdec02d313: function(arg0, arg1) { + const ret = arg1.code; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_contains_aeb13d2d343b24e8: function(arg0, arg1) { + const ret = arg0.contains(arg1); + return ret; + }, + __wbg_contentRect_0fb8c8900d689a0c: function(arg0) { + const ret = arg0.contentRect; + return ret; + }, + __wbg_createElement_9e23ac95e40e302c: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.createElement(getStringFromWasm0(arg1, arg2)); + return ret; + }, arguments); }, + __wbg_createObjectURL_d8fd80787743974d: function() { return handleError(function (arg0, arg1) { + const ret = URL.createObjectURL(arg1); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, arguments); }, + __wbg_ctrlKey_00097886a21597a7: function(arg0) { + const ret = arg0.ctrlKey; + return ret; + }, + __wbg_ctrlKey_e2bcba26214642c1: function(arg0) { + const ret = arg0.ctrlKey; + return ret; + }, + __wbg_deltaMode_af2c1b6d0e654eb8: function(arg0) { + const ret = arg0.deltaMode; + return ret; + }, + __wbg_deltaX_002e415d6311b335: function(arg0) { + const ret = arg0.deltaX; + return ret; + }, + __wbg_deltaY_5c6a1e0334a84e24: function(arg0) { + const ret = arg0.deltaY; + return ret; + }, + __wbg_devicePixelContentBoxSize_442c861e416326ba: function(arg0) { + const ret = arg0.devicePixelContentBoxSize; + return ret; + }, + __wbg_devicePixelRatio_8e5ca44720abafec: function(arg0) { + const ret = arg0.devicePixelRatio; + return ret; + }, + __wbg_disconnect_970650440e4877a1: function(arg0) { + arg0.disconnect(); + }, + __wbg_disconnect_fff596d884a74ee7: function(arg0) { + arg0.disconnect(); + }, + __wbg_document_2634180a4c694068: function(arg0) { + const ret = arg0.document; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_error_6fe2f1675730f2bd: function(arg0, arg1) { + console.error(arg0, arg1); + }, + __wbg_exitFullscreen_5fbe5f08298dfd87: function(arg0) { + arg0.exitFullscreen(); + }, + __wbg_fetch_8d9b732df7467c44: function(arg0) { + const ret = fetch(arg0); + return ret; + }, + __wbg_focus_c80921c19b28520b: function() { return handleError(function (arg0) { + arg0.focus(); + }, arguments); }, + __wbg_fullscreenElement_0961e3f245820d9a: function(arg0) { + const ret = arg0.fullscreenElement; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_getBoundingClientRect_bf4a017da5494fc9: function(arg0) { + const ret = arg0.getBoundingClientRect(); + return ret; + }, + __wbg_getCoalescedEvents_3e003f63d9ebbc05: function(arg0) { + const ret = arg0.getCoalescedEvents; + return ret; + }, + __wbg_getCoalescedEvents_b5b03d5636ebc2f4: function(arg0) { + const ret = arg0.getCoalescedEvents(); + return ret; + }, + __wbg_getComputedStyle_3fed4438a3d089a6: function() { return handleError(function (arg0, arg1) { + const ret = arg0.getComputedStyle(arg1); + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments); }, + __wbg_getContext_486aab500e1c34c9: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.getContext(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments); }, + __wbg_getContext_70c2d1bed75d4122: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.getContext(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments); }, + __wbg_getOwnPropertyDescriptor_7f06ccc7c66b4ef1: function(arg0, arg1) { + const ret = Object.getOwnPropertyDescriptor(arg0, arg1); + return ret; + }, + __wbg_getPropertyValue_25192bdc8db9053c: function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = arg1.getPropertyValue(getStringFromWasm0(arg2, arg3)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, arguments); }, + __wbg_get_197a3fe98f169e38: function(arg0, arg1) { + const ret = arg0[arg1 >>> 0]; + return ret; + }, + __wbg_get_unchecked_54a4374c38e08460: function(arg0, arg1) { + const ret = arg0[arg1 >>> 0]; + return ret; + }, + __wbg_height_e9afb506f31052a9: function(arg0) { + const ret = arg0.height; + return ret; + }, + __wbg_id_b08632fa6ed13f45: function(arg0, arg1) { + const ret = arg1.id; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_inlineSize_8895590ca1875f8d: function(arg0) { + const ret = arg0.inlineSize; + return ret; + }, + __wbg_instanceof_CanvasRenderingContext2d_d0cab9e931424c52: function(arg0) { + let result; + try { + result = arg0 instanceof CanvasRenderingContext2D; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Error_77d0cf0b4f31a32f: function(arg0) { + let result; + try { + result = arg0 instanceof Error; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_OffscreenCanvasRenderingContext2d_9c9a48d2ce5a9707: function(arg0) { + let result; + try { + result = arg0 instanceof OffscreenCanvasRenderingContext2D; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Response_79948c98d1d2ba75: function(arg0) { + let result; + try { + result = arg0 instanceof Response; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Window_0d356b88a2f77c42: function(arg0) { + let result; + try { + result = arg0 instanceof Window; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_isIntersecting_e02cd273824aee08: function(arg0) { + const ret = arg0.isIntersecting; + return ret; + }, + __wbg_is_de5b366c746e004c: function(arg0, arg1) { + const ret = Object.is(arg0, arg1); + return ret; + }, + __wbg_key_d6b3d2cce3d41872: function(arg0, arg1) { + const ret = arg1.key; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_length_c6054974c0a6cdb9: function(arg0) { + const ret = arg0.length; + return ret; + }, + __wbg_location_8fedbe6d61285cd1: function(arg0) { + const ret = arg0.location; + return ret; + }, + __wbg_matchMedia_049ef5c8f8f904f2: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.matchMedia(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments); }, + __wbg_matches_a4c6202857b1dac9: function(arg0) { + const ret = arg0.matches; + return ret; + }, + __wbg_media_405c765b5e24963e: function(arg0, arg1) { + const ret = arg1.media; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_message_6fc0a1f59fcc247b: function(arg0) { + const ret = arg0.message; + return ret; + }, + __wbg_metaKey_1c2847b62ad062c1: function(arg0) { + const ret = arg0.metaKey; + return ret; + }, + __wbg_metaKey_e661374fe349fa2f: function(arg0) { + const ret = arg0.metaKey; + return ret; + }, + __wbg_movementX_128a28f825e5e554: function(arg0) { + const ret = arg0.movementX; + return ret; + }, + __wbg_movementY_10cde9bca43c7656: function(arg0) { + const ret = arg0.movementY; + return ret; + }, + __wbg_name_3e4efa9f5ec3f127: function(arg0) { + const ret = arg0.name; + return ret; + }, + __wbg_navigator_935098efd1dc7fe5: function(arg0) { + const ret = arg0.navigator; + return ret; + }, + __wbg_new_10e2f2ad134f940f: function() { return handleError(function () { + const ret = new Headers(); + return ret; + }, arguments); }, + __wbg_new_1e52e3c919312525: function() { return handleError(function (arg0) { + const ret = new ResizeObserver(arg0); + return ret; + }, arguments); }, + __wbg_new_26880d0ec07a54ec: function() { return handleError(function (arg0, arg1) { + const ret = new URL(getStringFromWasm0(arg0, arg1)); + return ret; + }, arguments); }, + __wbg_new_2e117a478906f062: function() { + const ret = new Object(); + return ret; + }, + __wbg_new_51233fa2a760b272: function() { return handleError(function () { + const ret = new AbortController(); + return ret; + }, arguments); }, + __wbg_new_98e81f23cc202cc4: function() { return handleError(function () { + const ret = new MessageChannel(); + return ret; + }, arguments); }, + __wbg_new_cbb1396a0ca9d6f4: function() { return handleError(function (arg0, arg1) { + const ret = new Worker(getStringFromWasm0(arg0, arg1)); + return ret; + }, arguments); }, + __wbg_new_df50d6b5ba0c6ef5: function() { return handleError(function () { + const ret = new URLSearchParams(); + return ret; + }, arguments); }, + __wbg_new_e469d9dc75b3bc01: function() { return handleError(function (arg0) { + const ret = new IntersectionObserver(arg0); + return ret; + }, arguments); }, + __wbg_new_with_str_42b7fb3e03fb91e5: function() { return handleError(function (arg0, arg1) { + const ret = new Request(getStringFromWasm0(arg0, arg1)); + return ret; + }, arguments); }, + __wbg_new_with_str_and_init_5b299538bdeeec64: function() { return handleError(function (arg0, arg1, arg2) { + const ret = new Request(getStringFromWasm0(arg0, arg1), arg2); + return ret; + }, arguments); }, + __wbg_new_with_str_sequence_and_options_b67a6653749e1791: function() { return handleError(function (arg0, arg1) { + const ret = new Blob(arg0, arg1); + return ret; + }, arguments); }, + __wbg_new_with_u8_clamped_array_483dbce5ac9306c3: function() { return handleError(function (arg0, arg1, arg2) { + const ret = new ImageData(getClampedArrayU8FromWasm0(arg0, arg1), arg2 >>> 0); + return ret; + }, arguments); }, + __wbg_now_e7c6795a7f81e10f: function(arg0) { + const ret = arg0.now(); + return ret; + }, + __wbg_observe_592b94857eed8e27: function(arg0, arg1) { + arg0.observe(arg1); + }, + __wbg_observe_91d5c6a617c3d390: function(arg0, arg1, arg2) { + arg0.observe(arg1, arg2); + }, + __wbg_observe_a987521b8fae2aaf: function(arg0, arg1) { + arg0.observe(arg1); + }, + __wbg_of_3ed679d45555e384: function(arg0, arg1) { + const ret = Array.of(arg0, arg1); + return ret; + }, + __wbg_of_62183ea089c00bfa: function(arg0) { + const ret = Array.of(arg0); + return ret; + }, + __wbg_offsetX_a9bf2ea7f0575ac9: function(arg0) { + const ret = arg0.offsetX; + return ret; + }, + __wbg_offsetY_10e5433a1bbd4c01: function(arg0) { + const ret = arg0.offsetY; + return ret; + }, + __wbg_performance_3fcf6e32a7e1ed0a: function(arg0) { + const ret = arg0.performance; + return ret; + }, + __wbg_persisted_cce9b6da86c07260: function(arg0) { + const ret = arg0.persisted; + return ret; + }, + __wbg_play_3997a1be51d27925: function(arg0) { + arg0.play(); + }, + __wbg_pointerId_2c4027fd33473f83: function(arg0) { + const ret = arg0.pointerId; + return ret; + }, + __wbg_pointerType_2dfba7dcd3e033b4: function(arg0, arg1) { + const ret = arg1.pointerType; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_port1_aaca04ed552c16ef: function(arg0) { + const ret = arg0.port1; + return ret; + }, + __wbg_port2_baea902d7925a544: function(arg0) { + const ret = arg0.port2; + return ret; + }, + __wbg_postMessage_5b1dc9f8de88488a: function() { return handleError(function (arg0, arg1) { + arg0.postMessage(arg1); + }, arguments); }, + __wbg_postMessage_d05b4de2ebb80b85: function() { return handleError(function (arg0, arg1, arg2) { + arg0.postMessage(arg1, arg2); + }, arguments); }, + __wbg_postTask_e2439afddcdfbb55: function(arg0, arg1, arg2) { + const ret = arg0.postTask(arg1, arg2); + return ret; + }, + __wbg_pressure_727ca025d24d77ec: function(arg0) { + const ret = arg0.pressure; + return ret; + }, + __wbg_preventDefault_c047d3e292b08cc4: function(arg0) { + arg0.preventDefault(); + }, + __wbg_prototype_0d5bb2023db3bcfc: function() { + const ret = ResizeObserverEntry.prototype; + return ret; + }, + __wbg_putImageData_253970d244a23731: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { + arg0.putImageData(arg1, arg2, arg3, arg4, arg5, arg6, arg7); + }, arguments); }, + __wbg_putImageData_8205094a7b8e8699: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { + arg0.putImageData(arg1, arg2, arg3, arg4, arg5, arg6, arg7); + }, arguments); }, + __wbg_querySelector_1f3658f4b48e268b: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.querySelector(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments); }, + __wbg_querySelector_45b96634e7f85460: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.querySelector(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, arguments); }, + __wbg_queueMicrotask_1c9b3800e321a967: function(arg0) { + const ret = arg0.queueMicrotask; + return ret; + }, + __wbg_queueMicrotask_311744e534a929a3: function(arg0) { + queueMicrotask(arg0); + }, + __wbg_queueMicrotask_3d05fddf59c892b7: function(arg0, arg1) { + arg0.queueMicrotask(arg1); + }, + __wbg_removeEventListener_c6782a9c30557d31: function() { return handleError(function (arg0, arg1, arg2, arg3) { + arg0.removeEventListener(getStringFromWasm0(arg1, arg2), arg3); + }, arguments); }, + __wbg_removeListener_9b7f8155dd998421: function() { return handleError(function (arg0, arg1) { + arg0.removeListener(arg1); + }, arguments); }, + __wbg_removeProperty_2015da6fd4077af0: function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = arg1.removeProperty(getStringFromWasm0(arg2, arg3)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, arguments); }, + __wbg_repeat_46a5c90df9f6ef0b: function(arg0) { + const ret = arg0.repeat; + return ret; + }, + __wbg_replaceWith_c96b99889c4b5b79: function() { return handleError(function (arg0, arg1) { + arg0.replaceWith(arg1); + }, arguments); }, + __wbg_requestAnimationFrame_b92ccfbc8ca777a8: function() { return handleError(function (arg0, arg1) { + const ret = arg0.requestAnimationFrame(arg1); + return ret; + }, arguments); }, + __wbg_requestFullscreen_3f16e43f398ce624: function(arg0) { + const ret = arg0.requestFullscreen(); + return ret; + }, + __wbg_requestFullscreen_b977a3a0697e883c: function(arg0) { + const ret = arg0.requestFullscreen; + return ret; + }, + __wbg_requestIdleCallback_25d3b54bee33cdc0: function() { return handleError(function (arg0, arg1) { + const ret = arg0.requestIdleCallback(arg1); + return ret; + }, arguments); }, + __wbg_requestIdleCallback_3689e3e38f6cfc02: function(arg0) { + const ret = arg0.requestIdleCallback; + return ret; + }, + __wbg_resolve_d82363d90af6928a: function(arg0) { + const ret = Promise.resolve(arg0); + return ret; + }, + __wbg_revokeObjectURL_f8e75e321c6e13b8: function() { return handleError(function (arg0, arg1) { + URL.revokeObjectURL(getStringFromWasm0(arg0, arg1)); + }, arguments); }, + __wbg_scheduler_a17d41c9c822fc26: function(arg0) { + const ret = arg0.scheduler; + return ret; + }, + __wbg_scheduler_b35fe73ba70e89cc: function(arg0) { + const ret = arg0.scheduler; + return ret; + }, + __wbg_search_15207b35b594af69: function(arg0, arg1) { + const ret = arg1.search; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_setAttribute_8bccfbabf2a83682: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + arg0.setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments); }, + __wbg_setPointerCapture_87f6b55ceca474a9: function() { return handleError(function (arg0, arg1) { + arg0.setPointerCapture(arg1); + }, arguments); }, + __wbg_setProperty_da5e4438a912e787: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) { + arg0.setProperty(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4)); + }, arguments); }, + __wbg_setTimeout_6e540f43d21684d9: function() { return handleError(function (arg0, arg1) { + const ret = arg0.setTimeout(arg1); + return ret; + }, arguments); }, + __wbg_setTimeout_8afa0b5ed243c77d: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.setTimeout(arg1, arg2); + return ret; + }, arguments); }, + __wbg_set_4564f7dc44fcb0c9: function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.set(arg0, arg1, arg2); + return ret; + }, arguments); }, + __wbg_set_box_3fa4e9e6b8b96df4: function(arg0, arg1) { + arg0.box = __wbindgen_enum_ResizeObserverBoxOptions[arg1]; + }, + __wbg_set_headers_6751c09a8e579ff7: function(arg0, arg1) { + arg0.headers = arg1; + }, + __wbg_set_height_ad5056ea051acd78: function(arg0, arg1) { + arg0.height = arg1 >>> 0; + }, + __wbg_set_height_ef298446b359b0c5: function(arg0, arg1) { + arg0.height = arg1 >>> 0; + }, + __wbg_set_method_1120482abe0934aa: function(arg0, arg1, arg2) { + arg0.method = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_onmessage_8700520361fa9f48: function(arg0, arg1) { + arg0.onmessage = arg1; + }, + __wbg_set_search_048cc0b716fa132a: function(arg0, arg1, arg2) { + arg0.search = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_type_3194793b7f2a05be: function(arg0, arg1, arg2) { + arg0.type = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_width_031bdecd763c5855: function(arg0, arg1) { + arg0.width = arg1 >>> 0; + }, + __wbg_set_width_f9e631f4ee129e5c: function(arg0, arg1) { + arg0.width = arg1 >>> 0; + }, + __wbg_shiftKey_273ee8c901267b55: function(arg0) { + const ret = arg0.shiftKey; + return ret; + }, + __wbg_shiftKey_e57eae650446e681: function(arg0) { + const ret = arg0.shiftKey; + return ret; + }, + __wbg_signal_4d9d567be73ea52c: function(arg0) { + const ret = arg0.signal; + return ret; + }, + __wbg_start_8ca2b69cfb7618b6: function(arg0) { + arg0.start(); + }, + __wbg_static_accessor_GLOBAL_THIS_2fee5048bcca5938: function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_GLOBAL_ce44e66a4935da8c: function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_SELF_44f6e0cb5e67cdad: function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_WINDOW_168f178805d978fe: function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_style_f00fbf31e0a68f0e: function(arg0) { + const ret = arg0.style; + return ret; + }, + __wbg_text_68ea00f7126f2706: function() { return handleError(function (arg0) { + const ret = arg0.text(); + return ret; + }, arguments); }, + __wbg_then_05edfc8a4fea5106: function(arg0, arg1, arg2) { + const ret = arg0.then(arg1, arg2); + return ret; + }, + __wbg_then_591b6b3a75ee817a: function(arg0, arg1) { + const ret = arg0.then(arg1); + return ret; + }, + __wbg_toString_73456969ad00f2fa: function(arg0) { + const ret = arg0.toString(); + return ret; + }, + __wbg_toString_b9fe6ec9b4809cee: function(arg0) { + const ret = arg0.toString(); + return ret; + }, + __wbg_unobserve_e9b5d98f37ccf1ff: function(arg0, arg1) { + arg0.unobserve(arg1); + }, + __wbg_url_c6a2bdc371c879a8: function(arg0, arg1) { + const ret = arg1.url; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_userAgentData_31b8f893e8977e94: function(arg0) { + const ret = arg0.userAgentData; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_userAgent_3ac81db86b32899a: function() { return handleError(function (arg0, arg1) { + const ret = arg1.userAgent; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, arguments); }, + __wbg_visibilityState_39ac2e458f892eec: function(arg0) { + const ret = arg0.visibilityState; + return (__wbindgen_enum_VisibilityState.indexOf(ret) + 1 || 3) - 1; + }, + __wbg_webkitExitFullscreen_f487871f11a8185e: function(arg0) { + arg0.webkitExitFullscreen(); + }, + __wbg_webkitFullscreenElement_4055d847f8ff064e: function(arg0) { + const ret = arg0.webkitFullscreenElement; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_webkitRequestFullscreen_c4ec4df7be373ffd: function(arg0) { + arg0.webkitRequestFullscreen(); + }, + __wbg_width_08e66520b99071f4: function(arg0) { + const ret = arg0.width; + return ret; + }, + __wbg_x_aff60dcb3e218497: function(arg0) { + const ret = arg0.x; + return ret; + }, + __wbg_y_fe6dd6897e29b956: function(arg0) { + const ret = arg0.y; + return ret; + }, + __wbindgen_cast_0000000000000001: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1006, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2f3200cc7dffa6d9); + return ret; + }, + __wbindgen_cast_0000000000000002: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 588, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h158575abd8390d16); + return ret; + }, + __wbindgen_cast_0000000000000003: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array"), NamedExternref("ResizeObserver")], shim_idx: 594, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hea6cbb90ec4dd390); + return ret; + }, + __wbindgen_cast_0000000000000004: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array")], shim_idx: 588, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_3); + return ret; + }, + __wbindgen_cast_0000000000000005: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 588, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_4); + return ret; + }, + __wbindgen_cast_0000000000000006: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("FocusEvent")], shim_idx: 588, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_5); + return ret; + }, + __wbindgen_cast_0000000000000007: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 588, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_6); + return ret; + }, + __wbindgen_cast_0000000000000008: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PageTransitionEvent")], shim_idx: 588, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_7); + return ret; + }, + __wbindgen_cast_0000000000000009: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 588, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_8); + return ret; + }, + __wbindgen_cast_000000000000000a: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("WheelEvent")], shim_idx: 588, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_9); + return ret; + }, + __wbindgen_cast_000000000000000b: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 590, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h41c00db73ea7c4ba); + return ret; + }, + __wbindgen_cast_000000000000000c: function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return ret; + }, + __wbindgen_cast_000000000000000d: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./brain_brew_workbench_ui_bg.js": import0, + }; +} + +function wasm_bindgen__convert__closures_____invoke__h41c00db73ea7c4ba(arg0, arg1) { + wasm.wasm_bindgen__convert__closures_____invoke__h41c00db73ea7c4ba(arg0, arg1); +} + +function wasm_bindgen__convert__closures_____invoke__h158575abd8390d16(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__h158575abd8390d16(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_3(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_3(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_4(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_4(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_5(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_5(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_6(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_6(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_7(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_7(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_8(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_8(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_9(arg0, arg1, arg2) { + wasm.wasm_bindgen__convert__closures_____invoke__h158575abd8390d16_9(arg0, arg1, arg2); +} + +function wasm_bindgen__convert__closures_____invoke__h2f3200cc7dffa6d9(arg0, arg1, arg2) { + const ret = wasm.wasm_bindgen__convert__closures_____invoke__h2f3200cc7dffa6d9(arg0, arg1, arg2); + if (ret[1]) { + throw takeFromExternrefTable0(ret[0]); + } +} + +function wasm_bindgen__convert__closures_____invoke__hea6cbb90ec4dd390(arg0, arg1, arg2, arg3) { + wasm.wasm_bindgen__convert__closures_____invoke__hea6cbb90ec4dd390(arg0, arg1, arg2, arg3); +} + + +const __wbindgen_enum_ResizeObserverBoxOptions = ["border-box", "content-box", "device-pixel-content-box"]; + + +const __wbindgen_enum_VisibilityState = ["hidden", "visible"]; + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_externrefs.set(idx, obj); + return idx; +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => wasm.__wbindgen_destroy_closure(state.a, state.b)); + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function getClampedArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ClampedArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +function getStringFromWasm0(ptr, len) { + return decodeText(ptr >>> 0, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +let cachedUint8ClampedArrayMemory0 = null; +function getUint8ClampedArrayMemory0() { + if (cachedUint8ClampedArrayMemory0 === null || cachedUint8ClampedArrayMemory0.byteLength === 0) { + cachedUint8ClampedArrayMemory0 = new Uint8ClampedArray(wasm.memory.buffer); + } + return cachedUint8ClampedArrayMemory0; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function makeMutClosure(arg0, arg1, f) { + const state = { a: arg0, b: arg1, cnt: 1 }; + const real = (...args) => { + + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + state.a = a; + real._wbg_cb_unref(); + } + }; + real._wbg_cb_unref = () => { + if (--state.cnt === 0) { + wasm.__wbindgen_destroy_closure(state.a, state.b); + state.a = 0; + CLOSURE_DTORS.unregister(state); + } + }; + CLOSURE_DTORS.register(real, state, state); + return real; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_externrefs.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasmInstance, wasm; +function __wbg_finalize_init(instance, module) { + wasmInstance = instance; + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedUint8ArrayMemory0 = null; + cachedUint8ClampedArrayMemory0 = null; + wasm.__wbindgen_start(); + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('brain_brew_workbench_ui_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/crates/brain-brew-cli/assets/workbench/brain_brew_workbench_ui-9575fc3c29e6b0e6_bg.wasm b/crates/brain-brew-cli/assets/workbench/brain_brew_workbench_ui-9575fc3c29e6b0e6_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..619b36b88f5a390fa33a12663e87d0794b21c33b GIT binary patch literal 3405849 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Ν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

I7xUT0A^Dob!C;WhevBe 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

2~Om)?pI%O2b^(2*r@tDP5HJl4 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%0A1w?2>r;*Dat#GXgKh|SO9gIelKi+qX zIJuJivCx-4;MR9C7=`>{;W`*(8fXkCCjPs-UsJjtu9eYrG>fUuDv^NC^Ea$wz|Zi5Ww zkSRseuQb@dmjf$>nim#Jg5OXYUEn8rLOIYqsxXrreZ&@M4HBz{)GwaWO*wOa+=d#kw7n8ty1{4mM-U9<{Q#p+gVDz^$+ZTLk)CGSLT z7Y)_&vo6OhgiX3_%F?4LDo+I>iIS3I=|Vm`F{ySe-Be1VbXDXVeoJ%IWy10$nhEnp z7}bI%_b0YAsLzs&MtwlK6B=VzX@Z}9qx!R@A=BHoIsgJ^FBcLIVN|nciYHLEL_izm zoRHnpMADO%Ng4eilY=1#2=3f;sIk0nW%UTkc(V@Ns zwxuM22{JAxfkDbm2l6yrVShcjk_5KJB`{MM{>_)bOzrSJlfX8KgORGJkiZtMB!Rh( z#3V3d4FA8EzzAs6m%!EuZ%zUJK!!{P2BK2$5NOOet8Ik@)-EJ4)c(6%^6Ji4oSUxgP=$gWZHr<6yG^pVJ`^2<^nd2RWkG!H) z4ZlSAfN5W=;cEyVUB!^FFcSR_=mE1x4@$y^H}5+je3;UpBz!=f%IY7p*!|5XK7Z5H zKYOl3e7^VUACmxc^^b8B5FZ7lhxqvVDB{zJ>Ys%*h!0Mci;&!o6QE5Hps3(!V(a_| zau$;!106=!e2NvuWazW#KZfqbWjAZc9gt|>3;#Nbn!KV_4Znm|8n;RZS2cVMTG<(G+y9Zc<~{iCDQZli)~A)YJ4(Zf+aqoi z`1IkJOGJ8t*r48#eOXF8XzAKVuS`nS%(gE4I}#wE%{SVc@OYuW*V8itPtOlLy<9v= zC-cm&<$8Z{LC+UYD&^(kN$)NePkQ&(z|)CRneFUM{v^zs$`+Sk4si5Yw1Z^jy1O3i zU%|u%{}H+}40N;oOBlJ02gnbS?IPaiX_y1=f&;OGWO0SmL9$ZBU^_U*4FmEJ7e7e0 z>j%kN>V4SLFQAXNgJen3L9*T4cT$9bNF<7E z+=B19(m{r7lewcfNLCNu(Su|a4uuna;{m>7s}C~d%(OKQGK2)XOA7RN?So_?DWYlt z8iOc@ClY;K#CMd-eq2R4EUb&0P+KlgOp)6aO9#o$ip7zpKb{whb%3J=;1y>jP)P+J zU-S=>9UC}Eb~DnVj*s0MHX}6U2gxGJ@2eIE$?g=#Vh71`hw@Ej4jd#q!#wLC**}FK zHju~sAX%FN95_k`$?~*fC&&6hvOJ!!M?Xjwez`bE7LKmb>Uf*kzJO9#nLqspUI_k?L}bn9AZ=;_r5$+A<)O~lv+q)ou_ z2pfeMdQ?U6AXy5rgJgArEa7#xap54@T>$07u+4Um?1CvvaW-b!a5-J;AlV&mAKhjL z$(HhPkgT3o9VEK|McH7-xZ-|<4-$kaOq=*muPlkpX`j`yBNjL-4wBuWqhPnqh8;&S zC3VBL!GmP^Ms7!F)p8F?R$12k?8mfiU27~~Z@@jtzur(fNcLUiVz&v3OJEx(r{W-4 zb~NZ9*{>$~$A0&&o zp@U>e*WnPHotqydOAV!iWG&$NIXSJF!LeH$B-@GFc%}RnntKJ&>9^YuY7PSO3p8_!pNf+sG7a>;8g7%V}qB- z5=$B2UL2gted*v-Y^FLm)f`y=FUAwCe_{}?^@P7ItSPVJGum;MQf%ewf94jS_@=3j zoE@jKCozw{%!UMcrgyYEBQb|BW^HE@uCMLaC(kIFB-e9EZ;$+6f4>Ty>`3{H`!owz z_xFj0&N70w)%Ig`WO{|nbrd^wHr35odYhB(&-oF_45q4 zL|!J(piAUHCU=QkqrC$zkpKzSmyWh$c3;Bje<69b%EWc#kPS$(OS%B#co?OQ9nB$I zXat?n*`-Ai9=*o&Sm-A?MRvlhz5KLFe+{?jus(NrWID8;oM~jAI-MKp zf~me*1;vG@9PcN>sdDh_lw+iBPxo)}<@8jVXC$|yN0{$>o?`X<*i-27?|usZ=#M_d z=9-T`)!P@lxJ%c!t3W*0H>-F`bA7$a0im6J)#C(~r2(*z=55JA4BU+a)rJ`EH0L-3 zhlF9lSn{X;d;I6^jA&kA;Sna8QqH_BiGN#|JI&h?LinH@TchbwzA-g7ZE8~}SfTpH z!h#l2EQVnL9F{3^>_a5rh`od^P8*x_eN}?An6+ruWJa)|=DwBo0HokCja8Fh)-Wq- z{N9uUgxg_na0ozrmY+=~kEd`%^9UcG=iu)vTK-k1_jIvzfErY=cKGL8w-QE53 zZf|n|5Hk!(U!=9l@MTw>%VFj-;50GTM<^8|5SglWLRfeUodqNv)<$ICm!5uYcjFd3nmlT{aN= z=#^VWyW)ug9|?`VD<=?z8xz0g(gwx&N)@+S(eE-uW;M5&%5)li3hb!4xR`AAu%Q^G z)bNg6o?)*s9(uusWx@gv9Mm#wYUw2CG58*iftIll+}ocJlM0n-Nu1~4u-8Ds^U^`f10tQz3R%U z{d8)`w3Ru51_oXQjj=AS00zG#!$|*XK9n>Kt6o;@rKXIBTvLmG;Oj|H_8ETI%j!$wg*9E;1*|drRwlGg~R>kfe%72cAwBPiqunYGcMxt4gW4 z>oh8bTMgASacAFQ#brE#45s^2U8+5Jy(s$U583A=wlg& zM!^~wSeqv%cV9Jd#}0^3V(y74!l=bY9*`8s9FNHpa~=Kwcw&-bevcF>TOf}Z-^M*- zkGOF7Z-|##mpy4m30NKZIiEQ8SxJ4uEmYNtS@OnYemsbom;R!=k<;(Gv@#tjT<|4iKPkS@s=-t)zpXn!?PXo1borf_ zKqT}|u5USSX%-c|lZ)5SktQYP)5;g!HqYr^?wszniQ8oH3a82PE8H&aYuqmJuQSgj z`hue-3TGj}({gnqUi;&^(qN{=I>6Fz&HYubg?r{1dugl;bPW_oiG3h>0-q!kqj5{< z^T@CO>$FiOCuYJ0KnNdYWlX4B7##2Yzq4w_Eah; z;wUU^*8NhtIV`|(FqR0}*?$$|Dd2elO+o2j4u8;y;Sc`DKdE$ouLE`OQmYq3DSYxE z1;H$B=f!5XJPwr(FnX3C0t`LAMsmn7kMzQl5Q~1IDEdmd&X;l#{Qoj#z5UPgX0BP) z3q>JU`)>5B!*-_@rrZYxaxYA7=TaDY^E@Yuso>^ckYGBd~hDJd0|41*uf|U%e^qk>?B#ZkGNv8kCS91 z7L$Amp~d7Le?o4#gj_?q{(noZUlz@UTwl-?a!r79BiAfZMy`LCYeBAGw3j)#eqK+I zYxst>$n~~&L9RCoJT4*EFQ{&4Bfn*G4cl-jxrP&=2q)KeyzvW)vSzf~ibjkwa_weO zg^X`QrbG#IPYk&&I^pifG`XExSt=|>!wx6gGX>c;FAcH4kYw{*Zl}B0-eiIiy256< zAQ`9cg_rI&ei@-lY_mQBQO)b1ORoR=b|__CoO_HkGeBx#N#F<&e4>DfkY{9-iWex* zygAAiN~>a2yT4qHq&Ddn%A1g75PjH+$Lm7sK?-_&RmASOBnlm8{nB^%KQwXh`n#!tIno=+(ovwP^iSk+N+T4QDz& zcNrRS3+?$zIgFPG#b&1?ujhn9L>5DoEd?g3m<&vz6^cY@ ziC+k$?^sWMjc&soFQVxgYxp1xytX^TLZdeaYm{ebrA=H>7vy0`2YB+%@1m1)B*DK}Xi@a^YVo8}-YA~*Zl(B6?;ag^I$b;| z=Y{;q43U4D8zRAaBJPdn{F=;#uO}%1K`UWuS5p6h3jZmwGMoOOJ^ZML&)q}dUyXu{ z$uPOtEXpB(H98O_)9cDT0IZeW)dB!pBJWW^pqFk@WjXU_QV#sTI6Z?T=QSs%H+ zf)B5Pby0~@@tr}|M4n}`-blg^cK^KA{l)Iv71uOgVbq2PM{VeD#i+e-`B9UY@z)-; zQ5;$v!!3@l-0T&V&Q{k3dv+~S1CNMNS_+BRfU75YWYCX+=iELX#xRA2HV6Iz0PL2y z73~tvN4KI~LR2@S^B!gu6PwD!v}{Di&;t(_u1{(oO2h!2tzorea<&LK*zu(amMe_5 z(;J%@qa(mMw*YYTWfI^fIk-TP|9Z1(g#K6<*Wi#XV42g$hBv%l0jQ;2rU`kIbCkgd0Hz#0zeyn#_4C$?9RN$ZQ`OW}oLoS+8Ba9OP| zg2cOk6;(u+3zhCg6eRRlW;Z~DQgnoiby;!wlDr@-JFx=WJ8Z>;wqbL*RM|jcsnMrI z!wKkFR4BoaJW}6KfI2gcRPpwgbL8gO2eB0fFdD+Vk3?&{nqN4WoyfsLv^IZt&7G zE8pXUOcPC28Uj>Xr`np~lrhm*P67Z7XZ8!tquEb3wc8k>N((RNhM0BMaS6F$$?uq0 zo$2=~j7wW*8v4|?vQxqsn|i*?o-CeXkk&Y$D29sxRg=z3k*Ar>DaHaJe&}l5X$BU) zf_MnR`65Ppff;zRCVk%u+!$^ELBiWrVreWaYZerZlxVRMR#E~LaoZ*%F+tr0)_9ne zs;vi@G;FK{kE$XkPm0bN$E{_|AqO1X3&`T`qzIOjOr3@e45nBcCq&$~xz#)4VZMnY4?mlm(){XCM78!>kbDI^@Nwx1`Z?>hjeJN75H+cDVigZH z?~YdTw!su*kWiVePzHeRmIJ$~0AYC6>q8jAN91n{+lyDjtjHwY7`=7Oyl`wC(B$2lvgPaY z?PtHTnh7BFhC~Ivf-n6G08a_8!-)O z=>UkJe$T)RhaeNIpapinl09@ZY~*-Ro<+Syil9IZ3Haq<(4d+QJdEprW(+WLztM2Y zB93PYGeT)`MCc`|IMzY=kWus~Z*>T;?m(H3C@o)RP?!UPg|+>(kV50+qvU1w>%rV7 zE-Eh=kt8oJuR?wjTCYz1Mb0qgZD$m-CyP`a{&B8&qC?r6e_njS_s5HO7Kg^pELq0O ziTPGM&m2T-TfU%EWbB+FIBm>c%gZt^doeFvi^t)VygpH@jAp}`kxnB(&{`s$3&!OV zqnQ^E3-Gh|v6{`{9|PqASsrFOC7!_lBGovO*vh!9gA5qRJAnto6yONbT*R!_^wF!S zWq|=eQUUgbxYgy+o|NNbp)3C~qZXr#FpC@~m4O~kpE$LZ85_IPoKy)l#Fwv&vHMxM z^;&&a3eKP~>gcX%xh9d74p^|zXpm_b2wEY`P$MRf^@C}vVG-!s5Q$ngEUxM3Y?!nN zfLZMr71nRVm6kAsgC&C!<&s^DD>yHT73ZZUGO)~(4t3BH%U9zZR3nZD5k+l04w;@|pFbN8Di@`=X5cPmSG178URVUdO#Yjuv(L%LoL5CWn zqwTi+4==#<&F?GN4EbR0PlZ<>%N|{&nDNDs`Uc_tFB`@I7 za>1_-re3VNKB+Qa2`cs#xh}Qs_GR5Rsa1;V?D;SLvF;F=beFyP-xjTSU9I>p(3PA= z2y?9bLrgE%w-A#*B&^iK@u9RXg%Y=KHkbvjD?=<{bJ80@`h)+d_e0_D1uj1vYLoy=riCo| zfKHsLq1@~Jh+vdnsd!>3gDuKtRFF13Hw;a{t zILCtq={c6Lx$HDc+Ym4i+B+Ttfo@u&Lkz&p@XCLeRC{)Z`gOs87 zNuI1h_Vi;s*(q!G^!LNuzUnK@-Z~ZYbd$bO#u;N>$7Xvx?Mluz!zTg{pU|t9n@}~b zFEv@BxIWoL<-zrI6V-|h=eP~6x|ekSgszb&t=fx<##{^SL)8xlgzZDsJGe}0^IlDx zSa0U?LkF3cTKM6E$&Fk-c`!N1<;M;t*K+y$2b29=e)M3n$OVtD1ul%#JePSLBvA_^ z3h7)6fA1hso@?O~2MPR4Or-+}6BE`8@@ip&w2ZazBL`^`u1Ff_#8z>o(~IYuX-)I8 zki0-XRF!rJ9msA;T7 zgrr{4(r5nQ>rn#pUS-es6Da9o{**!9$$t6|$u4#wKA+KhJ$rez?B(Kvx46ruL4j|M zEM>=E+4xnHWM)qvP?I)i zAV8|2(uxTNl29y~&umDNhp+yLP}#-$#Xcggm0nYqPeSjwtWH3-uLgQNR4eRnLczQ0!b^IaMi`oamwFzIs<+8y*L)O8^Y_Df^;7-o$x*Ie4N>@hud$ogs2ge= zJ(>1SrBh+ZA2*#$x2kY>>M$B>91TONv5T3SzoN#WFqL@O_fkhr zrn^tU($@TO_GG%KIysR%jKoStLQR!S1y1w}vqdHBQOPS|VJs>ciu0)Q`|~KCY{sh? zJDKh|#WLOS$Bie`y=nkE!oygs5u>PT9Aa||e?^UrP|It?H@T>>9&1$j{Te~SFzr>0 zpG^0iVo4eH#|48wVi&9JVhu@P%j`TZJ2 z7Y%VwSJPotuv4fR(x%E?7n`DbsCoF5y0?Yyl`AW{SFSAYp2n+RSy0*YPTa6=8C6?w zYIY`)u|+3eXCvL(@AT^FNT{xL0)=2~lzn5P@p7)80JpePgJ>U_+|R*c#E;+V}3bmPX=_??EX5P4e|I+f0cwpZ5{ zDnm}XREJ|X_}r%;Xu$I7hC;ijZa&AZgRAkm7bXSFTSHxijd~5NEYyISkQyeT82lAL zFo3^(iqXw$xHi_1;~2h(>ovQ(LQNIPA}{(OyybU2HNZfF(R~UrfV_PQ0rEOc+7D#y z4l{zI#n4dMBVOo;P!sYnL!m=Z1pbO<58!8?BCL&{i`1|uObcrF0-RJd<~5876(I-H z)Sx|p{1r6};A5WxU0xS_;2uT<>@@UM^Y1Nhg+__gtG zKF7ZcW&At*-NC=ZLW-Lm|DKEZcf|4U`G|jaIR3p5@$W9jzZWC^-R=1IQpCS|9RFS} z;omD&t?xS>|2`r7V^^w=S&o0(8MArAzxfjW?aUFPjDI^K{%xm(J{kE$gk3`&m;sPa zOasUl)0& zub~rb$k%4C;qF)i&H&V4$T#Z9huPc5tBic82tdO!@{RUuXvZ3IT=p97iZwK}vN|TV z91};^!o(3Uk-wr3i0=IwMq>?w>*F191T`2Ya%z(>abzt_90wEmD{3Hof4_#2Si|7@ce79e&bp&(WT|1LjnR%g zG`zEM){$v`BeK$wONKi(-Z`?xu*1eI8y~DPKhGW?E^k+TKsKK|_1MAW^I$#B+@937 zq=!gCSoEY(OL~ALJTpA0Wl8suG!iy?(ugJ9OVU`_;7Mba^l6gDbF>?`q)(Bwj+K6k zs{drDpm<6Ef3D|~G?8M7+h>x1X9Drtkw6kYz*P+8&Wdi6i3+k!kbHLP@!xPU`d@5G z_B@tE? zP8rg~^sYmw5b#yGG$ZbQ%3XY@eg`E7(qfeWor;db3h?`s6Yv{pp1-8<^h7{`7A4FQhdv9X=UN z97Ves!J#g-hk!xC`)F?qEq$%F)EoBZ(4ZRewPAK`JN6#l{xs&9y=XSWQg2t7%}=5T zTjc-1A!-6^?h`O{GXczqfWD`)9aT(nAshZ&n%o}}3o3&16Lrq?f=u;%mOcNI7o(yG zkx0s0WTWimB5?-Lnx(|K%a$ljTaEH``m9YR`X_1qW>EH{PgECG003~ACRqVe6i7)p z$4TKqCJhP@%vn=`J4(tf7OmPMTNROvdu*Tn%W7|3w(`?oh~XdfobZNlfTXRr_Z)NB zo3}d@@}$bKEah)2e>?e`3ZYlC_uK6KcG5=dj$_OykC1?TwZZPSZT?~W3Opi=HlZhQ z1{>s9VMEZLKcAD>^K@!YWgcBa{(+qnKbIySWb|5T106+zHi4q>6J>J_G@9-Eda0ld ztsWXh3fK-yf`LqyM|=_VcVrg52|f>mjYn02 z8$hD6DuYT~pJC4V9R`T}C@nQV%(HP;E;b$4qeA1(ji3u%2T{qS2A#}~|Lh+`{osUD zQ*YpXA-yTLuh$TLCp6hvYUj4&sVD2_Fr&QJUr0-|i8LyON^c@>x&t49dE5m@8Cz-< z1+`)!p;o4lh8voZWa|`J7ELGC0Vz(cE&#p79vb!<9m4Wz4@>qi!$a**bzXW*PI#3X z{gz{j3$<=JEPU7kR)ZsZDmw^GenVx4N*y~?-OOd^P!&}i%gxa8p=y8{%rg&!X+)Hp zVBKr?dSMK8)JQK4&ED3quD1;%Es!I>gb4=e#82gjwTY+4^C$1%2k^Iehdv^M%2t`! zP$BOSR0D-H!;~iK_6Be&ah%#F3{tE&{+Mgph~Ug0z;P8qdv94}AlnkD06l6Crre?F z4)53?bkK3orOnn63)74n-RH8M5t!arO?$0(Q_}jPYud%HgQ!_+?rTtHi|abLu`Z2DrkqEFgrE0@1;WuyIy=( z%p+$0Vjj~1v>^a&m5rc{)OSP!2#MG#5>f7lU|hSbw|{UJDS90;1Aj+m(>~l|L@vQS zDkY5}vb5k{3MPYlIc!YO!;3`^MTy@kD6x8frg*2A9na)Xkcwm#sX()xzlFSr>KLvY zav8qE!9ZeIo0LQWj`Qq40IRWV%h1D-?dL{}42d3r?9}m0(Vi;L3g*VDUm1mZK;mvi zEZZs92qVp+Yji=&rP!@ZVX%~ty_L5tU##R%Hb)U$W}uu5)^^A;7sXAU33Iomi>z(L zZm<=0>i&T1zlf_=SXDlScLKvn39a4+tqoLd~#z;=R!xJr7f&*~8;9u3BDf;M_O4oP9=~Zrq8P;czRf z8Le-uX=qi=Lt@WMH4lmGlxpVWy;U_^RaX0p4KY9M>{tKbg$+zQ)YQBu#;B5wCal_Z zcph;Y+{hiQ-u;VpDITDZz)|>+HlSopRd#OB&#?OhR|JSdAd=r-caZwSjUX%wE=pTj zY8gE36B*8!UhNYBKN+s<&7aBH1U8S8t=>53akAx?nBJpk>GfZ_D-dT5njn z9T{ulFGLb#braJH?=&o?yH;}oQu^h6BIibgOA{+0H-M-TDjyQ+paiHItRQ_r?ZpNu z4bc(Et?a|NGo+;)wY^f9K7|?OPIRZK_RIm_bYd>8ewfK*29Jz2giYLikUMBIwPa_$ zm~$GUxCQ+s7f;1I4B>clRc;;regt z>F_bZK(*K)hKR^|bz7RureNA^g)K{F!Pu+E9Pmur*cHx9&Q67 zT>P}VKMja&2o1`9j51b!;M5uDEYpucv`Cl$V%f+b+c@7GP6?c}fS!L!&5FGsVZTyu zPFqaVG6EM@oStD5Fc@>aU9GK(CXgWMVZS2CaoR48ed#|&g1`s2;tP=g_7u>zdL;b~mO$uOR#P-xav7eaG%5@PpKc3w zS7+@BiS?FZ)Ju_Y@3+WkrAPB5o_}*#LNC%AS#UM#%F{^MdcX$Px_1j|wzA?rT~-fr zC)AX}K}Ho5Ov-O{{}j^I|BC5=TalX!k(%V@LLd#fx!_$>p}jsXO^pWOOmtuzm+Ylt zPs&F2E*pz85*(>GPGo}soeK`1|op&URx ziB-EH?&3y3l%0T|OKS%NR&e>;;}>IrZkCx<3Os_(%VJjTRhfMBLWF1du%SJ7;Rtt@ zeeS~=J`1XIL}ebpafsp$;5gI>un;=pY=hi^ICJYDU}jmgUxh49sSxWRkj~@UAl-o> znXu&h!*KW67ENxLN`cpUwW3$Q9!7X&L|;AyJ>ds5oT(KG`tfu_iu9^JWb;U;^UI;v z{n=U=+NIrRbGn|ELX+zg)!r6evFrz3ACbX;>j_hb3O?Zi4W-Qy(?iP{O0!+KjM4<^ zR6-x6&3V{S8n*H4A~XP?{5MZ}r!M)a0q zh9gQ_zlR;AZK!gT2Kq`Ut!zDHFRo%!RLf+6(oUESr72wNt5AAaJZHmDy8l&5^a|k= z=Pe6V!AGGq2q~1tW^UtRC_Nn5f?I<#YL!qLcBzEY>~Nx4MBG+U_JGH7AEjF*l+Nek z3e_V-IudJRABcU<%3mXjQ+bs@?x%NQB^W zIEsz5jHB|T7{E~wM&D}LWn>QP02SeAcTF4}H5?rYTy5Fq>Nq+aag+osYr;`Port3o z^kc) z&qt15snU)ARHBozg{>x5wQp8vSoL5R_P%o<@rf5 z=umChwSJE{j@nS=IEtJ=mVDV&wjLT4t{Bh_M;X(&>{_yh4YFl8N-pd}!qHK~(f(KR zBoU4d3ue`D01}9GH+8j2<6=09-;QvUO9@9u7}EZ-dzEb*^Fv}$t1-?1c#fltNeM?M zywO*xzJ#Mh(*0bDyRC4P;KHBdvp$Z}gAc}qb_8%l`xkN4?hHpQJ2=XK6*!7WCLDzr z2}eg9M_FNoqce`9Cbe0XX4nV{&S2iK1=g3iD=medZG&KyHi49%K-Ge=L{nWf0Svr1 zjbwrQPp7Q&q75ieN(&*#TSh|GPQ%!o*6`uyONBg?7oq~KoOEN(B@5-*#UPeprkr+P z7|8Mf{J8i87;T!;eQ_KLZ(}wS zwuZEzanoUA_i@8I(Z5UIIo&g^r~J9dqZlU*@lbXagC3OF+bW{l^v0HF@R4dd=TJi% zPgK)QnnP}3h>R-3AZ^g|X`{}@Krx}Tb6}29BbEp)a}3!QxmK5EfB>;~6apwQ5D@JM zhtT_79Cd6Q*WKZ>bpkyYGp?r_OvE zV(^#Wl^FbMTayD156O-DG@N`*e%w-=B08U{^%f;UKU(YUL6KhNP%?J?EF_QAdV6&} zQA1}f1$*s;r5~^LdP-lZ;kZ`MV;R&|c$qz2!xfuy14b=wMCu_Z3x;{}uVZ`=bzmY|^4k+{|O= zY!u_Hu2hx0~w=1TV!xkUg&;rb_3E+97+1D=PBrHLfW7vRApv|M3m3sJAWX zDPeuHw|PcMl)c3@aOHgD-utk6Oxa|v9g?iv(p7*o;ErX1TbA4c%6ZAod?{}-;| zy*uvPxTb{oULJ2Ws!6Smx}++1q6QZ{gcBVmGRjkjTij~8+)i+dWq3WeS8>Z2OmiE0 z3A*Us3_~!5b=?d=D8+3Ohw$@EWbEc8KC4Y$zt7&-p8f8hO{NyKjX7@Sv0&>DP$mhA zAJD7Pr(tUlP$1Qwpn;uW!`+{rAg-U9Ox!(Wii`t48mN3Qj-y0Gt+<1LP&Ah3ebrX) zTC2TxomJbL_r`HbH3K)-n@w;~#{`?sW!>A-15y{whPm#4R@f7}`{pRNbL)dnd)t{c zKWd4pG5f?Sv>>ZSiVb%c)1dZC~N+|)V&M5T~&4O zzt-C4vCr9ipR>;+Zy;-}yiP(+5+EcYJCL(N0tw-zMFmBS%D*2zIn+N1xm-W}P;%7N z<|60OQ`|+Kg}o` zy8?CEh9>=TYJfBt28)CFGW#2J5tnYKJxEmyL=$V@XfPA%+TSqm5qR<;Sc~T}UZV-H z+pER_HbKY)86`$#$%tf{Y)!}yzF5A!{*kKtDyaV(>$Eh(IF?>z>+|pu9+cFUltog( zPpX3KJg6Wks-B@#O1@QbgYDY=;|X7uZmyU^j~rbIM;KVoxX_7MM)=!0q6Hz8sHht8_i<@U zU(o4$Dw-@g{hRmzcBdPs4T>oqt-F;sa~CAjSY9wxm@|h`VCW-Xl+ORZQf_|d;)mTx zNZIOaUu-2zH9Yp2U> z17y*0Edrljy$$KD9owv$k2lgBl>>f;8u~zSq+t*|&d`US9m&7>;h=_6nPl-$ZurZD zm%X$(LXvxSDe0NR0$=?&517>}SI3&bc5M$$o7!4)66lKhM4IWV>b3@t@ z>%Y+ag2rtAtcSI+IZrs}jz%MAx40O4(SOT^kN@Z&f6#MFy!t+Q>s!5H*4_;-dT-hA zrZ?}|d)wsU3ICR89bX0|8OnEt^R_Scym(Ex?ziLB8+QLT@$-nk^_C42*~{*?#H%l_ zdC)K|T>YE(G1PI3MHuf$cHtwHlW2%D=^Lvz_`emdR$MxcX*LydfQ?N0sY5vtg~cKg zYRGwK1v?&9!iYemn)Endfyw^K)JKsD8mZ9cM)@H{pSkNbLOwu@yqfnS_v3LDN_^qW{w4$0E+f zm~uxalJXaqQ{|BHTtg3T%dnWku7P@2xYP+B3i>ITb?97DW4UlVBrlGLN(mYpRx-GAVk)r;yEY!t zeD(r2eDo)unV77t#^MGffj5IA+QgyKYAJg{q3msV)5OHY8&FT9`hE$vg^N2MrJKX!l;tXNZfMBejP~rv=O)UtHMJgHCE@MvrZ4 zmKouIH4AgcW6jJYO~UEoRydt(Fh8={G|?%k@T!;^wm^_Ky0+TxJ->;>+Sck&Ma4-ElTuL_W35i$-H zXPLs|J?2tYCTd7_HIefUre%*__i_wRaI!1nj1LPj zU1>u0Xi*5l`ZReE$7>u>sB@_cZ-+Tu=p}FJaBm2K+Wp}i19IVDX>bGL>WSJE^|O(S3t7s3@J zE+bwKORq4rW6Swd zJ%5^+KZW_zW6e*VXg(foK0ecYJkWeR)O>uf`FJ>g6midC@|+SWa3p=5lqK{D`bjt= z?H{2oxn)7ZbrK9Kb-c5srx19;p7P{uou$hw+;u=)v8eykd8lr>&o=cQw5#NghdKN| zF>y-N&tH~2X}8dE5XIFQ*hjjsKdS-p0#)X^#K+MdRVvG$y)E6ixRSRe1fKQH#bIxp9n0c&I}C82M%6oeW4G0NeQ_%$Ss%^_CcJ^16mAe~Ot zPCo3}nmnxAnm%lOtPJ4Ot8gD&i7?W{;IJo}hqjvd(Pl;tt>}e#=-?ayPR3QL<^Jp7 z7LD`K47GGm)Gu++!!6;AI6cvP5$8mMW4@gAOt)1a%k%2xwAe3Uk zkmi|$o)?Lak)4)2;T0i`)Fa?DI>9bE?l8Co;NX}X_aL~1N~3^#-twg#EDO;(fp6XJ zgr>?ddksTt=9t?ILxX1+k$41h52fTzcCO}xG3h+ah#6$Dv{}LN+^o!OQMhZKt&X@m zSSIGKlOfv~?I=1&vknPjk$q5bI;xZ)i;DLOZbp?7B>yLvY(1psSFe`Z)vYri2L$nH zjM~L4(A2K(@ET}SJl4^l6+~@&Mvz(9Q-Y{pM}f)X)J;97_EFxt^0$Y1Tadp!#M{E` zO|%?>7F5CRFl_h-R42GzzE==C?@2*S;O`Vf;_erG)#~6G#b*isDM3`Yqk^b*Pclzk z?!ViQm)#$|8m^xaF5ASM$~}p}|z|o0p^j~;Y8xi!HaIv%cXMtMd+lty&9#%Mh7%t(8EX< zKpnXbs3K{;q~Fa?;b|$1Qph=JOP5R53@%9Fa0(ZuaA^vAkZCme zC|P%*S>ZjTdLv7I_s3V!%JC~vV#qc}Zlk?LVHW9{TW$`of)NJCwVF;EtV{01uX~gB zxH`^?B`VwIcu882v`V-MaZ05yA(*Y?9ka!+VTL!Y@cyb>!dF^`(Y%oL`y0bS2z_mM zO^)sfU&GvVNS{=03@e1bE)Sa@zLvGizCze*^01|Bkj*TFyf$oRbsFC_Q&~t3+w$+C z@KwxWW);3{h7E_;up>985Y|ky5^gJ`xqwufv~UlbHFUUyRa3g>sOk^8)ytNML)7W; zuD@Ass|e9;DD7bv&8b9Z6IV5%T1NXig{I{8>&B{Tm;S~GLvN55~-6+n#j<&k=ZkD#bE7~3pa3W zVnwwK`pCj%P+_y8CKvX2KOxfmarQLjf*6ckn_gFc>q-`S1aPArAtfXqgLwh zY@$hGd6Fk$nywq6|NbyW&aD5P`_k82!j-OTx_+kw=yx2~CYEF>+Ocf=7ZNe0eGF8C2n(5zH6oXaIHIw_i&-j`jF_S@I7EA-vfsE z9#H3dK$Y(S9efWk4`?Qc50bNF_W?nK_q~Eh?@2+#_nm^sZ=1$76)Q(@N=-^zN@+@W zT8gwx*UB?SC0wSte}IZVfny;CrgT87uu9xAl!dRph1e3#nYfY2vwGE?VF>4n4RExG z20$@?FisM1XDX6yVqJ#~m`2a+vDS%JV2U+u-x_}B@LS8T!EYVEbNQXeZ#};;ehI$~{LbgMk>4hM7x24~A8T@Qa1i$Nl>%SB zAIemo6&57rTNzj25FS`@mWskc_syGapF%@ea^S81l6os3w1X;t-^~|MF^rO8EO4Ao zoH#s3Cop+YG?e&_JhdSZj0Hh9kEcOE}& z6N%{(ejE6m&u=5YP5dt4cOgGyludP{-6Rj@)j)aKq33uo+##QlZXuwY-0JHw5O!ZP z0?T_@gab|N1I@S(HL*vUaXG$}Wx;hdS-M{9wmEW{#~dcuJV z=P>5-DHHMOY8!gAz6;&w^V82eW%Skl(Oh#pIk!#z*{*$kkF>f>3Znb=C-|)h$g)4F zCeMpA-r4K1`i&8Rx*Mz9>I-l_&vNs`GtRZq6*R8WdH777r<^>;P}I*o8JgoVjlxxx z04JxNg$JE%ttU5TgZz`%!$IuExcp_9k1${8M?hAfpqf09b7#Ki3vcn;r5 zyG@l~(^BJ}izpe&i%KKGIS@`~5muEG$ewAAEI@jAEA7JYD_TVM)2*_UknTQZ{Hfi@ zRd2jhE=4h!=G+ygDb}(SwGd%-8!lq=G+vZ$K*+!(oC)QW1{#NBJAdtTf7_wxA>(L~ z!s;{=GAa!#N$CGvN@%-QLKq|9z=|!1w2>o0%K}eUjds^GjuX;a?Vy$_t|C3;DnlzQ zV56^T>T%^_@xrCL(iJYEGHGj*$Is+V-3Kesth1p3HgNK4ktQB$fst^~X+{MORzrm& z_(~kn7S;$(^ce0IQ3HNB($4)G-t*Dm7E&2$>kw@Uf$aCRkNTi* zjd~u=Y{BWz%{!z^_NYs+g~aM&JgmAP|Fq@#!DgPxHx2XYCPrC*fUK`|RD6=hZAt;p4_lSuy*^3>a8gcKfGls$6tpwR z#Cyn(F4p^lx}*j8fREwgpWo8e4LpxiTr~l@-MB4e2YY`R3a#1 zmnkxe*ja)oVrL3I>!St%pYc(N%E?zPk)O6E^6w;ag%Y_nh%Q&;y#YEBa8D4e5}XLo zoq%X{D@o+bmdNi0IUA_D-+{L_KGyb9^b$qBGeEHd?hjD1fcpXzF5qne%2zr0YfI#w z)RdrKtJaao7c7xKZ%yRSNMy4TITCPRDUlxz zP~m`w1JpU-LjkHCkR2%v7oWF8zSx?`Uy;a0CGuntQ8%~ z4a>=AEs?LcCi0IYGKLI!xg^i{fot^u$@x{}n-k~~4G zr1GRU+`87gl3*4RuK-Z8%K4EG01J^8pMeR6DOdbK}} z(%G9JOi6^^UQFc52BMTmm#syH0ncKBD|jXoT)`zwa0Sm`f(uNLjwC{FFD7zD1EENy z%hw{{fU8#_;DD=ipDplgrt*R-S+)iy2vicGw-*z+yy1OPkzTSE=?84E3oW>oy*0se zxYbZ_%_^iHi6Hezgx+3E;CU<@3!clCp5Qum^MDCbk3{J0 z#Y8qYyl*MeudPM;0ncBB^aF0dPpDwRO1a<|E9JlhsYfF8_F^I%8{WSw(iLlwe!vT9 z4Z#a&4Z%(3$G>E@8f%+ZW+Uj?n z+u`-J+P;so3>U2RK5r)xESkP0C4Eb}`j$?fZ9i)R=?k}Lyh#+^Im7FSSfv z<+me(7Q{|eGa|9S1d-PB?htvq)3UHKRHXv2kX?vAT>!FM#pZBHlyu)|Lwcut-;)m7u-BX_& zP6M9%^dHCmC*wZtO*mD5N8HWBr(Z0|0V4Yw6B93%F5|I}1;dW9?6dWupV4r1Jp_l| z`{Lk}rc2-OmQNeScR`mwc@KZBu=UF`w{=Y9sT~6^^U>wdq2(N%Qo)qd)a6HO4#4}|G!%r{OaR|eVP7y1Z${=IU?uC#(HQ6~RmY6RCII6ZYL_tatdu(l}ydb{?F5)Z^IEb~L<7oHZXk zuKqyR-C>2wIY+iRcf^4{J^JE{-@g!63TlJ#DfHIcGn%GSFPVYd{0^dhtGR`t=rN* zr&LdZ2<1~Y8jN5>MZ&d3Wk-Uvu%#&G7G@*F(N9~826x1U$%v8cg_}o;VrZCJ)DgiH^_WBuhZu~JxQt3qCdC3 zHRD<*u3wvDPczoz)M3lugQV4e&#%`&oen~!X3vN?a)2{*YAUf&?|7Vj_!yk+r=xpY z-TAKWeC^zL^gA_UrH%ZcqKd`@pbo(d=$)Z|PLn>?TxeY_u5{W0QOD^eFY?@wSNt1x z^##~9@wo<97QQs*c52M!gS;~xdg$6s)T&RH^}OfSzkbtBybyZL!OHU>&eemw;~q1u ztQ{;Zza=g8C%-MV80JmeM0yWJrIM96(;-GZs=LcPYjd&+JANa(oRNKktmr5wkyW?L z3SXctS(Wk{>OP6A{FBP+IHh8R4{Ihh#xYBkRsfx*Sd&fc&n;vPhhx+r30PyLHJCSi z*2QTox_jR4<_1rdLnniy>X9!{mSH!AEuF9lCE1EiYR3h-(57kHG)YvkVhltB`mT#_Lm{=WNNR; zPsgmimP)hE{fo9Hm7vC~b+lKL#v9HzvTcx*_;>KGN8GNQz1^9;r6ROT=6aOJm=odW z1Zx+HNXRi2b-J)5Dc2=wrBk#PgDqOgKm7M|Bvg9az*92^=OJ+jC%~ZUMBsJy(m0tWSvyZu$l{M&bD#%{8PUB%~idhRt^-v z{%gx2?P2A@m_;_69uKo3YAVLzMGc3Yq3K$rlObu<&M^DdkZKQ$+>uZGgjc&b7AR6l z+_V$JdO8*A`Z6NJ!O`x)uxdhvp1_&XsYO#Rhxz#vue|@_NvW>9{ESm0iY-y zk$-g@OTQ}Crw)Q!4=h%skk$hWHCQX3WX~^3k)5^9TtFBzBCCs4Wf$s5fEVj1m+FPe zX_M$LWHQ_&`m@;^WamQv$qZ>x9&LGhD0`ELYl?a1iPkObth>o}?T_`aLC(rqS7qNk z-_KWpnv(ecy{9lpg7Lngb+da)9p$<=(+|2$haz`~>s@+u*}oPS{jF#4T%L^F{7^-lKaw%$gdgl)K=s_8}hVPZN`{*maC zDH1=cGVGoorr6r_WHrc(74>(8G)XfnSp#ybkndA=*7-`wIUp}ev-{E){`T#k|G*s| z6?L7-T0*uc`g@f|ChZy-_TCIzt0n6|ZdTO$h17X$zCRb_g+l(hkZE@3fxJM-dqqRZ z#as_^laL?(Cy;5i9hO|(2v+s>#m|2H?O*-&dq46mDC%oO>sblA*=+@z2>Wf7Mj{#+cSP7RVZV@< zadq-n!mbA^F?Hk%AAXB2MoGTVg8ipI`(W3FwX>5i3VWVX_;H@DpFAq;xnNZxkA3zJ zKlu4)zV?alQ6c9fUsBLIeek9HgXwHNf^7(UpRidA{!NZPl%b*eF=5v#^2dd>Htb3M zR@ifd{hYAoYzwl#oTGpIwF3GpIr{HZcUkPmbM%u%^xx&^e=efGnxnr{ME}nm{evR< zi5&fW5&ic$`o$vpA9D0te^emwYdQKIMf8(7dS8aFjVAvn>>5?cdxbS|FpsTou&af= zS6DfosC6e_7j~7f9~IWr0H4EcV9yrzQ^M92x0-xI*pFg?(CB>)&T4PYHX5us_UFP`CJ2j{aGOuJt8P3%f#*_x_k9ti8+0zX-cr*mntQ z#puMJ57=eG-X*LK_9MN%E$pbU9}#v?*xAWxP--OkcY;r(xP55;uKM{7WrSL7V#ts2?rC?7J_CJKRlAV_PyRfGU z`?jaSS{aBT>?y+T7dB1dr^3z=_MSX0t^$SST53P}u&~BDy~)pnoh9t2@)SzRe+r8i zJch`e-mc_DVP^>Yp{#brP!m#a4gFvd{iYoKFHe!5)x?bC&B9Js8ZREr$`N2M0(P3P zpH&i8j%mprVTXmi=Nn+HYYivAE$oo62Zgno7)stM?4YoJA*^xVVDdY{4hZ|5tQ?{k zySfzpC1sb6MgK$Cx+1^hpTU~57$UzdM}Htg*XqgJh3!}5JB78{?8itOY@e`i)6kXj z)H{U5IMGkuBdMwEM8)ss=r3nf2#t|n8vB1{X!z>)gyp6#KY3DEOSvm~m$1yZ{N$U$ zrbFfJ!uAUL+gYjUM;Hxi7tW11pZllJd=bZ0e)9XmYVRm{TC~{++?BjrSe9XIbo%_4 zKYmY_MyEe0V*lpo@BfI=DMh1q_aQ~lw!+Qyo{9!TZi=X}5w}0$@1xYLVBL%UV z;L2Y*@m$%0FY6v@$ z$=Tz{wjFWDeD6dHGv)^^)g#RxKO1t!!Kd{*@FaO;O3;qg&DV%2y=5WBC# zg5V!W;Q`Gg@C|eRk9v;~>QjKGb5`1#%k5a&NkE zhW_ANxiH-3yN*aF2b4fO;EGFq4unJdqCs9+BgK04q12E_;HP$vs>0YgziZgy)ZaH8 zG5ifEnex8)q~bG(TKwus#%CxVhz6~AOYCsA@`{FaEp#YqcAvjbw5)$uJn({_c%cg8 zdE4gL!|wC1nUl=irMS~hDlYDV6j{al`3d8@Mv;e3Ch|@^lK33&V-r<_B-e|w{YRsB zaA-(Iy5{J4qQi00(N6cimv@=6-IhWQ_;(~YWZ-zj5l;?@NT4${cI!y~*d9G$N%D}= zFgpa!sbPt5Tl)S&2@lt4`ul;3`?%h5H1Mdy2M-JC+ryk#dMFKi04Tz@#Fe$)p01Fs z+yoa7_5J3E-BV9E3J#n3PB_WZJ*w`5!FXrv35CsmC&rN-yUZaJuMgz`hg$=V5TI3b z;y@E=>l2Oa>hvIwcIu|{%diGC8p6))NzKewalge*ACz>`jr7lTC5~2x-8*^qF5F26 ziTmbY8&|!AdSs()S90g79d?|mL!K{4xx4;Z`~%egV;TYkCzi>1bRCHamvdJVI){%> z5gGRY+mkv6FKAg+5Iq;y9wLCpxfj&ff)1TY)|>2; zeg<8XuEK&Ph&mMUo5_tcVqPw?pOF>ebQw0T9ei7F=ut9QDpV>-B>+Me?Kr3 z<4V+u<%Ji^yYS#)!JOsOzz481Bl-kxsi$&zO|0mO;JCb&b9t?m%kzN4tpP^}*u?&c zxLhHQ%VoF(-6dk%vW{5acj z^(@ELGaXmYa9lmzarHFE)kt}9^^oK0LC4htj;mqeIfV6BlG6Zvm1Gfs4if|DFbe^6 zmrvp$NxWHBQM|b8ZMyN_} z;e5FVQu6G^YVdZAluHR3$)tondJ1+TA(K$>SlVBD^1dR0`u}==>G?0~E9!8?uHw2~ zK8E~ag9}4P%iw~PEX!`9(8WxWP3>Y5T59ofDmQ8J;|wNrwBrmWlF7}!EFq&_SypfE zgSVt6NOEd0;dB5(vQym0iM+6q4<@LCPAb>h`=SYuV-6-=ZZPR`gGrYgOuF1)BKtT7 z6B&8Y?Yi7x(&Yvd+1W6dbV;JP5kwkUhlsUdg`F}8Hc?}JfYCV}BPkZvSa>{gUB-%} z5il(hMkHZ(K9s0PZcJznC5!>tP?B&vuyqgoKx5}G6Cnf&Tat9Cs5|)wG;5J%jw$f< zFUJ&EIqJ+W>I!F1E3)3Ply$+yM28!olF30`twGl-pr#wDoTFsOIZ?F_n7eRXs2XCU z&2b??hDOFOwwXDgB8ZQ}380RH->>(6^wPdLtGo~B1s#wV&J5`VFZHNc`xN<3-7AX$ zpJ6-@_Zp&}x=x~TBs;9Wd(B@N)qTj%#~hz83Yt|OaJV(#2mvXZKdKNn<{;iu1rBS7 z`5uc&9a5-h+GdDraK;ROZ368v{iO! zyuvTiRdo0sPZG!e2YnqkB^^7GfR?NzkU9q--ylrPIy}E$H+s-XwsWxCza*@MeR#{& zjLhGG+o=lqvd#i!^g1DT(L0JUZGhOpDcd||x7NQ<9ou8pT$+WLJ!-Em)=YQEQahrQ zY2lePNfjI5mU2qD)bl>_t~Frf_dmu1l0t{+dxZeIvP9hkAtNUz6{+7$qqdW(%RwG4 z@ITg)1M_WGtI4Fiiv-Iz?fgt3qv3Ga$T(&U^Omhfu#ATgsbtK$AQ@{pE}Y)XSnd#T zP0*S#9>&SI%xn(}*oprv?T~9x9?Lh`8&5ND+PSkp4QHW^nv?&GyDCIPN+SLiMPiSU z$mC!MbJOmqN3Gh+W1UC3EY@+dLmhxg`}qLIifxI(LzMIVuIQ3&I=j*;LJCL%t-PgSpgH%%|!d5neBo0riZ<=Fu_05tTJ6IQ$R}{ZA zX1TlW*`e+!F`akMn(Lk#IU<}3QxaX6;vCK$vy0Mmetrk-qEs50F2UIF=XFqG4k#ub z5w`3^Qdn0WI#vg5P4k2u6tD2=O{`f&9n=8r!@V3~XWmg&-e!N)5{s--r}k0Z%~b58 zogq#FvOdaEPnJn{dnRdL#*l^I^Krw-B~hg?tXZ}!yQW>3XCMzg1O{GvS-O=&kwg*D)o zIWTP6Q!ROXo6tMt2s#%89UKxrUQe}@1~|MrK%ZAi=6~N(>bIV%d$$THA@4JmnL-3T zwG`GaVNs}rcO4EPR2zF8nK7O$&s;c$^>)?Tr3_8XWtUf!fQr2 ztLZo6BP_&CtlpQ8Y!uKa!%HMXbKFfiX_;xHJS~->{cgN;V=$MSDfc?L>jGeMml$i! zU45cg(z=&kgZA-4_l}(>PYXv9T4|}1Pd%ljSL0kZ$`@!sMv0d46k60Bu!v4a3JwR~ zwDT9L84Oel)MzwO$LOVF&>kAi`gvq#ygMwC$?#T?_}yYENqlirr_y6V!z8}0@j~L0 ziM&hs2mlpBY!o;)3Yu)h#S2^2;$oxV|A=|G^@_RBW#l1Q4ox;nAYBawUPMV=4@JZ?CP&Fuq z5>+UUMXK-}+gdxnGSi{st8^CXan;+Z<d-~yikfJ5|0P74Ni+Y$Oh2!P)Co?|Z6 z3P}d=q+}kPcL^*EU5ga3Y7&$4s_! zJjS)gFwaUN#=k~A-*xKPntdhN)|Kr2&NsaJo@o%vRMs4EB>(cvH<&E(2W^#Y9Oss! zm!jNnOOun_7PPt)l6o&*{E9f-fh6Dkpe_!%7ona&!=xbd(*3{Od;BIknSl+~pX!pFvAJbwzs1~W{lcpQ*Pqv82?A|OpaOS3c~*A0BT z2EW{6VJ|)kqY9!#pT$~$Mbp##7)LITdDq*vLPbMA=WZnNtFVoluRAQ?7|*0iSyNRa zD~MnffWsA0Bflmrv&gRrp-W>ttH?_Vez}-ZQ|`?|k3lD3jUwT(P5w`+<95jq?>JRd zDOhLMZFVlUSb`_~WvHIz^FdBZ@E)OxmOJ`BHxHi1Jh(S`*t6QGvkyw?swl{Sa_cPA zm76)F{Af+r#21HV&g~>hMSWa;#%7t5GYjCdFvzzGn;yb$L_tu&Q;ml2qK}l^LCVwHD z`naOdO==n!aO+0l;)?vF)gUsiYXT3h12Gb!`fj3!k>MDk= zBa~}1W!aave_aG>noavxph+3}@IW(5af{26m6_vaiO351n07XQruhr*eaKlU1Ro3O!@O zEN%F=rM+0aS+0Ec=>7E6B@QA0ZcA8+g4;||r7mrHvA$syU{w%;@5RJtdyFgeQ|#qk zyx_rPCV2Ct0h97Lb9?>cJ*nXA9rdcof3hWJ*Umz8*_>Y1=#zcay4E^sjNqztsyQ6w zIII9RG%9nA1&BAKf0$pW=> zKgq)G9-iK)#q~Q_Aml(Zcqts!!|}9a>{1JwyF-Lb%c7Ky5v4zmqULcClI|Tz*RGwM zdr1GNUHT)Hfh!#@;7`wZ(uw8>_a_rRJ`-JqS$O%TxIdXb-Z!VzmHp>u4=D_2?tUGm z(|+Tq_pe{W#~i-TZS$^x3s^c*4UxxD%ysO<8PB7PTkujipqnSr-CSSfURziY3TURW z*iL_&wA+TzPj|XXSBgj0de^E_kb%}|%r}(Xb6`wq-7#i&BlFNsJgrfdIPTD!JOPXN z@E4N+MztEtvkZz1UyROmxf9B}#fZd}1ZVw+Sfznqs@i2aq!#urE?p@pQ*&H*)XQ!I zFpA)Kj6q=l7L-#iwBfkRu?-*{j>R0}E;k&*PGUYlx_R5=s*BX~2AFqsAwLd$CmV)E zIWo<7%rT)kaqTbR*FLO>!Pq|*7ulw;Y8rjjy7H*!!%!?xvPZN70p69oMw&tmZVCH# zTFfAeDYX}I1s_@b8?;Q0Tk50=LP?e^Vl5ezHu#*kBdm;iTo{;smy9Z;T8P#8y|4pC4`Xa z;?i|)v@z)eq3KNss8&?9u4xhksza;B8i4qXW9Q+S zT;f)0E(00yv{s2Ws0m7mw^g1r$sPj?UU(uVhCFO&u4Hct;yyhA?FTetl(M%%jFRp} zEu@jY6=DPoSIrou?5z-^B=?$yG}5<13|)fUj8V$o3NcFD@z9JBWN(ES0Y~wgF-qB6 zAx0^gUx*Q8Z-p2E@}rnW`c{ZhN|qL41le05Mu3u9Oe1|O#3&^z3o(N1tq?=ECKuC4 z-wH8GxSVaq2(q_Aj6lk8UhAdotq`MRN?{%)$X^R_bf}|{kzIgosTSgtIH2K}4_4GN ztX{>Lii5u`fquukbhO9NOxeqm#OSb&QWMKyQedM>Fvev98k($k&`+>Q&|p! z>o8JAs~Vrh;J70Rt7(o^oTZT?%1+up$~+ruu^KFDWWr2FVb zz&d#_wcMgVhB8{y#9PW@w5Ex-2V{wdl^f@9TLHC|wikZ;Z4B2C;*BTOH@Nvp;FdaQ z7p}}z3GqE*b*N%evsM}cdR;(oY~{}bR6f42(rV78046ZDSYr2G%JDb1w`%8xr9AW` zmaFm83~O+4_x15~W*iK1to)SV4eA3=11!j*C6dx5bBLmC$eJllk+!2qK+f}#9}tWK z4Yq33{fCzO%=;Ya{URMFtVViw(z`tm>y6vOtfT5X6`(Z?y3Hv!t=~uAt6mSuD2Ln8 z*qPn;&C%{Kw+6C2l3pc0lvXq@S~V5H@`H&y6I}bUKbbi$9%4WhHCPL&k_Sa8PFGV( zr#HiT+rw0(xjg3-O+mNcQ zKO9P{ZCNv{w>@k`WzRU&8kmh~`>vOOw1MMps*N!!ZA~Lj6Wy%Z)Sz1QsY*d$B;j5! zZH;K;!il$pRvDUYC}duRaLd?5YbYic-)^jJv9RJ?#B$=_yr9xRl<~ z;L10icDn@2d4goFa=Xkaq%;`{vxUjG3WG#(lUZ&7tM7B?BqY*0DI+Xk_8}(H=v>25 zZz|5}O>tIF#o2VXsN9)}Gu;g`Aaw}h=$jbInuH19dnbvRA|Q55%+-vC#5@$nn1?9H zL^bxM;LQ#(wGRpIpVCKyB%N|J>+aWW`%BCVp- zS;?_M)u>XK6KSnsqDo;-q_u~k|B*MctOix|u#_sPe6(8DK75WWgDfV<{*93DNSj|E zO_k!LTFn*Q4cU2xIjPnfmQ|ROYVBd3Ewe(JDvZ_Gl(d>i6McD|A;*vw%{uFoEJN^O z$Cb6Jc-09ty9a}UHk(snPN=npi8_UiC?`x{sBn6Zt8g&ZXiz6JGM&Kof6;(@mhUS~!lcKlr_FJp~(MC(s;kN?5RDm=!@OA=mzEcv1q54AMfFK+1TM zx=Krs%Cyc*rqxv?nleb7L45i(whHdH%)EgRXE>rlE>R5#KMIX-)Ai9>cttJgBpnI2 zPPg8QHJ5gs08{|dgfOhgJdzqP7C_Nu8p@bnNW4i~XBAPg zI2HbvEP%bU*Fq)PwxgA$ae_*+m(XM=*{4YF9;^_{Eq_-&6bB2ODIZMV&lU!Uy_2IQ zoAM;V1zcTWl7*QDuKl}&ez3OT>vW%V7az%rj*m(=hNzFF^dlOklz&9UbsdaPw;7Wk*ALzjFA0~l+^!LoG&^CU z!AdK$lca8>)VZ|7m*OcOg}8IkmL8NUQ&m!ysojHFq~iW~iCrGHq2(%~TmNa6@>GdNF<*w%%yXq^np6ZHALwDNddO61wgOAr@gq0QOfp1neA*`Cd+)*> zJ0dQIW4n% zJC!dLjw_uudJ!So%m|PqBiF}>y}s}?W&tz8zGZ$G`xo(5sOQ<6dFmVW7GgKlaeNY1@Tp9}(hh1^6NbW&YpgpME92d}O zns5PVRLRS**p^xpb*Vzbp=G`oE&?+mxkc5DL%8vle`CnPgdf%~MH}fPAxjOtCbWJ` zJ@mlXIA{$qVOG~&xT&V|WLCqT&U%zW%Cg)K;_@Xm7+jOQIG&CM3ng>70EsLST3oE6 zr)B<3ZtN2;P)au?Bfh2MakC=NK|K2sazB+zE@!7-aK9X1(-F!yhM1}``@Xa$!=2?m za{59lfTuph@$?HAa!$Dn?_qr0=`yZ*k{lx(1)ZATa7%tsmY;X5{A6a4=ErtJA-`er z>!7(?@}v04-Z!V5#z_$!k~gxwiV=hnJT(PFcyx`7_gLgdd4^E>RW;f3&Zkr1=om$@ ztK9F(9;TQ|JniTDQfvE3*EkRfhgi?Voo+4iM?G4a;RYehNKYzV za5npIfwWm!7o-`Im*sG@#O+koORULQ3^R+GUsYZdmb43ncA3(>7oQc!=LGZZtMby^-yTDb?&hl;n~0cbK&Vzg>lRVWvF0nK}yk& zyUpxO3jpjnrj3&d1}a_%v-rl?eS7_Rc9PT#G?ls(iw&tN5TFuLuiS#|X6{-LW7Aaj~Dz8YT}4iZ7ub;*SW(DGJtqg(v1N+2nvUv}GX*c4R zt)ymk!^!R@3l2a?&|*u0MUGB{dXxa}qT-Stza#5PdV5N5pRXeopuGP1Vb6_{ZZJTk zyY7hpHf!MKNI;>i;wxegYZ~gr^g%5ga+buTaM+D>ja<{s)sV+_4z5S337uQiE{$KJ z4{8shM)!=d<=u=3W$RQKGct1BR1e+QYU&b<_ti&p?(((fo@?Gd3$}#kyyeRnq`+G~ z+BfexZ;=$)i^0a@F7kUaH7#Jpw1zE#@QSOQGD ziYgJ=mUlJPgsHH3I9*M%!2CVc(%rkP`;%KJ`{VCS1+@vnr&`{AoV}&2^mxWfFn}!v zKUKTPT!QxS<1G$Ww3-DMat=rZFVN~e)O1fsseH% ztwjB6nZaps#rVfACjJmyF)^eI$d|PZ_=?_6$4GRLBUQNalypW)LbTb(I@QN|L(kIT zV~$wujt1#4Sj+j;Rw&~`q|i8Da+mjZITUbXSb$~`cG!T|L5UggI&KU*F#SQVakGy> zMiq_1Qj1u+cVitjU2dtiUXnkOS8`s7kUq8AkTk7&Y=>2zs-fEi1i_~L<}#&F)Ut^a z35wmEE#FODGlu^{HAEFNfs~;2tp~$ZQoT4k=!7rT)2oYs=9|c2T4$}nVBhdOVznh^p**l^5Jw=^ zRb-ajC5YLSpjZ(Lu__aH8!q?jloc>&x_3QoRy(l;v!lojTDsLyZxKp*mCy928qE^H z`M=!|0lGtLc<4mS{jS)Tg*#PgpLei~tS}wiL8sOIoG7_0O>*cxG+TPU;W!+vp|69M zukBmyWDJBzz_3<<=@HsGmnP2*>b>zW$~LVRT;|;4hgI=^mb=djHi%5t=?B#tSw%)% zl$l;oW2XEUgD#U4XOWLg032A9Niiv_M$-f>6!Np6Wba!tkx9hlNX`s;dm?|+-Qv&W zZwh-pf0J(?WO|3BAfg>97|Ib#S0t^%1#t1R0_s`W(~>g)h%|WDYTB1VyC?~EuH~=z z822JlVr4UZAR{x`ucyC~$*ZoUXFdz3)RCzECfgB^B~QQt>@0%j7&{r?!&k|JImyhZ z!urLmTzMDmATwkVnuZ#~{Gu}%Llxz$11o0g4pLuNuz$%haag1%V|Zt6rYYTdXW_Fc zx_R#?N`Ky&7`B8Dw#CM9xAEon zgOX6Op!S*n3+@TieT|afa4|JYPrZp7WjeP|p8t-qJBJ+T^1K8X(IDk@>2qb>qwBAEcX*&X1sb^zeYU;*6%4ya+H0~ny00MqCTfLRE9z^QD1 z0Rl`_0bTTRKyNf%pf8#sFi7VGOvfq}Fq4B+z0SyAAJyyh{Pl=l zr{%8?>vcGNjTfv54hu1qhd-p(!Tj|>y$%;Z8$D{k+70+9PA*~lK4bO7u^KOrqf<9{v=L5X(^zclF zKKJ(cbkJw6u^R!Ggl9PPy4&L=pwC!?Pb-hLrWFp|xIJDBdc_)>XN`o*9eVBU@d)VU zYwRS%vT%_@&%Zrh1bUf{`a)|Moh>jwS|xC=&xNwU5w3m(;Nk5Y!KKmp0tfwQqu`2Y zlK_6yE)ZN9T_}L-v(18QxIh?yx3Y_%a-p6d@~vLhaSDs754VJ);YB)6^?{{G&kM)GF?)FRoOofl(D>?_IRM;aiSLV1=UET% zykkb4*%I|E>lMd|%F?XmWBG<>T2DDnR5Zsz)HAI294BhECF%<6QOAjjR#QlIx%IN+ zL`7XIL|qmxGop`K10Lf(8*ylJ^}p#1ef6z{0xJg6u$b-xFe6Z#Kl|e z;*Gd?i(0a#J=ewa!}(eM?r~!M@fwTxYfc=$wZ(|P^2G65n~nG@P8`3r?TEkh#PM4j zkoY4fj^Emf#GilS_^nMz{P4u_8P*C-Io~zWTGvDk*F@`x(rm?ZU6k`&l=UvkSbHm8 z9T)nMdpuz)UU}m9Q(Ezg6UU#@ikF`_{*+d{_{8z2wBqIy$Dh)Q8&4d6N-K_?IDTs@ z&T~z4mTRIjT@#(r(uylwl;tkUG8ZLjZ^fJA1>u6MFS^GQw&EL49DhnH?mBV&DXn0UHD2Ya+~ySgTmPXd7ojkHLWFVaWU^ zGyMM4ka<|sZ=$pqGXKn-0A}AVnih>O#7minoI2`=4 z%U&DtZ9A|QVvgMPhU`GAJ9*6(uFN*gwx}%H+57IG&A|u4yHdvG%UI7T-U@>Y>oRc&g)_+g+(oA@uVE0-t2yGSB)t> zRngN!QARx$4ON|GF-2K>a_$MDs%#Z&ohwD8%>wDYDj5|<6ss^h?HNRDm5T?;Jix$J zHtj@?%s#0l>Pv?rEYr09D%f}trAc7I#+~h}wo=*qi&3fNP*wb~s7!1j2r3iUD=b4n zcQ}-iCmTb^Gh1slFGZL_AedtiSpUTc?EB>itn2ff!0y%>9E4KJ3DJgtX`E;udMl-?Mw}_u;yy8*J_Z4Zf#&Qm=uAufvuLaYlfG|L1*G7o=y3} z*tFUFuGy@=q_!s~=1*O5c})Jyx)!8ELe8S?q^Arj+keNPDF@^o9E%i3KYWR6%*;fu zPg}C;6cS-pmg6VU15eX={YY-)WSK`7)_EI^X6A8E%gB|?>=+Y0gd}_yaeRP!ZW`v9 z+3_HIcz9L9rZDQE_ak^y!qtW8nsHeersKo-+zoI+K# zK$Qrpw{mPQvra$|HT#PZTe6dDDa!H0S6&no zUujW%6e0QvJF00#9PjH?a+Ogs5f2D15$H$#Cdc$~%yy~CvF^;mxa=&9F`%R5I|%D` z7RHPdq~a`$F^JefgHQls)i~XEPn$ zRJ84hguBIoQQu2wGL+n_NY2K17<&I(p4+_?7-+%)HJ*~)}M)IdHf0~&;)$^xv z_T-WXk^w|0rNQ?VCuZfx3JqW9#hJGRyUg-R2j)`CD<3TbUIEBv3s7bZ160L1+Mr`* ztqr=Gz)Uvb0H>g!141_80Q1>|1B}S(V$@s8CLCY|n{a@YY*Yi*uuTY9$KD^nXJ-!3 z$<7?0hn+bV% zb0XGFM!j<*)<(1;c!oe1do6%oHdg?BtknYsS*iz2XQ>`An;i(iX{^)(Vm2TE3)$WQ zEMY$fu#62Hz?tmW09LU@16a$R4B)(IOrR7c0^QLDfl73~K!3DRU?|!oFoWF-z#LZj z0dv{B0L){NAFzlue!%Ih@dK7e7Ym#fT_UhLx>TSMT}DskVSW6XhW{s;OKPh-o!~Y|}Ee-!p!Oac-J%SfD{QZI#H2il9Zff}K_j=*R zhW{?X^YQN@xS`>{Q!r`xw+fCm{I?0NZ}@u!&ujQ^6+E}$?-5+r@ZT)hX!sL?Ya9N5 z!v5ho4gUqfH4Xni1Xnlw=LJ_a{C^WXyW#&>aAm{)q2O5!|2e@k8~*nM&uIA13a)7Q z-w~YG@ShQk8~)RRQNw>qFl_kW6r9`ej|!gF@c&8h)Q110;3*CN9|Y$#{3irwH~g;( z&T9CN3(jo#UlyFv@E;SL-thlga9YEERFG?9z987y@Q(;$n(%qS;fDXP;84T=tl(h7 zKP-rW*QW&Q4gVp*{)Yb-f_)ADLBU$X|CnI4;Xfc)Y4{%z>}~jm1bZ6(hXl(F{~&O= zzf^mY%l%vZ=yJhj(E@>Y`q4`SwWJTY&5vFxcxJRn;9Y+7Yl17ak-6O8=SOS@E%#Sx z2XndqZa;dN;2LdRF8BBQ(aQzbMyCtB$B$kixGq{MaHk)!ue97hPrH=M{Xg=fs|3fi zVY=L(^rKe_ZqQEYa{q2WdX?bDXq&*je#8dVa{q$pwF2+=qgM-Vj$S8lz>lt>pgdg^ zmRJJQ)1Ub9>&N5ZdS17u?rCq@-XMY(z7F#SB3;8djP~Ho1iu!05rSWhPt*3`3kcqZ zF$KX_V*=S8tX;3=;gEt~H69PQ2X7#F8RkI*zY^C~?ZFyuN73C0z6xh-?ZM{}ycFXR zg0JMp;k-~|lq1iysSOYOmM2m6<01z(QW ziS}SvOU->T^QCF+VHYdpQpPO6y z&hhZIj(30E@$7ZtRi(O}L3BlX?%N4l&dTk)_JHdMSjGZi+#c}j1dL)t8np*#_jD;M zQek@l>`$u?g>%~jwh^#|`QB;m0oM?)m|4iF?E%^V9zk(Er9FUdN$d89bJ_#6OU!YP zaCUnDLVy;ng|pfNkR7yTHJsTVfXKnZ3{Pq80Z1wovEFccd%&ft_v>BTzQMKa4X6nq zccLZ$cA+K!ZbD4}yb(14@SCU!fSXYh0Joqf0499&1mK%}6b0ZOKMDok>PL~_ULRcn z_%^ev2wzWQzowm>KSAh(*U;LRwTB!~$g64YOWQ-r$` zgP?A(zwPllG8X6*75r_w%a+e2<4 z;1rtYoc4g537A9ctZ5JUO#)`qIIG(O-bes$6vI{R0XGqVn~L!4_JCak=(2*9?EyOp zm`=l-)gEvI0n=!gGus2+Kmbck;Ti1#*Qb-1u4Ef}J)UVwV;rPpnkxI%ZoVq9E|lEr z+d5`lF1-)>nyhKFikD|Sdr7yngux5YsOPbb#av8>CphHB^p1H;iCH08q$d=#Cdc&Y z6gRu-(54LLoqY1+eaG6A!8Vr7fX5Z4O_@ZA*ORAyzwoZ`?A>DTdv}_WGLqe8VF!ne z1$JTwx! zR){TY3g4mIBTD4OdLOehx#Pt9my&_^yH(Ad<5V4!&z|wV1-?Hp!2j+4`2&4TRLxVW zU-QQMqP}|mUuXJMGT7nX2C!abUjxsB@+6B(Tq+;XmLnBJ>g+#F0^ z@F0=&+sbWeZrK9PV2-QrJNZmepS7sWeX|I@k&UDJW^m#~ow~v|NXW-X5Al0c|1rj- z8Qr1io)6tx(Q>6P&`DDqlTKY(I)zT!lgRCKx*{2!Ejlw5}AbtZz)A)uleQPQP(1T04(PYoShE1i9jJmF{ZwN6kvN2rkUG8qZ21hZSXA zZ`>@&*mzq>##EAxDhbBsO~&E?Piu+U_eB5@PwPCbY{hks%?~*v)%xLxK3sat54j+@ z`5`B!T0dN)57!;@!(#WKj9O+9^ZSWcKf=r6(h)x%+E984cYy`0gMfI;{NmAVh$8JiT?bH-WA-lK?WsV#5}lR{oeanQnYtY9g>@l zy1CX@^*bUE7WJQSnt+?7e5wivF^Dr=-NXxol;~!NG)@tal(w(Uqa?2PwGgUz5*UiR z<vBa%6oPx4@p0#R$sXYv|*5han699a- zu}DNtxHiS_v`D8cOojtjc7HY2p2#`mgU+Nq#@Z&es5(y23Y9yjKb&JlgrW(=Ocru> zcE;9(vu(SQ+b1SEQj>h;B@w2)NS2*c4oO$i2I=6|dM&nUHI)21lBL>dZTke#+@2G+ z#wn*to)hQ$+P-R+m*y9;MOwa5jdHX2pEF5Z#qCfR0P%aPkfwWpyay@4=bJtb&(oMgavw5RV% z+vWGv3adoIE|o|nw`Fp%EA2x!)Bw)t;0AEUM-3lYzag0w<6`)v*B#mIP1}SWw$@g( zimB3ubl)%5r^q;5{bn-8ZY3=V zThx!yn;}7b@3aML$f$?rJxrG`(Y__Q_mztaDCBnJWRiyFpgfvQ{rg>_393q?-Xw-H z>V`btrAPW8e8AB_$0=Q;ge;WENfFDN!R@b;4N%2l`K(dhI(oE1e%VRG2k$&c*8y}r& zo+l8IVGo@k>9Us5G2$LbOFMi?Cz7=#n!V%rPof~?YZ{rqIF~6U5?LiqY}P+#9N28{ zzV1?~?ShMq>~P5x{eIHp+Uz@XFA`OC0wb} zbQ#HPhLnA z468F|-jr+23AC8TxIOC!CPncVp{;xRfzo8N2Ef=)<3X4trTj;4u6ew5kv2>#g9kvr zN-ZYm@*M_GnFippIP7wxId$UtorzR?(+wC*T)Uj;ZBkC>j%SPoB@p&vRFw(@R=p{m2oy_*@zb6Fi5*gh89o-5 z_M$>)uMxRjO7{E4p@u;4mgI3>j0&Qplk=tI00`VPFo>D=TK+N`vpjBj9p!?H zthYGX*w@|B(LrD7psx&wpBZKHPGXyi2N=7=&a?{wzg#CVF337b-^q3o4W4Nyp-mwp z`Ji=u^Kqs*z7tDS6ZJQ|+PlYRf>&CM2r}=K4ug)v#Cg8wb#(B5`+r4LCp9gWZ0e78 z7oi4Z^vP#^pxQ4T&Y$Ix(RdQHM75MMzVA+c<}o|fH@FJ7Z(z!{Z&;Ayn>+0rx`Zik z78UMni%MG)^`N5aOfY9{m945J6~(4BgMn!?jFcWO>l|Fo>;xqSL4TB9g}fWQ&omTW zDZ>{>{O$D*qCjMYcIr7Qcx!4?b1bIclJ13W=7AR2Aq@h;d1T0|@9hiA?hZ_*!Qw6-Eu|FYrZk z)F?HFBDlpyqIA^gPCG=&Mh&?lXQc)ufsH1&K4Q0`+QOJ=oy2ehhjc|P>YyvyG`}n9 zs?Y#_fsf_%E*NY6gI~2Qluve$+*7XLBtI<~5Vvg+w<+Sa;vBpD81@S3 zIFAX^aUK;!!=fTIjFP46HOBUINUJi~+eo!_jf}nJwWXD$&C2Owa+<>2Tnz3p3DmX1 zno3itt?M2_=;Id6UovKK>2NxWe2Ce;qC6-+T|j1+NQ>p{FGs{1^**eQ2xPVinc7X| zjuBO2$FZj&)EbKwt4nnN!mkQoV#>ap>7fAjt^)vm&3%BBQ1Q%dflO)sX1X7yrZk!s zu+Dl&_AtObI+oQ8MAwH* zPXjp+D+w#VO*VIp+11xXLK8^)(u6X~8?N0#t=J_h{2&v~Zl|94k=a3N{jbZq9p(Tu zzD+Ee39wo%KLa+~nDPaN0U8)fEm@Y_7ear1<(KuEWfBh7Go64u|UONOf;_vRYD>@9{A(7FZJ@MUQ+tbkTy zgoeL@fawZY)|SUI0%j;+XwqN6R+qb#h_Sd=H_G{f{`>(sb_Uob$3P@mkoB2iqj6`ay$wR}x4s5y2-l5_D zI@q2zV~2(>vx(j|ONWO4ICGlz!14Q#c|#-ZW=Z}#2>NUrO;@9g)wXQrondZq^$ z3p^1si)uLoX%K~~; zw8K&{3RmW8DHqC7T-+?nhP;Z4QM4}F;ij|>qoowaW*o-hIxH{O%d+Uad+eL8Y=HUiJN9l0mSCA4@)?s0PCj#CsjaySMA&-%W6(D&s-zAqJ& z3D{}hmuGxmKI;4OOTI53^L_ca@5{N!cjlKP-#X{Yg$-j;&hhcBc7V^=l#_ebw;b6sM&+!YaVm%O z##D~ec`KB1EN`V!&f|?MeJ1qqH$x9U8+!O#p@&%^2tEAmg*{BxVml62#`|?Y3N>p{nkg1G7Jd??39*=&A--KWh#g9j)z3(`$EEmxDjYnST*qYT zPl2rBFyBv=aXklGDpt6BGTFhUpb7=|Zv?#o996Q{$r?GC?1TgQPI79xs=8e7$z(T| z{|;d4c=)5-{0DA0>&3=A59>VPf23eVzb83Zt-5|fiGhFgTQ)ItUxa|~2wyW766_l%6a{r@p4}yIbV>rUiv}}eW+)Sw& zQMy{;L^f$2^?OmeS&(bxLG9&Yx<%1K*J{EKw;oms7a!4Ma3^l2Y&9QmRR)Kvw-#QD z5^XAOKAkN)nf4~@l=q*a^cc0;>?}Uav2gXvOtO{q-;o|CeW!3RxuZB0OBY)rDp(E8 zjLfElh`@NZPE5e8GLsbrDJCPfMyLg?-lP!A4?1q;m@2JA8QFvxiPghAPiy{Vj_=^p z`f}l9O>U{3e9vJ;X8Zh(eOwgCk-T<_aDrh*s_PQ8$~S-aKLiJ>WtQ%=_-= z4vyRg#kEW&>c@r)t>L@F$NEqHOa4V#Yc1(}oHkog*k0N`uzfJvzP+bh>Fe*^UX6>} z`B&S{W(~v2%;3`cWc$BCZiXfW{0YFGrQ5b`Te7WryJBRk|2JyMvT(O`>(Z@Ncc8W< z<4n!KwJQAv5BIOQ*8dwLYwAF4vw=4?D_gR1ll>d>3BN0HSatQPjp5(K#;VH>)UL6I zH|XI%rS8(I4I5V5oi%gy`t|GAtgpI*wRKi*ZPo2NP+M!)Dd%Dj)Ka@<xrsM57Z{?nuw>@AE=Gn z^~$Q-aiB(MRFD88-Eg3`!mc^`R$*M88SE{N@h|57W?sc9vz-TOqgG^@Rlm%xIa0Oi zKyAdXm)bEU#u)X~z>=l;za>Oq-*uq2#7YfU-R=XmVY}wAk_P{o!6LGnn~y3|fLg?< zCBA0RuGw$B<3J4?aPm2>K;n)Mma7ByugZCcJvxviD2x0z9;jg_$TjP}dv*Ru*R1*8 zbf8wYYYx8La-hZn9QnxEO7W3G3cbyzWyBu1Z6cf>2;B+L{YTL9!H;BdJc`}yP}h}Pw^y4o(G zMC%m729D5>ir{e{K?-g65(!VFUM^5Wf4;V2Bb|$<9LtVmvwQBxxnV95FO(f7z^ShK z#PcSxnhebd^b&(keQ<5WSGWI*!9kDVLfGJ{SU}KUn{lAlJakM*G`bnBHBUIl=00I_ zEo<5abqbo#3t$;$30ZvGxG$4E{_}RhqhKV|;!MzPoXO&!Cs=S_TkD8b+MD&AW++x1 z@94}ap@yvT?OB1LgR@(|SUfpy!vi16cNBYyZJ!a*cIEgy-9<(9t+1lPbZYBgREoH7 z=IM^YeBY2;OQ2k)iED`^f(^YsbBhemIRrY5O;A=i;X{w1ZpM#R&zG~V--jLyC9lb#}7moQV)A4bt(F+A9-;JwAc6Olz9nfIC zh<>K$S8(uHHxoE8XMFKDW4?^k2~-RnL?9`;Nog9B+A@IyP+17M@uD=auT@Y=biX9~ z;k^6N?EJj@CE3&YJsMc{yzb0FK1yZwYJR692WSta!%`<8PI(Xo{v%2x&1XA9VBI%i*_aJHJi2{1ubsMA1ld^GP zlVB#rYjuAe`XkAO7?Vp<_BU)L5iJ|dS=Mg06xoPT^SS+v2!mHrrqJt^?(~KxUysrE zxn-BcRu$3JH|T!1H$ZtRM)BwNpJ>yBlnM7WO812Mcg%c%TlQK8lrj(BsB~|bmdDH! zxc$v2v2{kK=bLnYbC|8i^7A+uUFIl;QsxDll}?38e9Ux#+ux3$Fj6M-*DBrTS8u)< zCr5Sr{fM(3q|EWRDBbTDb)JorW4iqvIh-bC>c3U#LBDkOt(dt4w=YFVBT`lbSVojJ zo-F}A7c;deDB}m#kw?-*H{Xs@JJU8j>omw)R-E23lP1J%YBSRFE4~hmdz+>%6kns_ z$7fR4Sr7!VK)Tv(4Fy-L;O#T%n$Cjs1B*4Q+?G&ql?oo4Nmq9kq`P6m33qKMIH7_^ zXVO)j1)(aqblhzY1;$Ris~m?q_{?eJD7p zf(K^O<(&n!B3))!TNesSU3B|r($UU>J1D3qhHFDXGnu?~CSBH9@Hz^zWa83L(2OVd z$#}A(joT^6nFA(EYWpmLg;!@XqwFXMDKcSX!6OumRd7lMl^q3*;(utKdGnR--!=`M`#+6X^5FIhp4VTGSoGlbe$U%~7_sQV z?H4^SzY$?+&g}!@U0qD4w_s7It3Qt1EiN!&2QsE9JIrFcyO|^~UPqA!C{kzT-Q7eI zSgxbU7b!BtGQ8VM5}2){$o&)ifGNxeUv1y zR!5P0DZ=b&*0IjiWU7uLcT=RoI=Tqli}c%Tn8JAPFqg zQN%CzkIuRsa7AFAjv{wbOD`+t?s}5IHXTJiP7$pmy6Z>+!*momL6H(W2<{^!fmJ$+ zNH()~H|r)_Owv)rg#Vj?6~ey3p9EHT&~wIHffd3&!k-3K2>S_d2UZCC3V#+@;f&{u zcLFOs>N(@j11o&VbH=-Y6&~}P@fU#=9`~Fv7kgHi3&Y}h*lI-$0{eY-)| zxUHSgZ|T`%23_s8bV8pM^r%5sxobP2-xTzSK_?uZ`nYNn+|LMl*r4NXQzz7xzz-R8 zr6Y2~{AX^@GB}~g7BuUg768IF-f?$t0erg%9tp;v8yPYQyG(md%DWP$0=g6kY)BR zvhXpAm@M4gRTh4UA|?xWb(MwM6*F15v#TuhTV%*WZ4WFi3r|yv$-*67W#K7`m@K@$ zt1NtoA|?y3>naN$q=?DFk93uV44hi^1IgNVv6XPH*kEbyyA-JdyMtiqSsX49vBPOy zIyW-Cw6{2Zw(<+nn6 z@uWhNUM6NN`#{Zkb~d-shhvi?yM7-Y&CCO2+B=~0=Ka}an`;t)I*O-s#E@MZouC>x z!zPP0ky}EsbE0sYy_J-FcfN9j7EL8R;5I^sB7 zaO(-*aH0M$WN6u*cp2BW& zXtZ-$Ac@zJIW@x%`vBkP2=T^vN|qz~v{yL%q{->`IgDQ`PQfp=?7T(U5s_ z(>x0H<6DfEIO5w*6Z;|E#N4`WSDPD&JZ3oEB-d~?VOJZt8nY|>ATa+HbbS-`Y z0U?u~Br+WqQUnv%QbV0>5MLYN=41-ME7-z-MQpA5KjWkHrgcIZdw<%b(ABk_->!ai z*Ai((pSE##{EpHOjtWD;K>eQ^N<1ozWZn}M;qzba{QT%D`xJvh>#p-4Slx9_Dc$vi zQo3tSsdZTWXSQ(3Rw9cKTFNPe<&_Qs_qL1KTd)1!l3y9orl1h^C-E_?mBLql>z{J@ zuJRBZ?BHbrzlS`Fda3eQAc=PhyrGZ`X8!B<}vMPC7`CQJu74M2) zRX%)O|EEjY9Lmo9&)CZ=j#BnRUg)ndRNCn6LHe^dD>*B_4Ht%sg9t&W7sdrS8Ap?uJ@IZ*2?37KnpfR^0;E*oNYX|%xmM|u)nuL0^VMXN(sR{h zz0xPD$>t_+!u`T-;L9paDl#g zi+j8r6~XL>KA`#gNhC?vUvwLYr?Q?3csp;b%yd>1+Es9N8wXdfpsbKOdu5x4P|+?HRuXFORaf~7%v6?Mpc z2Q}(dP_rryZPtskLN#GvND8@#HF3@E>gnmMNBwYmrd>Y0%&}UnkY7g5wu zxj;Tu9cmCU(MY&BJe~5(7Pnbv+L-cmb(hw*^(oMdKE;vnDXd=WQ^rX66!trXL&m?P z0P(T@zz0E#?^bn-c~PbQ=X|7=z3pkSi5?e&>G@ZbQh|6(L<|G{OcqaI1WL!tY*x|c zrXPR|I|Eq}?BL8dpvKFqt3{2kNe!GQt^+z^biy1VEakIWtySa-|DT8D{Zet;@G*}q zZTHyvCS$)q3Rg4Yd@R!)?F%E27&nBBIJa8U6NQxRS`eloM%eS_5PWo1c09uy8T3Z(41@2 zhO%e=9$NzxAiYETrMiyJC^R1W49+EL*$&lEQ`_k1f$W$6(H{;RWe2Ea(%8b#5rrFkXvwhWG zkOtsn+*P?qImTS`%H|-n#%i@iWM;1ceZ<0H$_Xb=(o=7LiL54T86vVc)tGJ!H6Iafp zF~<%#o5TmzQ>TU&VrQt&m%Elt5KMPuil_f)BDrRb7r z-=^aBU7~51cn2}7rLyB5NGf~5IjQVcih)mGf_3H!YPW|)=4*l(B4@a!-DrM;FN&f3 zhT(rnek1#l34fJgn{b6S4>^Ix2oZAV z9BqLS!5H4Ty3jogA)|c;#uhge5CO&nUSx<3FvNG(rP`Q9lVCov57lL^ybEESU*f37 zG@AM@-d){?$|A`Dn%N zFY|WZ<6pqZnjyVF6FD|@FaIo+8G^4hrzV4K^jAzoTA}7vWRmc(Kxb#J?H6UO@_-_aRtR;(8Q5o zqL37)sD==VE-{H(#5Fg~sT!5@T}cmrON!*dPMkDk=hJWzCxHq?3p5o`I1L`KG!XG{ z!APikBn?w&lQjC09yiXZurdq$YcMj<9z7=Znq&*{=Gmf-sE?QUyt=xjK%{Jy2f<4Y>R1cQ4ohelw==FhT!PS=EfYDay&`H_Jg=SdF+o5ICicvW z5*>Y`kQ%5@m@rFePyJd9fZCbAQJD@x@q-LDyHqaCA&s5z(wh?df<>|=GN2uafq~;S zneEQ2-FS(|Fmxr7B`wnQb;M9kmuZ?7slT|YCqh0e5}|wat-WGle#F8vHkuDeYFm54 zi*dYRd}&p(mc_9;t0qu975mQgU71&7K6Uh2ss_GR|4T5S`iJ%#ehJViir7*(logJY z@OR|~w-yd?VogKUM&gN0DA~boB*_$NJjq_Pc#`dG&5~?k6P08mVvJ-RQl1+2Cx(J} z%4nDE=9$Y{Avp#nYLGYXF*KYmbEA*QZ@CLl%>z0W_suYT@V(F{*RPO2I`$7w@*gi|Owz%a6> zu%CogBMqjk6Kx?e(T5A?ooKmB-HO{F!0d(p303c(Ywq^k{Hq_5CyzT#qQD2YWMi&P zq#R&H4vCe?4HUBq%jpX5dSn?Jgky*ri5}!rI-Romw?nMRiOrOG{cg*jc*NDFQ><9G7>HYz zu%M&ubT#MhA7P>l71itt^B`K?Pm>jTca(rRaK};+JEwZ5)3suU6wc-=b)Go>=b+^KJBz7zI6prx-EVq#a=2*ve`SSlCxT0Nv z2Ci6YGI&U*z~47ln5(j;JT_g#u4hoX5DjzdCm8O>zd}^%9*0< z+_Pb&M{ni-S)a1DpARcRd61ki9U({$v`U83V#IJl;p~NPQIQ=iBFH3zjI&jSNXMAC zX801}x80&~=Yof37PS7Ocl)*cSh|2TBxo^4lJl?uV`~;erltYX?h@vSUiVc&6lN0C z7mhr7Q3gc}Wk1T>W-2bn7waI#4Ia^1zh+%Y!GAjSg<9+bZ8W#XjCK7L5;PEi4qS6R zAZCi1RY>D*D%Iwcz#jEi&9S!i^}aBetY%L&d&4m1heUf!xC?LW2|dKwArW0gq8Qjf zg1*DYXaXU`2zUKpfwoAOFtvwQAVcUYy~g@#kq;9yO3^1VQlv|w7BK`>#j59pGcOhjr{7lCdOFv zNDAn>G?l09B75f_Hop_w4mqe=iO&MQ$Ml?n9~p!vj=M4;Gs4kom1fvZ4nb&(nT3be zNRKn@qk0?e3*g?69yBjwk)kT&r=`GA%~1A6j+ADHsuj)p#iy_d$#GsACs8plLzCY9 z8xpBzU0(21PULMvH+W`bH`X!Tvy^tWS-h8asq(qSsw7&3_=eT4P;IYQ2XZyGVy&Ey z)4+_Pj(2iI*ep}pXrOF!XE2cTLc^tuO*P?=Nd>`?yw*t1f%sIjop!9-7Zikbe%i1O zDP&Lm4^0YaLkd%^+iEttp!xCgc<5~D7g)k$MzLtD$A)}YfH%$#FB5X~zsP)BV;b}Z&lfXJMAu&_>QpZ| z8L=J*@pZ!@VITMS7&&c*D?pmT9*3D357vKN#-fC}3u$?C%x*s6>TfL+3b(LRFseJe z>JqEInYFzB8`T%7c+DfMi@L#w-LM|wCx3{oXg$PF{?Lk!hYk^X33bYHtD&mu!>&(N z@sn3IsykdSwpG>n(4mKsZJ1`Fb&fgzp1x3H-^2M!T3LsP0m)*6+2xfmNn=5}%uiTl z9C77v#p=zBjPizsXtMz8QS10=wocb9<+R#npVNAi&^8a93y|6Iz;S5~@W4?FmI?_S zLdTh!b2=r$R-I|XaC?|YSxOB!g^6UBB*l0OzJ?-L2s+JUxHOvy2-PYqn_Gp@g%}u= zw2w+^d)Oh{8dC|Vt2}&_t#3VWD6OzoOn^`!klKfY+UCMPL9P^hF|^FlGDNU7iYr2M zRpvF%cF6(Q4tjg@hC=g(uC{rt2M(n}G>_HV(7dH;UM1Y1F0?goyPC&TM>yk(rRe$1 zv;AlQTZF~`^39vl5^f(0!OaG;>^{GNJJc|W?ENprjL6`2e*?_=J|SUr{u*Ayz9bn%bZ|xg*=y?l6wh3*qZ|Z`Cf7MN=z0 zY;9bUm*vdNl2+MT<`hj$bWKBvsmt1XiYceGS{CG4ilw{A)prt)kSk)U*+=IU@^XE* zzh{c0NB9^Y7P4PDiR^Kasf4|tLB)iu@nsAf8P3O9s%L~94sr}C z@^VaVY09yylcfDYI`lV35?cHI1>bR#WA(hR>(r7<4R%{~_NOJs-hazJkl@4e*2dfV z!^hJ)a*UwBpN-xX=o8gFteG#;a42^RKh?Hkbe0^JOYZWf9#o!X)j$vwmcXQt5% zweyca>k9%cj}Iv$R3yMaXMD&S<(m@C;zMfj+5+7o1x%uul4Wv}Ml{v0AV-;S)Y>K5 zU@m}=Xe|N6V@NbvOPWNpt!)NIlW5jZcDzlN*_yhSWj%RSyw)6&n_!i2X3UBaRIC?N z*``YFpy%m$6mA^bqk6OKH9)B-d;0Cy+M+}92835A6IZi2phSl}uR`;pL;ljfc-h&K z&V%~X1}@_0Q2ad63Nvu4IeLLvDN`yb?%T8H0BkKB$01wD<-tqknY6r)DI311rS~I! zQDQl8Zy2pG;Y9ip3z#V!+pPElpoE+!uxg>tDy#OopEq>=G7YqeRojd`Uuuf8IVgg) zew0R>jWM$>XV3M>tWP;??eXL@QD%A&>anZVi2+!~udMkbE@!VX&d9Am=~VW8iz~_V zD10dOt$Vg=&*Q#Qi$v#0bq?)Kc~7UsuUXTcCQO1xuVes3yv5diDlgyIf%3U{ZP=(i zO5qOE+D-mw0G=0~l$9PS_JxC#WiJtUt^QLMpj_|P!d%Q@j@4ogmfI!^f7hJ47qcRz zwibRd)|t}YO)=&L+Ns5D!+BbT38h=9eSCb!Q$VvOAeb27;CQ3T<_I>w$}9<#&Sph< ziq=jhI)(SGT+?7#J3Ovm)C)+6i#x{U$@#bA`kxK7HIH7!pFD3^>EBRg0}7o0Pmt*_ zQ$I-7TvYncgS$7zC)5e9gr#6y|GKqrF6KN+uUxz`ZuH8@q7DWWg)Xam7w%p|`-g$Q zb^|ELo}$6wd_-^d2JCES3z`pI!m06V`uglgn|!SlX|MG)3cS2YaCYUb&Q@*OsGnEc z%bRu;4^e8JB+{#eyf=03*EDera(NfC*0-Cc<6>H;#c_v@R~Yrm7Lwte@qQAh;7;wz z_wpWCoHM~J5bRXHYh!*duhMDXK2ml8`DPt+ejXg?wAhPEp(f!(+x)MRaz%imo$-%w z7|C)PG()lxIZ3jPZD|sRfhoxZ`#2}!&YB+F$bZuPx$gX?AYue2+EOINV}8$L%T z6fSf78n>=wBI;dT_R6>P&iDyA(y3)dNQn!+^Nka(hEi4Kkmd<;_~bX$M9}iOQtz@zt|L*!yYt7t32JN zE88eH9XBc~N)T9n(UIdlLbHy|W*VJ|V zPecAC4I!Y+cW0Spm*R6h>dv2pB`g?oq(w7A zn@KT@o{Kc7Ta7$&4u)K61L_}u9=zDfzrUS_(esAU>OqDOJ5$x4SuB%&NdfZ+TF%~t za_wuHcs4wO=S#wK{JI9%tHrbRS39mb6QcoF>HrPLwpGJ766pmnA`6o5Dm8E82vWO7 zz$#^kZ(=Pe`Q^^Ob%mBf0`dIal1^BXmK6<~w2p43)Zu(W>E*&<&g*h97pbI{v(VE1 zfsKCUq(_6`KvJdh*H9FEU=if_7M}1!xYx6gGxiZ1$>&fVH-yz-z1$Cy;7l`w266TU4l2-NxK8@r5x?09P0{H--dL~+_rFD>O^@fV-bw=ZV*lpFI&O4 zy4wChs%!WNR55nTz#r51`yhGOPbIf~0Uip`n6 zN3r~63&l!>Vu^@A+{(Iygnthdmva=Ca}<~7qqy9TVi-CIjX>3{j73n)yFqa-1VFLn zJkyr*lI1+rmUG#1o@~o`!E(;Eaty_^>r(i_7K$0Su7u)p3&ll4F^i}n#x? zSKdOgQlXf%iDG44g1_Gb#TfMk;Ze^~T%V8PdOM2qz(gje5ge_IMNn+A<+)t$f?~^g zrY+|s%Xz9Thbn3JlWjRKSkAdtj-i-#T?${=LNRBNuY_U+D`Wa8K@l5@Sy#wW>~a*l z7K&X!u~MO!w25M6U2^Y(jAQeuxRtR8ig`CwoR58=*m9m} z%X!Iio@&dvY&lQ1<-A}y=UO?2V%l{nd|?a4?#d{3EfhzF;>)NLIgT&Ovd52w=p*@9 zI2Ew0R9Gf$Vp&<2E*IVt%~LZ?G*9Jdo|=#5sdh9|T?d}qauz|fUJs^KD3{4T@0s?z zmwet+?Rl4d-jnTlFZjH3ZFz=r+IXq?xhWsf_AE@jJ~FtqAEx=MJ?7` zSz4=f`=*vk$G2&z(m7GX`^Gut@_zMIg-i1KD(Z>!oU#tMqj1)LY`2WRQo|Ujb2SX5 zf{iuQu)bUk^GX={ua3^EA{NsHf~q z!@{q!2+WYY-l;hugEeTBeVO=c@&DcsEhs@0-c+;!n&{LrUPB@Gl8v((x<{agd(Uqy z%LXN%$qRJEQZ6&0g)6M_>FnLPH*B#>{#jEkL1{f0N33?nob}%{4k0;ogqZC`&JJtI zk@UIMEPMW>a0f?eYSjd{ezt_{`3vv>OA{n^tb)# z$kQ>4=yp~TGGgsGPKy>1MF-q4i%|1=*bHH%yXPRUWR@#Oc1&MpQw?4uoPak+vaur) zMa;5fsi<9t5*zD^qXGh;d4EMY0W55_j?mH8a<-AhZLL>xiV_KRBm0UhWZU|(!p!i^ zpd-<26K@|U+pyBxft6mv3VWu+OL~7;Ve1!Xw^>z3K0JQ<0)8q*2*vdY3pnHzc1lM< zhFFw!lEdSP9VgMYi5%wQcLiIsDL$XWOqwUMN~G}BiXiQS2{Iz6R4R>z&*tmbJd&0< z(l8^D1^w_dKYv>GbxcpU7S?6uQy4Z05r)SiT}vkw9ecSEYmLHSZ}#-h{wSQq)DqS6 z`JKcTtMQfhL@CAs^9W6jNAKWh8z>!oe<&Rah#W>vb6AuW(xHITWkM-P9THOMY7L-V z#~fp+D&`2ir?mwU!K-=e2cc8pO-O=hNMK?&05wtE`UqN8O@wq6ekkf^wQLuF29VtQ zFb*0)B-unnNA@5t$>yL?-^{cdAx5W`YpErBGk+_=gJ?V3KP;veHcX4g7CQYJjzb%RP=~|=Q%SvdfK$dL)hlvgv%sq4 zSTVs!!w2c3XYelB{32^Q-$5a`OoZdu^bRx?KkPWZ92@mu$SUfIMLp6)EvWDfPSSaBH7FyqozHD^*l?mfnF$15_&cO1+0dNYDcZ$uD5aP;SpTlSu8@gDNG2%YyqcYRPL z-Rwz`RphjHYk^aBS$tBmGut3X&6%A6jqkYdI<`e;dFb^&3(A>9tMAq}cvY|a!`b<@OQ*-@aBE}22j45+}s z!`i+ECvM=#IJJ0`ikVW5vCNknyhf~;E`wj19A;9R8_53%E^a*+ln;$(#o4U*h?v^y zZe`%0XF=>4iQB5`p_eq0AiNYLtF4M<5&6 z8fAm*k5HoB#J<8O_4A1tRZ!57`_49W@FG)ImgS=Tt6!qwKY4CcE!s z8ljjMHnablY*%Vwh&D6bOFp98SIr&aX6)sz*X=~IL&@FA4NBfMPrIArDS6=JculfP ziA(UfxjDXU{slM3bLLoZGjl?D6x`3r4 zxjB9%N_vZ0$?gR2mz&wbPVQ0qRFvGS^o1z-ywZ1~ zWTVnYVGWYE&HM1?_%s~D?OWz;cys)a`~`1j!#TND>8$(%Z)RUP*`oA;WQ~&7qhzbn z`xECPUEG&$Q1V)oZfm6X?Pg?(oO4?n>EGDR*cIXVcS|Gv!tR*uA)NbO+erWB?pVid zR=LfM^nRSGa6RERHPWAxpOpyy-q=XLDAy|ymh;y%(!V8dEV{m~!HKME+>Tp+<(Gf? z=7zie)|)EJImqsAxb@d=`^im>3k}MAq>=Uo_)53Ekq$Ns)k2|*Y4rltpl)7ZJmun^ zaYTHdZka;5>OJGTIYXdk+(T9P>iJx#?(SwypUq!4?(S`*{Q*AiKHo^o%|djo}R z4M8|A*lWtXuELa_Tl`kI0ljWFubXh6rYg6|eKJ({sb)=g=C7M@cQw)^z$e^i8fm3j zs6T(*$OWpwyz_#+ro!unT%ud)=OcRE4qms)eT=HyW_L%Z?nJYuA?nqF1UCQf*-CEs_qEnCRbz6Ad8h3!I+!l9hsBV9= zrhWPA*0_U>bX|b2akn+n;bx(=`RkTnpc;JdUEnpmh}SJ~>vX$ZM-KG5jl9meTd2xy zbvK3TZf@4JH-DXTQ;l?efIF8p(k0D8>+;u);l7EANeG<-zs{-5&Kvd120XWs<1_ZbZfas4u1E6TR5drQ5AWDHqph^5e z0$3)%2>|L-y3+uo07eBk20+*c@dj89V7UN?00>W@$N*yi#st_8Kwrei7=V~6ZiN6- z0D=H;)qs_zWWd4IV}Jw0x`6<0ax&mY0S5+iBLUpxJiyo{yTA}`SpYXV1@J?F0|C3y z0B%w?;906Zc-xP2LJ~Gabp48q#VHa0}dVPRs?Xfrvcvwn3d6|`DG|}tb9J@ zFJIH)p@zSnn%T)vM2YHd24d0y99JnF!Eu$+WjL-Vi9}kezsjYJ%LsQHa+fJ(G#ypSK)GBgV;}fF@<50 zR}6-xl|$1ip=m>*Y4y;w;n1`tp=nF4X&f+My)|zL#@E&mMjmU(uu>XA$Y5*8Ql*Tu z;NBYYx0Rx~aE{;ALPPpOL;6EQlF*PeG-PdP2(H8oM>qikM;#4e+GP#tR|-ZL7Of#= zrC@|vsWpTZGHVDP9IPQVhbI|#%?0w9N{)za9`v`9$OhqFSwuM<}^2qvKQR_ z+&sh$!|w(6H@TVRhH?3V`vNx)aKnIo!Tk+x?&oHM+jha-$IX4*)ZBHq77BB>xRn>v zl?}#PyTRjKt#JVgL+wC5+~ae{FQ(%S_gR0l@nSk?MNnH19G~{N6BpBohP%_>Y+^9B zBIrGcl8^b^RTtA$4R_q%Y`&QGSrIfR3|hMbwf1W;&a`_)HU2*f3x*s+HXZr{*YfE^|>1^rW+b=r@z^DF|Ak; zlt(tBq+ht}oyWsovi>&x@cD+j+xhQCKXnh#8=q;oPdWdcsI>?ujiv5{^WTL!j3Co^ z=Z-l49jIFfJ&j3jzw_-zdqY5K+`y%~Z|6F)hUlvs8xAMG)-FfZq$xpeM`#xc9zsxx ziu+t>*BDtVXxCj0_sP(%5wh@>!hrAY2<@tqwVZYxYq-OqT?1r|(yoIIcWY=@nXF~B zYpQ`CdEc%QStGP-PeYE_tz88fKXW8EQu<q|JFlB%K-qQ=F3M-|I?oB|e<`Fi#xI4K{cW|)HE}k~;<~HfD`Nd<& zH3DCb{QP1r@)L|_%#pdK9Um3AknB<75}RIp-<=q`>BS}`Z&~P3O)oa<_DzdGs_BKzGTw;% zyy5{r%XmFXpkhrgwmQf~(~E6Nm|kqirx#nD%cmDx+?srPajjdOPcJsRRr&N{lbgt= z7aQGpKE1fct<0wv*XjBS(~G7G@e!46PBXUaCRZk|c6xD-nsJ5cMN=iYS7n>ijL+*P zS5A;?S2ex(v@mdm=|xk;xl3i6(~QsPCRcVYrdKz;_?VENy!Z5?sWN?BWt-ECPv|CB zo-U?WH@!Hje!s%>qN#!%Q`zP;<96NT%Gky9>ZTV5L~>V{UNlv@gDTscX56NmT)DfL zUfuNK7E${ZrWZ{WaY|*I(~L|vxw3dMy}IeeM;(lIh3Q38<=msP&1uGsy2+K#i|L2c z3$0F|BYil%_;7l`F#gwSdcn}xnqDwwwx$;hrmg7(qikz>!SLIfUNA1VrWXv@t?9)! zx1oJ{(LSS?H`i!QFWP4m^X3{V;-?qwGm3e04Hfazi}o4Cyt#&opf)kRXrEEcn`@|u zpI)@jDCW&IRK!m&+GiB=<{B#Erx)!rig|Ml6+t&5p$9X)AWUz=X|Pk4pI!{O{SBu9Nm+h+(eGGQ%@{P4 z<);@Fx4Ypa*p-E1#`L1*b~Fe`OR?U(j=7Srx$(hj)v1{ zudF(CEvH?F8%`s#vi$U7z}?z#8a0*Wrx*S1riRnVrYt|bsJLAXr_o1Qg}=P%#d9Ng z65jO-rg*X-f#O+6ieCTpOXtiEmgxR|_Nquq3GJ~(@fAv+h}dfnkmk#$Y6qcL3HtCVrROcIsMKVp_u4Y(Q-bnrtMF*}qq;whxg*I4d-o)72jv-+ ze!1=+<*)+D7{2{T-U+%iI=uRc984{fTWtc*&0}f(VwyITKY>=#%zigY*IrE5H!iTB zuX*V31y}$4IJ3{S4OaRIK5~&&x#Xf$l64m@zR09**d?en6nydFlP-GrH|Y|+Z^BnO zbTLgDte;u^Ou5&EnsJ1F!3|OKkkpiGNct}_lboPt)nzsJOZ!y8R?Q-><-W2(TT3YW z8|M2PL(LqJxZnn>gtbA9reLdPQR%3!tadTo++a!2+RS3Z zwV`GXKU{D%YOYCf+(OcKkvZ}xHLEVGxzCMCv+rxGS;W4~S2l1l-P&-y{)Ux~ZJ}n) zuv~Bh)I1;!c#@>|A`|ju)U3L!W)@7*wkX(Yvv_92S6024vL4anZ&wYPhoz7%+Qhi77D+Qz)QJ=ZdqhFe*wruMz8Ifv!F08W)Ja#9ATTTHiq>*Sg*YlfPx55~FQ#jTi>9UVTNf)~L13^)%R1 z83~mb8H+*08&L47D^jq=IRfWt5M3H7F^Uy0hz%gtl~*KIjf^`S)O0=M<`HYeDY6&E zLzgSANS7K*jV`Bhy1e|KLzh#cm;IhDzaQxGkf+O+0$m>Sbos+TmnS@3z8vWCPEVJw z1iHM-)8&ua=<@EIE?a~zq{|e#Wb39$m#M#N@%r6-`Bl*+8$-Sl6U@46RdN@V zToqjox1(V(oDybCG}+KFj6L1%d@Qas~a zNkRWG4JT+NC>*Bfm>QyDk1@oMyc>B5-)rGy9;4|)Zl%u}vaCnR;*h1!8nmp_WYyTy zvLU!;Sr3tgRiV!su&i0KFmv=-Rm*yStct7qtcqpbPnKe+p`hBP#s^u9?p%nTrzxWD zNK>={;x+ALaf_yCh`irptmMZFux*Ot69KOec#M_(@B#8oaXc6BI_s6r0sIb7BYnYm zZ=qXbi8rgLLTHLo5=~uf(-cP-l^4+zTgoAtVoPmYpee3kM+i+(N}{QY zZJOdz>0b*q#lYL_8=@(;)W!vx;)ENMrpW2+i>5BNX^MoI?$qoG&_>V{Q4pG%VzGUM zm-?ytP!ptnwzZUVz@7COGs#&9Z1HSheEE6j_PN06TRe-SDVoSsx^iMY_-|+tZhV=l4$B;o2E#d#7?g8n&NV4itIL8C2g9b zltfb(-|*Az#j zDfWa)+BC&+QLxm-_oiU4DK3?!*qki1X^K)3O9S2zY|H#Nm^)7!kJ7)GsjO))5s?V4g3f!a02pgg``O;JI2$NmyE z#qX_&w|Vz)PsZLN(u|u{noS1^9&SfUEZC8h(eFqK$wWM*|2jX<|FiLwo4_5|S`*T^V5RM z2EZ!gWU9g@sxqC=pfc<AunuF~8xX7HF#E$5{q z!}dYsayEq9Sl<0(w-T$6<{f^5%vYT(G~$flgN(!aOnm;};#9^28MwjL1Q}(qov2J- z$WQn}iejr)nKsumDtlpq^QoTWS0v~7&Z+ZFyX0IXuc)~>fAFSS4tpqSY?knsn%JyI^zp6K{1mcLO zJe@f#&+6aA5Gg700a)l=3MSoLGA>aH4={EBN5IRRS!7P?cJh2SVo|lS5)A?L-6&n@ zUE|?F(7stLBE!v}M%mBSoJ>=>Sz0c;y=zqdRh@h%WyIQ~lyPmdQii*=N*M*$D5Vij z>17>$C*`ov7Ns<5T(!Rvr4wSHt-5(FN|$_RpZiL(TQML`r`z~D3Rj%QKFMu6nUzpEM5Ihzj7-0Wf6BsfY=Me;Vi>Bb$XOQ# z#G+(=nv{%7>*1pm8B0vEVce~(yDMA zj&TRDK{LF4$F2R&Ajt!m2(_MZOK@Ry8az-Gd1Q|&BXW~g6UV}6 zyAXLj0uNWpIp1YiA*_ zDhUD0PeW|-^cah2<7pYz_L;$F`F8B`OFDygI>m=$JiRR<9*T_h?J3Tn@&p(ltlnw; zz_wMBs~7@FyyEv+tw3fH_wF$M{AzBgs?p1_j^dQCo}mC?L!0?6u|uP)-%D%9cwN7X zzLzdL-udFJz(7EhP7xf8`8}m&I+2$XXpEN<1O+<6oN6Ui#53T4rp!o+_!2dPBX$)F z6QD{k9?Vq12!@-on8Uep|IByX2rl%vi)CT@oyRF;M$G;e8i~$5=5Cn#Rb==K4=QN= zcrr3js1{^r^%4Ap>1_CQ_z5GG4=H&4rGNNFA%n3b3$hn<^EN;ApAOkGOTshPU*Va? zvkRZuafN4|$e%HKQS>!WFR{@J_`>ZYUoU49q8J-e2K;aWv1kmT+cj>Qm@hUhv9Xqy zC2v_&f@?Pj&65W;>8TdKLgpe5v6-h5Xu147a*+V#lRC?eB-7uK@gt{dG1od1iFHWJ zhB5CEvuIS0s03Q8dWk3vgEQZ0bxS%p0{~j2g6LFA0AY?5tB=xX#=i{P!?rFPk-!{w z*AnNJZd-C=bT`Syo$*~HIDEX5Wa7q%+ac;i_fcIKV;|ju0?z^{Saas-%dAyUJxR+N}(@wWk+pK|w~})&dAqEr`_u6G&cA zC0Sf0LR7(Cv7XnA3hTlI{*$37BXe*uEoAg&KP#L}HXx+YF|xihEGza}JNV*Jgapq- zJOd{Y36E#C77B^Q1;vHSG`2!DjR695N=t+@uN>t$@Jo{og9Q{Jx#*-3h8s1rnm|{& z)3-*9x#G68SY_j5FsTsZFE$Wh(8Z%ww^SG;iWq-O`RnJeqEbGqQpBto3SME~7iYJi zKV$Q)V0snMR63KPFTw0wGrq~<6rOUuvy5y~2AASdLsdZ?Nk3J|B4$BV%Dhrl{k|#} zAykS9(O=LN&oEt3)G~-ZWCswYER)Nk_~$b zdr63vxtj!uu!97Nu$>Ve4^M}<8`x9WN-`=ZlNkNVBQdn2n1NOKcXcEc*=j>xayd{n zHH@&pL?cqM0bdNuLct5%!O>^rzi_qrt?^?bs*hqETf}Od{KSZ$`tO;-I|q%)U)ad= z$qmpE+UI#4q+fm*lwNa(MZo1wQmS+ZDg!Dj{-k*lVFttDABn<!^x zQY^Rvvj%Fxkr%FYc&)kK(F7vDM?aE#D1WbrM|>RN(2%D-1f9G~@EppO`Z%~^6~Dvm zQi#*B?0W^7erFZtl^SuFEPF`-n{t5hw)-*}T`=(56>%HOKwvUrZJ35nMh!DWo{Vfyrp6Hhj2^Wj`<(F_B@5XEJ)qER`pt-H1uu#>p|I|Ac4&N`J)+ zmnWk=S~KJ}mVv=!#B_+o?8yk*2`rf>qrF-*_SDuW%5y|jiGP+r-hTO(7 zK)_kKZ7fel&zPa}WHhB6GHzoTBuqxXZJUad(KpR7VKUmMr6O))*(Xd!OwCvunv9+` zOX$gHzfLo88_Qr}GWs1mKQ$SB%M7C@ql4NN<2IH7!(?O=JksaPkYO@9lsIKS7t5Xj znW&p@N9jFqkS>1^%{DXR^j;Pklz9ZH{xcL;zxZ+H#t4kMH7fVHnKYSoPM3Gjq-#3M z(J^k7fS;L3-7Fl>B?Dng3CK+(&e4yAX=sk06#X9j?LN_;XXc-F6%6JGv$`5+#NIN z=qxHfmnUY@rJd#WQf^ql+X-|w%Y>NA<1^`SXSqF;8xrv7OuA$i$1q$Tn@NW{%W0pX zCg9gbe7YW1GBZr9bhP%H9g85Wbo=N zcOB*WRPI(n+s-oPak-zhyUudkDc2)lhSQK)R30w(;YYBu+&0P;1-u2XBeOVW;BpH0 z#hvB0h}w>MnZgQ1s@vnqS|z_5CF_))NLXv1j9ACmp!DMj#!HjY??=f-rFSNn+-S8T z*{t+a$+b%UrJ3+gMt3D!b^D_zp+S?;XIZl#`2+LGFd5y=?44UyO!)lC=w2q@BtMRr zeUsiNXAD~Ukd@SA^aUA7Y0-r>5Yqc)BsCemZeAEBqc6%zYBKsmvyz&O9!PG|?Hgt! zr7PyYvr7Lsa#1G^roq)3chjs(+|8Y^2Y{_|H^OmlZzt@Fz{cH2;WD?U6Lvqa6>b-t z<#u<%z5r~wy8&);J3C?b0bAy-hlAXXPT0M`mb#C?HSW4j7@~zK)g+waws*piN=!Dk z!X0i~Ck%ncoa0(J!fokhZPTvf4`k<%NKM8dDkf+nP0-c`nbo!@(PR%rI zGGbT2==6-I(?1Jz`lzSVcLJS$$jXY0pmU#{g?K;H3H+9TtKD6*Q17QZfzJv!;XXAB`F^q!_)P&< zx;tl~-%oS`pAm4(eS8)I{#YmQ8v>5H6SGk89i70Z1srk5XCdL+JAuC<;1YLi78*X< z34BVxx;rup5g+aZeqF#pcW4$WzO5735OBa9oP~@JbOJ93SaJJjq2pUSflmrpcKc=_ zfpwEbr+{^nMGpb%CW{^f)=d^M zkPw+g+ME1$HBF1*RMyBkEQ=?oCLS&{jf&rH1f$}=zbqO)%L)_@ONX@r#j}Z-C_AS2 zcwYS<&1o6V$@1{5%)}I8O1t4omN40#B5y&!*8ircB_cJ3fUW)6_ZjUp1waY0*sW!I zmw7iOlU)$kU;o3Nq{>`{rC#hds2neArTSNfE#zQ-w&n~led>U=43t#lVU4Nx;Q*+6 zKz#vV!~;ea0LDCEYyp662fd?^)@-ZJW7x7vWLZ=dqKO@@D%`TQSF=t+9$9=OWhYXRzMCm{cXPC)(%oqz&+ zef9wX<G(Wq{`&ghq<6|c)l&)xsH5`4pg-<9$_f2TM z1!YxPcxi`@w4fskq2BqS)jZY#RjcyX!h<4U`FPFqpa|s+NG=VCr2Un>-7my6Ggw>G z>L+gYz!obY1o4A{z>d}fn?VB=x{Vj)G*lK1**Xj~1Wid8xe*Z!H-zLP$~xc)`J5fn zYNtBD$XRcN4?j@->WAm>UU1bfh2BKK?^l?`Q{02S7M63h( zxT`hAT^&I$u_%SjxT~!wg?zO>9khyb^ugSF&ar#GTA$w5DpaQ-i(*)O*l<|SVe6)u z%(j)?qWiYGcIZBTS2^1~!xB(mwp$;HgDw`x(;@m4T(UiOqVNY}Sef3SXTBhJ){d7bH2K^^4DN@!c&N==&m7`*s_N z|AO7?z-O%xpYT~L#3yCpGa)o*3;0Y34?I1e&%|5s*}Q0?9-$ie1Bbn@ZfoIIEYgfN znllqvmB0*|q~tWEqU^ikEAvJw{g4)9LV3_O3j|GOiJZ0PUmuH?oE^lWX7(~0FoedG zt_k(VgK=D7?2GFER_Esng}bHkY%Tn6C3c2Me;2*slr~228KuK<1+KOh3R!$uZk)#) z2Sf3qYJdiwC|I$jpw7!vkOC{P?AAK0Mm!=M9b&T#wqy_{W(;}tOVBu8S*iO1&&=K0 zc#ggwRYoDtDA7!wg?#nQ6k`I(-n6D>`L9p`{NL+me~RON1`ec?ls;4B$=;$3B{nq3 zEF*GA*xr+BpPD0IyiOCqkP=aG%Fu#RkjC;QOvw_Kq#QD#tZA`q(jKctE@lazttYu6 zW^++nB~Se0e-pMADB`Q*fJhQ~1)EoRC(;~e@Xosxg97HYXo&1Mp^8vz_V#z1wQyiU zcp6~yn@io47%tiKkr>gTh(`WGbM#AR(^|$J={f!NKOMAtg?mujsatP?uv8uUui89{ zwCNVyBt*QU$zi>#~DZx`8p?cwkhJMy?S&&rRa>^y;EIfUg@hmg%K3CoWnw$&0t+-J&~LdxK+^3*r-_TFAeEwsP!vh5 zS|Gg&CrVS}Op90RW}K^kjYCg%0)MsAXQE_{Qg$uanl-nmgbg(V8oUgeYu7;Gr9j~v z`&Q5K<0+g#pY0Srzz2}gqHyURJSz(4nJM&2lD%nt9)**|!E#Y}#V8z4zM^moLPho# zwxG|V29Q~Xhe$x6QTVV1sGcT;_XP^4Bhs=coRYY^rmU!h{^Gcj8x)0?O*3m!xV9c! z6iyLO;Y=UlCmw^pn-q?|Eec=aDbIIeKWZ)%u7g}H3J;QC$SB-y2rt^CaB69z@L^R0 zh0|I_SW$SzC|s?lptW7wcp?{_#ULS!E1ugG>{RPM45^;NOP;1nj8V`rkAPJqHrE7m z2sBMwT;HKU(>?T#b*9m@T^UVVacCNirsQdwIz`h|Cz|HvqUn-|nZU0i?K%lXFh5g~ zvRetOjp~S`27lJ}qlY;@?@5u@3bHWZNwu6)D65dTvos?yoKzC;k)3PF zLMeTsarj{rR$k5$)N0oMjCxt=;)zC7|5tMDFS0YC+)1*_=acLp)yyN=p$?KA_9VNc z3(4YZ9-&P943TV)Rz-|t>yT`ZGRtgIk|4*7WJin)n2U_H~YY@uvb=@Laig=QRTwxF%WAUR&ve3uvknA!~N|K7`O$3;*`C(5R$zm!a z)*d#JwVM*>5Q9*LJljZi#Dpv3v`Drhk{vLTRqH7zlBFs3CPnB)V{{y0Ltcw=QL8D_ zNwOU6c_if#k?hcXl0}BQ{-zjaB?Srw#YVDrHIHQb+ejAv5Xr(cBH4aVvN-1y$>Nh! zBuf#Itnt-yC~`%zvTEX&;T~doR^BB@7Za4e*q8(_ZGuJ#HaRmGN8;sDlWTZ(HVs_D z^#dtBLG@ljFSLnCXt`aCqB|G?w^j03TP2S+D^Z1359*1=1au5Fmi;pgC@b3Y9>OVa zTanXvv2ETmF6P(pRP(z2zjH>5nDB_Tb(#e))wIj&O^~97tRxYKYiNXl3C!-9nDLV# zSi?>IMb4@)Yhj3N4m`5rqJ`TzItIe-GM+96o+f{Pk_tSZR zXkzeAU-1b)Tp#nJ^`S%_L<*dwZqXofmc?jrLTwHoFxDiW4jP;<7Z;(yEp3%-Y*s>J zeLeVwYf=<>m<2ANL5Cmewj%3Vv<4PhAUd*HEYKjUMUYck69KMDMj1|Q4B!QUDprC* zb*pLjbS(@h5s~XD@MDB#CXMK21sQ$cbB2Ew<_tghaL(}IoZ-VcgGAJabB2}z_Tij? zxw$0nU*9>ys~^r8KAbb4Hhef|$mbD%iF1Y@l>VB`8N3+yYdU9m^}{(ssksv$Z{g!GZ4c?VFgrLOWLz$`j3~c_qk5YlF3- zCr4UOR#fvyUb7b4m(4J@WRY;1)sI29Og1lrG@R-DV6YG^`|n`!RNs*{H**yXnAKHn z*QTQ^1le*suS(hCQnEa>sh_%8_tmy6Q*?YT|Hd&ntA5{}_o0zBx^M(Q}I#xORaIQF4(dr%9Pvo7dHRXIm4ii{aT7(K*B=Nn=H?^ z2ZSpSa!EVQrv3D0Tl@!d+D8Bm05t&{17O?(#&f{B09fe(D|3JgfE6CFA^?_vcU!ja z7iN329agO04W*!!HA#9@yT|MU^gVv7XD7rOr^Ccf#PiMBDLxB#FU=XMc`!;;wluI4emTSgBu9O=XC}5j zeD=za?OCbpjAKYFo5*GyyA&p6mrf;PN2>>Fw!OCm!s``eK&-Shuo6cjR1^YdgO$yy zHWum)lw?HS+rdJ;9W2y4pM??~Tqx-@lLZons9T(5EQC`lu@DZ5l>|;|_TqbSQaE1` z7>W+!M5kLVhSH`3^sS*$`~PC(q5`&S7?NudSMUdDCDJj z2NbD0*#hceKtd6q1t4U_G zuKxf-c;uU%X!*ft?9N8NAnS^zJ!P8)5b zV2Y^Z$U+mTBi46oWVqlu4Hpg>Y?WZEJ7EV6HZGWs8_hEll4)V=RtUDT6SmJ_%LN{&c z^=7-T2J*h!o9SZ1w@lD`y;<&SfxPeY=&6vkzm7T zOesZ9C0(Wy-Ny3y$ggDp;{#|^ePogPysV#8;2 z8ch1I;WIjY(9m<1==6-IQ*8K*P9ODjiVdI9 z>6bj6z8mQDF;A!1@EM&x?&)+cmVM1+G-n=_&4h1-?!Rol1kHrc48xzW_!efuXQYZr zpV2BNd`7F7^aW!5Dt?YksD3qCNUh`vy^E9UIS*W%UccgjixaGwe08JPFZ(AKC)qg< zT%2Yfx5h3`voiVWCR@K`u*GRsCSToXRwiHFXjUd)-Dp-OU)^Z-A%iVWvkw|Hdm2+&A9`O_vBGsvl(#98iJX@ei8(Zx45*#+&5aV}xQbjv9E%{TPRMCzHRQN7Ws%Xc9y8Wyt zRkY)}ZtwP_igvt2xA%HdMLRYDexH}$uLb%21y8DI$73pNVI0iE#Y)|N(UU6L@q}(4 z@T7`%9BB2A1FcSXp;f8I-Dp*+aW`6(YTS)hr5bmmRjJ0^X!Sl|-Dve*VBKg{s&O}3 zm1^9LR;3zuqgAQK-DverVBKg{s&O}3m1^9LR;3zuqt#>T*;!AkXvjvZ4|-ZfLpEA{ z$kQqsveD`(PpfFirgEJ2w2Fpow0g$VDjKrU>Z6`k(U6T+zvO8Z4cTb*F;A;#$VRJ= zds;<9Hd>tvaupSMRINlMHo=OQ3!_$4Vw0?RxiE4?B{tEDmkXm;RAQ5@sK|j}zZ!et z`qiil#eS9Hr7Ovrdfbg>zoI9*5v{4m-Kh4cEjG0PHQDI*>7W)ZM!}{cccb8M>d9^t{EUFzDA-iwZX|3fayJ?_6}cM` zn~L0xiodP~bt7X_k-O2csmR?3*;M3ilx!+;H&Xt~P?3MO?7wyux$jJdhu1|lF1z*faVi^4w?cqpX_tc&4A_;eGb|SXg06DE}mNULyDrJ zTo|{=o zH4zrk*Ftxw1vnR!=^yYlQZI zx?!$E*OII`v=hg+yeH50yfxNYPN|8!i=2bnMLne^^Nu=Sw4*(xCiIRtr?n$Jr6%>_ zA42U8_mrMdukfs{(xIMGlY0l9&)UJBQWJa?poVXoJ*6i3;weX^13jfC`pO=H(ne3I z$-abZRcXDa)P!H+u&T7yQ)<#L0ajHS_mp}Ku{p+#dP=?gYd&zpp3*l$0RH#O!rg_A z*bpBANrg2Wp)eJt7wgY>yoX*gWkWwkH(k~kkk48bLrzmlyR0*yrgh4Oy4EZ0vetmO zE>SkbHm$VFdIMUUQ8u) z6;ZA9V*Pu5D+-;yN!idXLSE^``kddHLbjJH8!|>bEWKEN(Qj0t;ZGHXeYD-|G{g%1x zEq%S}?%@q1;LGQ-%lrD))ds=H^i6YFe-FD1*b(EVxvamRUEqYBECe|_m-Y9y3!u4+ z&l{J{W&M5bGE*2W@y5(t*5C6kQ;k^_Z(K5$_4mIEt|aDjdfi;MuCEtfkPXW%>R3CM zt?lcJR~rlSGo8(48J2JTO;OClbZQ>S7cxk>!L58>^D7=MFoY>z_Y2$sx2}D|uV?RY z%h)&l@^!OYwZ7$7t9QCZ>JPp8@}N`yJnPk$?{Z7;pLq4K4{N^{V#U z+&cQ4SGB*}Et}u*2c@?=0(#!7+TY^{<}dv*=^c(hUI^0!AQ|ExGHKwuVXyya?J?FQ zyM+5YyNy+}$+!sD(Og!4Yj>G;8I!n<<`(w0?lNsNR^mDu*D!DCE_*j+%W)me1@Oze z%Wk7=DXycr+x@2QvRf%zg6n9mP;BZhyM?lGTu0-jWwyKQU6hUCIvQKnOS{VsQZ|a~ zXe_vAy35{4*)Xo7lM$RqcNsa|OxYl=qgk=nb(g(^vH@I2V~e}CyG$DnI~MKZKicg$ zI2ElWY4>+G2qO)Au<%{Tq*`cRD9-#ngL z#K}$J%N|eP@9^{=Jf7a|@bndrr}sEK{l3T3dA|~%bvHrpUeEf`x|?8kpXc>x-A$0Y z-!pf#?iXuRCp|Yu>khJ-Z|N&7jZ$&Ex4{ECWH>Lt?dr`p(QQX>z6ovHd-F|P+uECN zg4&ke{4>Y}$(wugO*q@!n{Q&-Wxe?(kZtVEH&JXuZ@vj()4lm7ey#7#H^FPFH{Xd| zz4=bq>dlv!1@pM_aB>CSqPr;ILn>T|ZJbo$LVV+X6)wa$?o;7Hoa0^VOvT8|k2KI>W5n;pwS zMs~8Y_mclv&%(Ai7KV)MWaZGw%1?Nfw$-sTWMn5RhfY@h9na#nyQLTz*~!XbYO?Z^ zp5^UuEDssk$;#f#{^val+~rsxGP0AEz1MwY=m`z*3da(Wk)5pUz3?MLFH`Yux0E9z zJ6YL#F~%CG>-iM;IGS6G&W?Bcwb#N5ReBrGe|L`7omdPBmp zB2H8!=B_s+EGzCrMPlxHV-IgA`b0%y?s`K@hhk7vB<8L+uHX$tqNqsBU2p8-4aK9V zNX%Vt?BESWrKm{EU2kmX4aKIYNX%VtNLW^cii*VC^@fCH#i^)B%w2CtSZ+$>XiLmp zZ%9~{gj|uByWU`Lm`FnA9s_7#5_8uZMle6?!ZH+b2pQQ4&EM^7y}f3EU@J~o?!QHJ z!gBx3p%a$-ZwueetSnz3*vf5`EnOhk%B_?wSs>WTEtHKf5NzdLl#L-L8(MeG|3S(| z7YMfEgysI5Jtr*p-{v`Cd2Qbfo`mH(aaIg*yZRzwIYv%4l!g7D!;e_S>>LMpB_val>Snkg|oUq)VcSu-Xh<8X>UWj+R0mAZcPK1jUIA!x( zHd1(uz_om|JE?k%7~mH%OGhnR4kB;~F~H?GoObK7r(J5*2~3W=`DYN+@|W;ah`Z~+ z73IOctUjp_q$=j6gkRjV#^tWOb+0)t_rG{rk6F$!WL37$!{2eciYC4cRjzxJ)}-nk zoFo7*nKizyCxb*io#Zlo(I;G8C{`uMrpk|b_Lm|f?nlxZAuC-pgh+Q-E0-iA)mgbD zi3ND^CgP1kfYNrYxO6#1Gr*$yevSk4j{0`#ccs0j0Z~VXULb?Jz=>+DcJw)WvGV1! zmQmj4j0ES7wtZjsX$QvjbwK1g&*Ee=)6vgYx@)@+nWCeo4_HPCIw@4A4rj!#kBdP& z1ZeP?Lx9*fQ$>Ip2*4D31mG7pYVq}4K>&FS*4$dQM*vNDWv+2&pt-JQ4!QYeocLNz zhPV(vnc=I|eZ@veF^LVJ1uW`q;A)sOS;F>JujwYm9q3p%fUFG`G$#Gc=qW}A7Ia+e zH8=L=f<;uWTB{uu7Ib_mEanrqa4n&@&jT)u^#F~5i#XYccQcAuZ8z{6K-(mR6M~us)!>hu ze%npzn$SgV2{Mbk!`PFC1+S+8PzYp1p7gmt^7cAD}#I_TSOs>g%vX|Q-O@`6*A(_6gN zw0{F#dA}*9iGEHgDwI+MC0a!NyG#1h1@wp;1H(Y714te4=>Sa?DsC{&HOZ9Y1$a`3 z6AR-2K3VOC2eC>Al5rQoi(gCGD>7wc`4s2$b(=O`D5s4RVIozR6X1bh;ROU>n>ObR zsC>r*!Ua>ZcbG4E&><;&6*c-!n}QaU(`Mm5m?@jKA~2JX%{e?$Nn#yi=9<_b#D4a0 z=&TsyLz=w;U_+X{;r8sAD7nmp_rJ;c{&ZKMa+95qKb`DR=Yj}4p4hi50E6(?%M}Q6`TbKv^7)1}X_!=SdFXB;!p8jIi zy2b*c^U`h!l!RiyjmyBWYg!PI%aTCx8U|ImI~%o^vO%2RTMoJ-#c9B02hLc`{meub z6J$);8&e$A17!-E^n|3vyeSfi3)3a1_smsVi*2l$)hw1fG~QGUH$0*7u0NQa%4ud3DxOb(GM|Hf^;xQ@m?%z zt{D{zrsF=?N1QC$Hz`Xb!cho`Y(z>{OXJJ1rIQ9W$mX>?xNfvA1Un{WF_0?2*)Re` zS|VEWRsxK`9~tO4Vb<6JY!#rvS0V%fpS={d_8vJp8WdB{5OHe;WMLk1suIJZ4W#nS zPqXrQM%!g@mooGPt-7iSePJNw&003$L298daK_?5Dyl8w2#B+^LCPyGjsVg=#MBx3 z0yVZmKQt!K+uUqf&gy5d~)TW}Zi z)L#Q`3Yo#liTEP%1%{Krg}y-bDVpaKDLIFT+(Hs3kk6uI$&E*OT(<9MHb5kRd>)VW z_?2NM8R`({39;BM1j9N^UOb{m0$^Obw-%@dU*u{L(z2*EgEp{+oGn#hfcjKH2jIkn zAQ+RJG^B7c+<_%L#^E_b>*g^I3?TAjIuUS4$Qb}a2_A)Tit+=Tp1n66pf6ZzJ&xXA z#9Wlr-kUa5(x7CuLrO=UU3iOmo$wa<=5dY&1|Cl@20dduhMkB`z!-uX*bprT-9Tr> z)$n|PpEuA^e3Zq$fPMzT9>a7aja`J8!HG=(9EIyH5C2%;JkWy=*}DLAly27M9#1a1 zXU~e0pF$91FG@^^-AN57G{mHsL&Z+(sGEM#e1r8wEYpaHaO#ers52RQJX!53Z}n+3iHCtqb6Ae}(7G-;dNnIt*uelS zj)*Y;^Sq`II4}S_d5HnwI<3tB#t@X4I3L}qh<(GN>|)=TuQf@U;;DFMR_q&N0Nt@~ z8Wje>D~?^?Iu zVw&Eb;9Wlie}^r-0sLKDO1!}W)qsKsA?%IS$A-nSkx&Br7x}>3h3v3?$N2|^f;cSG z8pJZ$gM!4m(el$0e<)>(RuOcGHHKA)KTB^kVa#5`ak}v-zih;cRa34WDVtbk#h`#R&O zF^-xdn`JBWAfKUwE+Pg4wAT?!^yYc)%wkew+A>nJDp8$j}9n%d&II!-u#ux+K>3ulG`S?1C3h# zi25cx5BlUDi%&L^1nK9*$1|RJVsQ^{t zq2lqG_T!Y#4DwU#I05gJQuHlZ+4^^JiH^K>EYe`n0;o*>W5~lw&lqJ7w*HRLf&6?_s#0misdXkgGb6+a zv|X*`=Vl5ewrz=P`I9s49Uq#9T7I^YVLf`R%m6iv8$4#Lg@zuP5fDSz3Zq9w_4i9M zsD!ym1iu@pZ61<;SVx#PvNn9rd>vHPx8gFj{9%65dwTgnmqXn*w!05#gZv~>KWllr zH%y}CR6G2zM>os0Lz>0Fw>(;nr77(5=k` zXZDM6g!zEF^C_^vvyxg4Uwgzrqk+lK@2G_A-Z`~;!Leda4fEB=k734amTcA@nHdSt zlNr9BquVeP7*;L+3eVs>V*(p0`DZK7kF}rm*_r)}E7}o!nN*z@ZAcmuZCph9>r7`l z8SjR%iDVX{4}CF%-xX2E3C+NnnJmiXg%`@PwIGkQWH}NE^MDntJylj>G{8g_G;mRc z2BIGiM5BTRLhH;zRKQkNRPeGuBs70%epn_mTOz9sB#nn`SQx%MY%j1$(g}dim zXJE1#uhcywp3---pW$mJ0qRB$S%QY;^42+9f}Y+FH&`S+o6ZB+`mD*rWJHfQOlGT< zflqRW#Lw*KV=Wm=RQ*%?apyU9r?eW1WIE-5{FVJ|K0^s4Q5t9m28w^V_|9i7fdn`< zDZgc{B+7632_!N_6On=$W64sq@L)PcUVI3`3s^OmBZ(}6q$^BM=a2I~1jFjAHiZsIdDRKHz(TsSk8tbH(B+xoK+jdNGJ zAz7=3?b+IgGS}mO>G~_+JiTA4p@7~XNU(w3XKwpPMi>bbgFe^BAh}6ys<{AM^d318 z`$XRtZu9t#rf=iPwDsu~Xnrb#tS489PY)z-Q_@Iw8P`sB!iCj)#8KaS-|;PUocSXR zVWs0jNAJvi3URT^V*3OlpuDP~L8!ue@_L`_()obKGP%W@qoH>7LaT z6QiSiJ?-MjO~>0L(FZmwQMKbX9RqGU8k!EF6;ow>G+E1V%FoJFF)K->du$I_qjUtY zni&U?LK8=anTR|{sZSv?pAk*1-42-vZ5=3)u|-oqza#jn0rrXgDN~+o*zPEhDf!W%a~Ch1n^XQG&&R;6?7_ZFBRaDQYRE-HRl1^t2()^bxN*te&K zz~#S?42hgXTsFIpYL+1wldz_?M~~?@^ZGdHhUkV=_OIby2z-O_UqB^8*fcgkr_-Q~ zZ5R%sP2>Fo>7}kTo(r-mEy|K=+v$HpHY)&!p=$vEQ44k_SXp8=_}tFpwwx z)(m#^9Lh+G!9h=QDI9zzxg=RnYnzkHlG$WqvMG5}>){cTZ5`NE*{`8^DPoRK>8x&jcXjQ$*4aYo`;t%tRjNbwNxMU9&r`$e8wk|lz;-E}o=(=aPN7k6*ioGR zjI{2@;@g6tvgPtHgxU4L4>l_Jy~gfJf7S`VS-}r*sS*#rnTN8CZuA>F;b-8iPDi`0 zS;5a&vp%7Q8CS#1Yp&sS4n&AxXeaoAdce&q+8mrU5m#r=0 z>#B=q4W_kheKqrm874qHMB*U-U0yANPK)I0WJs|Y|C@T|Bz}_9$2gxy**%GeI%?UNF1BH^fjXvlTD*UDo(keqxyABu-j4AyljHI+oJ8wAgz0t0{S@N~vamQDTMb z#;r+Yl_nMoBg%g;KoB>^iGHe?W1iFc_=vs6L99|j*jpq1H~5w;8LQM5`Ki z)-hz~2<@-XL~66K3ug zGX@04akGkNv;4604#n#lxNeeJmLLRPu=rrcaw8f7drJ24WUq7X!FWsdHNkPYs?*^) z=Z!iAX-KrMtVz7Wv7cUP`UvSig&cUgV4779ysnxF2kv+Y4%{xUmR-`$FFEjf4j|U1 z>k@4F949t$DI);9B++as%pwqJyML$@LN8TnhvifXUZ!*!1*yhCYp01enX<4xZOfE} z{b^gKl*yQFO11AZTh@w9DOprHZPtYUdGRlO{y_wGtquse1lzG6Wbb$|3B8ypmduT7 zFId8HZPp6`6CskVz6qMsQpOz2BF|mfpa}i>BN<5CHLA#fko~kWN|7vOH4=Blf{WvP zYzRqqWi0?^Ih*zSsw_6{3JQDfO6s^PR`8(=kI2wb0Niz%o|}vb*Gi$dD^GS>;sX8L@W6UAZ4nsX^S8N|M2wSXk6|aaSG1i@SEen)3N_m6D1X zio4pln~uA}?dYoKu5kx<)oUphJftqEEo^+G*p7|6w!cD;1cHLf zGW!9{NlV-nS4G0wSoyuL}H)x9`u42@*E1!{LwMkRIpM`Zs{dO z#!BtUSgE}ZD+NGnll2cl`O|DVXee^~B3ck*rE)RWNGG`0k{Dx++-AvM8F8<$LDtR~ zO|1I|doKmnvPRo!MQ*1+(m_QGuu5eRo{*;ebS;wh)ah))gW08G?eu{C9vK@S%4Q|! zM{N44M2R5PRb9KTbqUq5{Z}>X6KbGJ)nK2LWSyWosgS*PLuGGX=RlCQ)#0_`QM70~ ztrAcZeE`)7oc9ypz?dr+GJH^iyr5^~m~adiU)jVk;L$Sye0wX77G zAMG?~;x?tPK6hP$Wpt>h-Qp(pjXl0}cQQcZfbd zLeh3;$4G)=@n`CN4_la+#Qf)r*7aEQ_TIm>W% z3d+zazf2I!T!Hg`j5$WK2t}cuq#7ov!OKZdMIu7Rv8R_39m3gw_U0DV;#v?I+k~59 zuL>{9U_W|>H{~&c@-UN_u1!d4EZ(wXRBltb>k^V0)J$~Oo8i``>c9eHLv%Qs0=hih zl3j8dWaR1gZ07WXo+9j>JF-jNI|s9M{+-*h^?Jv4{`qJA}3R+{SOo3`Hhwi{$ z{1j<|&l;Cao%Q@EOQTIDVSu$v7Cju-w5?4@+++^7r+A&4;`N_)bcu~7n>CDTTk%@zZLW1!Ykp=z~$ zbWX9pT<^ZDegxxHzI{|qJsP|AJC{{Z;#)!q&fF|$s6ZxjCu#JdqX-hoB8Xh-q=F8y zr1M8WvdwVhS{zwbsnglu9CTJhH$bk=is&l$q^$~+85Xuf^(kz;MCn;eLWtQ8)$ zhqMx_=)-kAhI)LtCW9gDI}171UXU44A?hkaA3lenn9`G2JLez7zUHGmIGo;!Jrm{; z5`&``WeIMkLP22@L=omfUFJZbRVgofhXe2IL`1`fW2JSqbr-6Jh+fl33RATJDrLNgFT zqICvB)^sNP_eb~GR#yy!Mx-f$70N(}w#-1N-Zl`jubqJqeeE(3vc4s)*6&3nU*)B$ zfe=5dp-wcFEs|G^+tQZKP4X+po)$?I^MPXT`!2vvLI41_v6ECZp=G08xvI_!u+l?FmoUl<5^z-oOqfV%bB0TaY2420?( z2150!fl$5IKnS^9XA}GZw>zelt7@#s*a-P)?J%HtgC|YEL~F~$wQYm|2}d8$Jv-id z#YPC=Rr1?5LIA#oi0HbN`o-7qlDS2Vo7B%&|a927`;83jjNIT$uX`-zIMwntLXHs3MyQJrI&6gMc4Sqx5u$Fh5qjeZVP%^TRti3a zjnHc%1ZKLh5n3!8p?Y4i5h@4)nNdKIjnIe)!T7r!^Oc1CMooxKfk$yQRp3+wqa=sR zMySp$^EO2am!_i|vKn%_p7x~;eGaZ`Wj}&ub0pazBO$YGJ+nk9T9AZ-vR3CXfXcw86>S)WpFhj=3)e#=N`2}VN8H4Kb| z!s}GpWh6w7GZHd60wW<;ac0&G*5mUS39Voy3sZ@wERfDf2*rmqdbJ~3(M=(53_L^C z%A}YXB~(1^RC-3bSOiscdbT8RX|RxVjK#@FXpF2N*Ea_gyWFgfNrwBY{{c zoJ&bLnl6llz&;rX876o;hM>nt2pWoILf~GMnGma+>cC8>LhN@N z-eyA=+jVE&|KN&Eb;jI9cbhQ`quiLg%!CHrj8(0pWG2KgH$>P@(O2||-b@HxQW`)* z%nvt*$_dfb6lOxjgfKOlwVrt~GoeLpLdKk#kXdwDA2DU>oEPkr+`Lq+n9#UNGKEml zk!XG*J1!bzDssr2Dx8^+3DuYhvFw_eP`$%Uh~b&7O5K|YA;ed0QI=?eMr+#B>Jf!7 z#m-C!&?NA&IY+O86EnnS{zsi5TWH0I*BTEf4Y~XbRV}5;8JYq>Zhn9SlUA9Z=#8Hr z44FFTM^;KkH&q)Z!;HT3qwT%;f2pSF`qeWh3(n*Uvmff3GqWFBQrHjKv|~TSWSRX? zJ#qF!HsRP0(bxg(huD#J*bgnL*bl7*pV(SVN4Icv!235B_Ct<_(${1^6pCcZ%?vyP z+o1s792K@hu0_|B*$!dF+VhQl5*F4&;SIWo%)juuIq1Njw1-^io6Ka}BmetK(U9AmpoQ|* zLs+pn>mjyihzX2*Ox8mVk08nN*|~J=RB4=5ZirT(x*%zu#^i{!CGCkB zpjau+A(dP6r6VdelN7ZWSG7i$=x2vfOP{9J`=-|QZOTe%%1W&%)K?r2k#YW}SW8Q+ z0$x=EDeFYw#>?@v#=F*cV!G^vY00c~!j;kqE45CTmMowEwL(=(#?R__MbUj)^Tt5y z&^r5e&E5)aHgGO`U|Me9g8~dzYCSNkBs{lu_FzUot3Bw~JSxtIIDK(5%;5+|US*>& z0ki}9IpJW=u&uNW`+$OuVIv*BmPU*ANDZUj7ziWi4uE%;Br;Z7n@qR9iV)>sF2zq% ze@xGc>>)j?Y_21$?Ga8EMWN2<{t44tv$i?l(C>3a>)D&cvA4Bn zHrwRpw3c07pj7r2y|dl1+D*yjs(`_4Ii0-4kL0bqV7<$>mfG`m$rcVylDD$BxWZbt zr7ZNdJTa4P^0hoULxa3ntzeTXxZG9H{&suiooz{(?`=HrVTLX}kd+;)sn&jbz{+lc zwVxaFU2DI@Vzcu2Nv-`7Y{y=0wYObI$7*kDJrV?arPs9DYg5&WFh2{+Jv)&e!k2>k zJcO?!7J0>K%iEp52B5!n9J^sp#(`kE_5!2$*_e1xrTazeM~Po`Bkk zjhN0jv`N5rpY)lTY+CrsiM2DiS$|sgFqp+~gej94-nqGA+JgSB5Q>nR0uCMRcs-PS zw}OvISjkNKGYQU2O{{dnm9mCesYwTBd?LeN?&h~;~U--W7y`5d=N`*pV8io`aZZV&uPA-ht>x(1`5gO zn1M^W`_QigvKV7GALPf$g>}4MG2oMy(H3Z13M_(O%J%35P#2 zjBVNrlcQp5WKc&p(r0VLEj26x;fSU2ky|~v9ES1H--V^|5wGZ6`uvBRDA`NnBQ&Dz zRaSGqu%8nYJwaii;JlkWhgPKO-K4ZosI@H=YTiPj$EC@-i{SOT041?CBbCzHZFfg+eeKl22;aCtz*GEd;W!aSj7x~9J73GRwJu1io- z$=H*Re}?>*RLm1FRI8dN@YBo_%-g2{!TB$dM_~-vaD(8?6Mprl#(xPn{k(a?KX?Bn zL%ITqc|z~Ba=_`#6a2JxoZY^`PG@MT$24pM(n&2;^tZg~_NRgbOZAWIz8C zaAER$tg>eCBBwZoc|y(16AFq%@dI*_%LaEV5vA?Vg7o}6Lil71LdE3A0`+loX>|zw{Zf~)izG( z>%(L*nhraq#BSsb@?nB;g2yR8E6zB9o^%-}pi24K^I?K|!g+m|$TinLvJVs4Ah1R1utDhS!{nCKe3+Cr2tTbp zOmMLFlfCbM!G{S3BDi|cy}0r&HcC^w8l$_{x=wh(hlxD6{51M7k)olW-+({9;KM}z zO@1ssOqTwnd75P13qDLP_%Qh==ELMXyy$`tlbcixg3qDM^#d5)iNrIu)1s^7;4PL(wld7S|1s^8m9#1Be7krp-@G2m5cu(o{ z4&{A5$V18x;lo6dj`Lyit8bVO6PY*24M})4wp+4ikjL3WsfCcK=>iCc>lW55Y+o=j z+i#p&VGNEZk|+*BUAarf<*-7x7qOqRXKuxqn~Fg>uUMCm$md9%J45nBf|*9ZNk55P zT9h{@aI`98X)}Q-E+|yohnO*M)wd*e_@#9$28PA6T!s*t1aA<|DTtgx$s?3mv16?x zD|Rdwt=ICR;ClL+`n0ywo|wLO+F}_DXcr3&^}wKE4ISmylEG(C@WE`(w`%T3N}ntQ z#co4w%0UhLv^N%NczG!xRo$(kXl>_Lp^3K*AS7slFa|Wcp;k;}%}r#WiS%yLoTIG* zNjFPl;mo}&< zv}DLW8Fy(*^xPy+De-Ft^Ld=)Os04flrzejAu<C&p)r6~+mwlb;7UD~iJ*~mw_zPk~zOy1T?cWDFp)1NP#QPz@`d*$0Dr8A;E zwZlud~vnS8!jrKB3A+@-m3W2}rd5OLV(s&|*h z6-HhuoKfnvl+c8vHNKU*v?ZmxG|P7G(x@D7V5%k=l&SQvxl3z*#Tk*gOKUoJX+%MD zL9J>MHPkD?rK!t~fbn|Je-JNa&zC?kk8bwq?i6xN&(= zUZ#k6!{mhdw+b&)Ow;^S>&p`iLa{H}X4W|CZvXo5viPDC@jc<)Sbd%1+GsQt6c@J~ zk_%BewVoyd;`bE_5~rjp$T{YMsVbb5k&@EKliq4897rnDaYrFTv7Tp7n ztLjGVx>1Y6s6o`cXMXwVApcUBKYstyLF=Q7fdrN92B&K?@%A*y=THCDw+ZaJCrsDC zboWFgbf0%_!$msp986RFI^e$8mnM3%7zVw@Hm==iVgzrEo?|dcvMqjA>Co2b8%on{ z@iR)-Y>Urf)XUUIxc2O*3$%1=gz2pjW^%?hB2Z0c2h8`jB#77|5MnKdA5>FdKen@>3Y zT;9O5q5lAcFD#qsMofe_?sm?^=C35mpR^3Fj29VaEd#5;BI7a3;KnMm%;9OI@$lq_@kVBcuUN3{PUvogd%HXK4l;Je-Pe*s+E6ug%g z{%deZvR>=hQI^n3r732vMC)7*KIL45b}g@(qwri&>M{fAVLaY7kEdfJOI0Tn*`C~`C0{buNVg7yQjt(9 zbUdkmdd_|S7h;K!%ab9)9KCZynSd~wiSOJNeJ_>_xPo9?4zuhFI8&6f{eFQ@LE0G} z@XZgu5g)Yz57`q`DE74g#e&NLG?+y&U>2^?90n^rZCxCrV(T=m@1Cu#Q9 z_*tE|uQuS|`njuGv6jwBMgrlAioMRhCCaF8vrdttT~p0ri4*J9W~d+Y*g{MYi=3OtkZwgr)|3I%AOnRhraPXS~rd zi}x4h2p1in)LKttl^_Yrw>$t9#peqJ*Q&IPTrin^UY|GywO zd~8Wju6(n>17UVizOucXeHh<-6YSRDbUOm|_aqrhs%&v$QxNP7AK-pdvcx##Jde{6 z6N^XBX-%vJc<9I?&-$XX2p>o1p3ZsE81Sa>(U1)|4E2*? zdNE=>EdGJoPI|SN4$?LHT68mExj!WgxM)Z4l1(sdM=4V1x0Mo7R^*XD9^X`I>lA5I z3>sGq&0|NmM&F`jHh3J*>XZInVUKdMbbFr`7t!I|4i` zAj<+E_*w;_{IaP$Ng-BJ=1F2A*Z<~wre1v!9)shW2H11!qyhTLklkm%{nY%a`~7s5 zl4xj@7$N$a2ptUne-qK*iA5ifG#r9xozif*j)Jq#g>afgTgFff9F%`91b?1~BH5@M z${WIlhC+S?M4WB?c`2dD|NQ2Rklb2o2IBze{p16bSK5X{sn!1H@Q5` zpJN@qom@frOG^l9lV8qM(UZ4XoVolhiYwp-9cj?$a==eNQV5Mm5sVb3BXtApOnAX_ zEkhqTq0InBV%iJHXpF4@_G}XKr36E&k`fG(=WYz!2kPY(Otx%0u0GMVY^UR4(vlBP z0t?EAX9xpX_K<@6hRH-S&eJ}gmLx5nuHtENGRBkfWbivuZ{fV1z0J4LTZM*b>;XLU z=a4Z7ceDpe2fwsiCTv1kLPQ2-9RNI=30#`v+?J8EdByH=Kk+{bU4k$kkrPxTo`V!M z18nC+Tq7*x#alFqPdEKJki~Q8H;jPf01g&HO|{m?ktqZ^3u_N3d4v5Hz)nZmzUYxo z>QcY{Oxyx8O1GpxEBp_(>A>0lV9O1(`9TE;WORKFp54qR4ks}>oP2sN!@YoHu!*@$ z%1V7l4(bE`_#fy7m>vIvIQ7hse$YEA6ZSR*WQ|~gh>3hX!i36e0TZgazyOi}3?OMS z=Zt?lKGH}wI~KC}P!T443#`ag8Unid)jZ^$8lFcnH-hevl3n`R1-Wm)Nfi}`+}+6Z zs)C3g+tE1;2#pl2WAS!|HIOXXPQ4A@5&mAK4-UA#>{D%nt-nU{Bl8B@BZ{WzNv}Hw zd4R!=O2O0ufydeQE@uC_ggxq5I-#AMhe0}kRDjqZM2Qt+U;vv{{v;NZb16@&?P(cL zE9^K zWQ4n>E6C!D?dNJiAxc&zOYGw8$MqPT~n>eK3FYkuL@L zrH&Qmr}g9M&+zhh#`Lnd+(F|T;vffY>rQn~wyV?hScm&e5A+REnl=uit>CrmzlXgA za;f6AU=RD?(mzh+4Z~{jA6PJ7y|=CK{Nz6 zgRx^w9hgoQ9-S7VvVqQ5& zD&-W^OXR>e${Z9;o-d_p+&St5va@46Ix(uPKf8{Q)R)q&Vpyk zg0HE-i~V7K))tfo>N>aEP1FneD;*KwS8Px+=x&)|G8erqGfd{9w=RPI zGqM87XSIDKeK1Th<=zx-f%TF+A^jdHn~=+uZiO3@{Ej4f(vODe7Ny%!G?E}eqodyx zJ{+cRQMv=ZP4fAWC4=;1VY*f6E?7AUy2Bx*Y)7{#y#m%t@_QkAAJX3l)9p%kqn{!< z7qae?o(a>pD&3Q=R`NxbY^9$F(;Z5$N-t9Kbcm{xl#R|#rF+@ukbF6Wd6Ba3!2#!{ za36bTl4rs+QHm}Mmyde7j@^sWPlwqRa~X$~o5HV#hsivY&wn4aU-S5(o#A!7yCy*< z4wJXdW#iq&`zXGc9T3I4=CU>2#d|5fh)}r{@0`mn?k>KH;?*d$DBdxbUDRE?hvG>T z^c25!E?eDQjAWRs;=GCC?Q_{=ckvYzuVitic-vgIs=FB2u#yd`caY@W-ObQfPnv3LT-o942`-NhR<&o?-#`kJSz8yyvW-IL7$M<(C! zbny;H2jBFFf3rjMZ+Xmqr^Dnw^r(B#q3W|9H{a!O@lQPR-IAti{@Y=iDZMr2=z3H5 zr(rs!^tN=JlIOw{rv{tCcc+&q`A(QlE4@9PQSyA4ZczH3^im~%8KyR(J5ro&Yzkip ziSTIH&0cd;_+3Q)ZU9h_%pz}G)?IK11($;C8@mhML%|GazM;F|b_y;5!Kb?m-c7+e zP`M@Z2;z0rmH+rk_yBTbY%fh4hR{w!;{D*3jQj$u=geXZd_Y zC0pScJuBzORPq+)vWGD}tP(|#?BPrwRSB;BdRfzHmAn~F)5Dt{RLPsbfF9<=aX^r4 zh6(j>rw^-SQ-X4*n=T)q1bY~XK~gu3OW(p%KqO8!h&-?h)DH>0t9cq94`gQ0LZSQ8 zX}<}aACIyVq4cG#xhzJ=Ww)h6@#nwzk^iI7`dv2u>C$L7|4ul6@OqZE4xct`dGZL^ zkY7ZzQqs&gG?pqExx1E)T|YX2$_8Cm(!ekwUve!g{5?nV#d|GGIq$HyIAmSUo;6XjdGPEKTfouB>Ful+-< zTBmip>^*LbW6pRqs$uL^oXQEMqO~*{ts<3)1%rUxtS!zoY6c#TGV&|-GKed$wb9i! z23O_*ar9J4s8bZ3hlUs*YziW5F*t{mI=Fc(I-&aWi`dGo;;Ejm=3sx7UR#}UI!m}* z!DSfve&8t5d_7;KRwl1K3OsmemFhaCngE{bF~!lf*dIw>*0Ab%PZ4%`k3YwcqJKbC z<{fJ>%Ku2lSFZ6t42ss@S@|Fxw?6)H`@Lt&_YPLx8zzEeb2Ox?R5L&1$M&W6cTbn! zT~+P)5aarCpn-iQF!<3Pm*)4*sn0xIGp7o9=^181pT`op9lD{~6Jkf#LeD}sP#}rsvYL>9*pNbE z>^2AGa9}taj2u0B6x>WXz4uu7&3pg&yS3I=9jjx77si21C?*_T z*7CI;r-XJn5v?6sWuPHV^G6(lGscA5@gOX?O~j2bfBA*q2D@FoX1`X0Z>fL=zo-U9 zQ;Qsu}zxx;MA`T2xwAPA>CD1p)jJxJW zfUN+QrPuPFctT)9U6iDAoC{v=r)rU=>diJ)MnNfOx0eqiS7lc#=0&ph>SW~Vqeo2< z!D(K&mwJHRDi$CyEy0mN4qG%9p2a1gHYRLGASJ_^9YIdAWJkcFuk%-gwFHwQ=?IqX z45bUe9!YzCPJp$yU%oTc9)5ghsJ;4%o#AGZWM`7&$=D9i9c{;sV1(pi z*>j>nkZmSRQpXB0XQxxa@c&w<&Cr*H5;!+{wON#=`uQ2JFT2c%x`@DZ-1KEMCiPie z2@s;7?0FvrXNF^ zHnkc0GE=_oa6&Y~HE=b3*)pZOpnei$ZBv_}FEi!a6;8xP^kylP=kV`AEBv$5{teH34mT&#)~QkzXCoH?7?tVfc+imz8Dt5l5I ztVd`UMz3&MHPmK3^0`oaxznnlHtP}6ub_CD)2g91>ygw&5=)&{4YgU1CPAb!;k0U~ z&3a^Zkr1*V$^0m(z}tI)2g91>k;8aiUbwZ*Qm{Uc2gozNeUFB zHtX4#i*!R4c#2^?arcZt(wjo`Wj6CSII1f2Wj8vqDfDFr99&tF)T8r<5PHXY*v_oHZ zyVF?|`m*<=6@3{-pDAi})%{%?AXj9%0U)*6W$aQnbr;-0!A6jMw!1)Tvkjp6rQHR$ zQ!tGZY^J+FYP0pA_9fi~w^1+!Qm^YSxRrtobY9zCa0>+&gUEf=W~)Hqo?Y-kz8GOk z-Gi0yq+p1Ba}Pe=Oo0sRdob}G6mZ7ZyQ4oqfiBAQVBd`tfPG01-rb-{L0@Lma@gTr zp)Y%%!@ELXcBjL;LSH5WX}&G=Wyc)e75cK{4)4&H8Q$IH@D6>M;obXF!@ELXcDKX3 zLSJ@|!@GH}E}QRCm)+Z?E<=5Gsd`eV%TS*g?m2bYrV#a+M?Ms0y!WB77yHOt0R2v? zq%Z!N+N>`En%b-{2AbNeFAC19oxV7D4+VX3@NNqF;^6x!=!=7QQP39$k5kYW2ai$E z7YC10&=&{qq@XVjzK?>wICz+XR5SAcYJ+YF?Je~k7CujPHzAC!I4t9c)@qcE>b>w_nsrZ*_lMw6Z;_c4|1 z0=f$+(}sIhW?q@rM=|D=X@kA$uQO_AJ9E;rvU+9OK(G4CE7Kai>MyTMtM{tEyfUrU z%dfmLt&jT4E7SU@zq~SSJW0Ff*DKTFUY;donp9>;K&GN%L#HP;I)%z?3{YU)=T&B7 zygKg?+&XS!nq53!BYS~C8>yQW)SjGYL z=v+2}B@!D8ef_{VwkZP@KA;(1yuv{T-RtbEEm{QP-VCy5aF3smvlto z5v)t0YIJ<4*K~J)*NB>0Q&&+n>n@im`CQ%HoT=1}_USxz`-&r1F`5%q{CzVMdP6FX zUBz)x@f+4-f)WZ8DE!ppSKhcD*ImU#>U9;Lr^k2>514Q%69r?1BC(Hb{-k%sDi3vl zU@SwOAivCZUHN^V`wI-Y*m&hb%(RL;3Q$2N15>`@_Nk1ECs7~8Xe_by3-Xsf`xim$ z!$X0pYUxYt-%FfAI|0V!&;B0om^l#!e0B$l%X>s>BQD$!y-Z^MV5fa3$GNy6Yi$no zo3J_mB@jD`cT4Cvw1uH8-4mdT01rX=Ibn0{9=QI1G}+wdT%umW>#diM*AuQoEWHOy zv57rb{>t4LEJk{Ll(0K%#;IBwB^S z_v8KWfb&CvML!R*P-Be0tTFO4mL40pwb9|*6?iO&13yL}IG*!rK!BYH2(Xh*Ac+0G zIuPQ+288=P5McIQK)__FM2epb5X5a>4G6G40YPUS6?%B(HDZFz`dt1i9tgD_OnCk$ z3#vh1ps#JS z#15YDiisiT1%fo?L5^B4a8KO*knrt97wIWRh+3N~i)*~@ksmj)8a==zw8f_Fzx`e57fAjngB}|>=yH^vYp81vk`+_hvYf|V1VM?<2urw$aged{xf-nUT zE(lWq;es&LDNL2C#=lt}FGDjmD042(i!kRx0d>OtFF9>Qe`K4XG~irPN3dv^*d-6O z{;V!HOfAfLh9%SDqGLt2!Y%^d`AeKla^gLKIx5~2E=IA>o8!omrUE{d8S!++lO5he+3 z6RF{JDf=BA&&`J#jX~>20+ci>GCmS42V`e9;WUIa5goyDp}UF3>zs?Efx5!xV?^rB zM!sbKXsG$m2NH6EY z5{>9)XA|=V-oi{qUJYbB7Rn~VdYn)`F8juDSwzURw{a+2hFOG20qQbOVQx6sKTR-D zTw1j_*OCJ;_WtsAg@zODjpok!izu0kAC5^DMq;UJW)G8L<3p{ z#O}}6fP8=DXMgpDp!NGpnK=Sz20z?rv_PG=G0^S@YmR*G?LjdA*3(Jt3;D<|e)hq1 z^>Fk7TAgnkfvKqM%5;?_^vnNLarDD=>Qy12P7SzD?XXS_sZ+R~TBTNNT3uHC5RiXO z!02grI8A7ac30S{#sdia!(i5{4jatE4Hpfx)Y6Y*fV7Z`)zQYeuo%t{i(9Wcp8mLn z?=hN33i&aO5xrdnqeG9;nQIl>*pRh9{GZI-PZtxy-1&7$i%HFxkT?e37_!hfGE&;{ zRBl+XiXTG22#5FJ2mrAFVP=>mI7m{t#K8VcE-`{6lS>Tj6yy>kND}$Oz&Jt9FtB!% zCkzbPwnw{3xc+a}U-Hn%(T6-VVgk998{1fnId7dvFw^|PGI5Gz$<`<#S?=r`7d!h# z1b`7T5iHdgGhUQ6+sJ>oZ(H0VN7i3QSS$D|mKU1P$%-ix)XlPSd?Jb0BACY#%+JPi zS(8&^#GW0&8j^eE#ZaAS<|JEO0q!w$bNnzu4l#A_*v3g3OzAdcZj#$M59a1Qix^;R zeM{D9S{Z0~^&w2~nKp88D+t>O0{f%ocKi4W2t?9g($wNR#H8{tp~~dz@NNPC8|BaYi^O+7CkPW9&7E7@uG4V3(Te+_bn!U zfdza?9;ah&oJb+uL1KBg;m>1DE{9&PsLfz!@IihH2+!!Zhs!xVKfofSG1D(M-iK{R zp@6A=W#%--?ych#$#q;u=6n!q2;6gj@rwdEFl&9cf$%#U%iJ=Z5VHZ6QA;U{_zCeWQXigIAMiaD^VOh}c1P4GMbBZEf(U~Qnuo2wU!t&< z90qC@WzI7wPoBKDA$nG}PP~sL;e=AG0;$YT-;mP|^(|gD|1)pQ`XW~=l zjdW|Yk7SeR)_lh@39=~OMFLUEEM?5dB3Zg4xSbx&f+kLk zpfh)8)?=RL!_2lgqkwiW8O3k|2uh$HwILyAbK4*pHD6;!N*I-klz=vy^;U!D!oNd)_7ogRHY*|6)yf;E`mh~a=MUKSjKF|QTB4j-_ zGk)BSu{r(Z=QA)~8He!r?JyhCr1SV(h-CSdF^O^pV42kOy?B%nrNB@ygxSQi0Z}7R z;B#EwjkxNu*BL3`z_{TfupSB18Zy6Z$U+tK17%}2#BmUdG2OvU&yHYDND68?2e@?U zUU{3aqBUBL%Yjgn4WvK=1~O=qFeHag@XK*hwr94`V4TDDGrC9L+-*cMk_-th@Qzw~ zDfJ83RYsFsx($yO6flG*m5L|o1vp7kZ+*(#sfomx{mE-4(-C&{FrJY~T2`eD15zZ) zHP6G=!Z(a<8)!b9X=5}zyY*-%*JSFD3R$FduMTg zF}=VO9Dsbo4weg?iD|9S%CI$hNo0>GfEPuqLCk5Qv7hJldxiPbGvuQfCYF zGy07{aZV}R`YE42sT3y8lJT@2Q5@ioPv%^^=j33PM|RO?Ep@A=78m_&=4*I-KsuFX z{#OCd_qpdSJkGnvgvVFh%PjNeuTO8^rxogj2)J;^g@1$BPogz36^G4C)Sek8GuIl* zT|J{s^&O9X)+<{=$rj!xs~p~|{9Fhsw@jW$XL%e&nmNP7;&YnXB#v+)33xMenn&`^ zvTSQV>l8R>N!%4D)A3|Ob5Q2{pf;TFqSKfZ8A=<1W@S3MC0(aPw&f%@r|Xs6po>D& z(LOFekX)5sqGWeEtz;KwH6+_H1}53UO*N9u(%;DA`%)#-pf1T2T$LnAXO!UnDo+lj z37Qptt-x}JRZiy(2r|yK^Y%k&iY5l@PXIeH8w4OzB(#-3tQjq0{Ms4(90{U}JUV;H zrIo;vu_hEqhRB8=A+QZffQe8k%0)ew&I$nrXNo4k+=Zbxml=2kA2_INrjtu}h8i!D zMr!l;GFqPv4wz1f4hhn2`mc%pmE}}1JfV6i=pTpT;uFj_t23mXUMe4tO)tvGnh=vM z^Qk>2Vb;ql%bh%fd&?HeAYY8O$QPqaapDXPaZQ}z`emm$BT<0NWEfpRlV=Gt!;>&M zi872pW^7KLp^X*UN+-@R#(k*sRu<9>0AX6`ffr^P4zGvAaVXi$^*6C7lroZKY*{mH z)DRL%!^tu@*PBjf_4aC%up|k}50WWqiIyZOK(-kGz#vBiAHEBvw79}*k|E8l{hA<| z-Hsw~KvaRQS=$BqShEenK(bO4l?b+XiPsJ!qdS7lNuoJi;y9{SFb4Q@?&2Egvjg;5 z@651-GpOu5)~u21xY>lHmR<%PAgSCz_b%HW-A+O;Zd2w~c1M;RBoW#)KqB;q185R2buE?e)MC-8qLNMtkxRciWVjt6BdnZMoY3OVN*-6f?SQ3kyWz*PEFEW z0T%5?oCpk9WM1nQy=2JNA~I@jvEqrfh(*xWA~4|{=4z6hh7)+!87=@!C|z)o-%q4A z1Jp)W2YH))1>*@0_Q3oj+Nq$b77JUZx#!mSxPFc&a_^r_nh?Zl#YW8(wBP{`tc9*0 zl0S@>KNjIP-p@V@iwU#w8Jx>XR+4HWH!WeSufCP4<39)ZTsMDmas zs5ZU;G!8IgJOl&ry_l5Hu*e6_V9l$4cDB| z4THx+^-MP-(KgtP$VlmB%qt0^S*&!k^Do8?NVF8X0clqo5!md4co4xMh;xi%3HQD=gcSrZvc>b#Wz3z}*-p|$~YskNeYp2(_H^!2S zebE?uTvw!N;p0WgMZRbe(ylAo>xxE`)xJpPa;}JZ)mJ0Qq%Tr@4Og_s6`?itMLIrk zMY~6+WtF~y zi~AxOX}hnuZ>S>dpM4RhqP_^-mWuFjtLgZIhv{5tGcFARUd1Gp5$rVGx%L5KAQ4=iI%kOY#$rVGx%bOiqqD(Nn ze5XT8t{5)W%Lg4=qIy`SpYL*L$rVGx%UclS$mNRR3Z=I?wB(B6O8vYIF@WSbe@*P& z?7sNX`j`Gf?Hvv+U$8r6f*57rwZDfv2-9FS&wfczKV*ORgXqUe0@d zIWMOHU+TLr(4>UhDusW^w(n2RlCm)(W)! z(335+mG#5V`+ZqI1l1ix)!7e2bqi5t{ZMq?m-WNZd)#OJaP)3h)DK6wji|=@;pkni zs2`3VcSZeh^q4E^hojs|R9pRU^iEgQ4@ckUiu&Q`Va?_Peoe=nA<7;)}qr^)ZjFI3c*$ zfr3HfOH6;c4I~FWl{Nm;9z*(J>tnvGAGUtdm-WL|-Gp2STXh$5A#6S4%lcuf?nCOk ze(3s$FYAY|AM<7X5cXkT)(>Mp>dX3}ESgs9OFx`_(3kZ?+Ec!)AJ*!Aq{i0|ZFOr_ zcPA~WWi1&5+1*Kn1CdZHL`Fa;?oPJG?d~L1weL>;`b0E(BGlc<-B@n1*NC+D{RR@R zY-+x)lr86zK0O;oyS4XuLP08Hdwe)#zoE-_t^^pw^1H6B77`L3S3b8|$2jaD^Hqmh z|1bKiX{yua2kU|SQAHonrs}_hllG1Is7K(D+hSBQgv3DUrH{k8)|ZANS}Fdyia4Id z`4hEPtIA?mnS3H4-Ax=urxoRALiVbOD?$q<%ehwm94CY$M|XxO&95FEL_CWz)9b3O z;cBbn#i*>tO2KZSl}Am4r{8=;|=-%P>D>)MFL(=^f4aaMz&vJ^Mcn2GNO z(WnE_pdwJ9V%8p%XZ6ZK-IXVG;GR!lz~lFLvG$2^yU|$tNLlyz@;WcmW{oK!jtmg$ zH_a(WBxp|db8b>J=Z0@xR#ygQ;%V(dO*No1DQ&n-H8AAVKXSQ_wmEW*v@bvgR+KPX_x=lLV$FF{m% z=}E?CrxHA$)w4M=w^4BpJcNfnYO*oq&sM?q2e1&wSFiw^YyA;l2!b|l7?^`4OQKX* z2AQo9%MVXyv}{}6=`f3T1ZOOl#TN*w{1gWREF8djEAS-6={T^Wzr^<;&HpVk8e@(}GmvW0TpE^%@j#CObj2VY8sl7D#mDPKXlHf-Te#XNez{jF%kJ6h}dC2c4 z`OQUnY*@qm8J@){^9TjXF_30aj>%yf+A?;8=L8KE!}*J+-ka4;dzJqK)PcbyQ-Jlv zZ14|Hy;pm{+2A`o0OK5n0vD(EIkmR_lb=wk)IR$NHjA}u6z^E^S)2LS{`bGO=vRi} z;{7&~TK=zKx3#!d3nToXY0&hQye!BUV^mAc^zazJ=I56l!B->QZ5-jeEXW6sU}cxr zazXxs4DPa`XIE!b4|I0ee!?V_5>Dxy~6t?D@))ymT0P?K;lJzJnPb3Q; z)*cq>Oo6HLCC911d0dwovA)rva@hJGG2KKP-{L&eSvv7!f&u^F2WzBzh|o%eP_Rz79gdS!+2jN;^YUR zGYbH#0JdiGi_St3QdlTKTnut$_`iD&E2$S3M?=oB#y9+_B1~s9i17k8MFZkMP1q(0 z@qv_#z-~$4NSdTJ|KXRBoSV9&1{1E16ry{`rGa9WW+QuQKc`owImqMg0WoHSY?QoTfFr0(O(FkqXv0`7y~RWn);u)W zw)Vv+`z5v5;>Vo^zYJy##oPrC@H(lZ0|XE}bxgYR;3Tj}nzm(E}TR#VrF}uT#Trf`6uTWjq9eZ*OO!x*|YEx_;WCLAxU=`xK z7Sa;R2QNqSL_|EYO^`zv8-au)xvSx?KZ9ZVBKA|tDdWkwlgS9t-z0s#fo%(a{i%$7 zbH;M~v5e)&gvf#*7hmZ(jwm!bCgkbBHrWi>K_P#2Y*}xe!V$GCcUZ}tAXm|->#hG9 z=jLm9Hh6-F$=6J>xsY!E$f!-|?z!1OaJH22?Tl}`M{!GUv8-vIWvR>B=(8+!SzQc3 zb#3u)S?b;bRE~t)gUkF{k?9^>CdBLBbPp~Qe=081J-AFj=`!7e%f!`&%XAMe6A-&h z_dsU;yshUy%`?d^K!>x&7a$Rj@lJHFYZoKxbHldOfgYjSZS#{lJ zi;3Ro>hvfelIg-7Wp!~(W$m7GA6j|~3skI7k}l6ZxXd0@(VIP}qRbvtL9h>oq4^;~ zuvyJWasKh=C26(@yNAI9&=LQHemI5v*KExbtR)@^-yQ_P?gtfk%Q7A=Ggyf9#%X2D z|0mvHSyjfVG6R=EuId2@}58P&!u zquRJ-R2sLvlK9p}Du$6wD?=vUmLw!nKFG4eim}?!j*IY&N-z#VZsJetQ0t?BMd$Xi zzc(X0b#(?w1er4HDN51CscG2xF$acZKz$6%f&poM{6faBq!R%}Yd3xzlX;VlaK{@!rC-+es-TAZ`9GXd~4qQ3+-AbrN#JSEvPW~(E`59)7{CR*=U@fW$ysfUS- zfQYMkCXAkpdzWux)`{i7bwm_s??VMn`WG92n~#q zb2}pU~2^xQM24_p zNw^`~9?ff(Nj2Ldth4$}L)Hz_`}jQzZrE-<+R09k&AyPu;2_Mu59PUkpI`GeO)Irk z#(1>=b+&djfGA;ruW-b2Usu_5kl(8|BtF8f6y~lj>m&`PE&||(mz%`z06GG+^sMaW zrAsS>IDQx7e_WVv8dEa0={NFjy$m2d`Xi$=a*zLY?ulcRDP74{M!;6 zU0;#6=xhGc=L^qquVdO&DUNU=UzHV(wDR~%^AIFR^Hv^(0ZUcrNm=pKQ2 z=#FU*7ZP7#|HP}OYGUldQjWt&AFe6pZ1K;{tP{LZ*~xhqGP*pv=@tR#0*h~AV9;HwhxEUUj^&V2FDA`7 z143*dWQ3y{J?ab0!d(`RF!*37ZiF%mN3*U=+8%5y9l(aUld<%p!ZEj8oI%O%pz%5f zSmI=m8`;3FG$U7QY2vA=wrv%8t?-9&xcPMs3=C>I*Xe+omM&hik@<1_C~Ux~F^#8) zwu}W8v;`kHKQ#?knV_d8&KOF#tE&cV(O74%2XhuN=|xz?xa6Q`d-qHYG#G4iOQa~R zVFdnh!I0s+tYU8QvIWz)g=2x%*@Qm3eif|R7LJ8O?Ix5D}>fIi&~ee^MwBjDL&1Ag^k$Qq#=l8I=?)Qz&CLk^^ zd)ze;t{0jWqofV%bjK>?C&-VHr6HkHBo!&cfVR4m2^JG5BEcw8#8UF;VzqJW%*BRqA_4(!Ez675YrA8U>b)wYng602-*Py zp2lPu@C+Wt5U*fphtVWy@bs8uKHq)Lz3;xNk}MM?X?tWX*M0ZibI*@`_TFco{p(D` zEw9sN#k7c6@ZCb%YCe?$7p);n`Er749*ndoJC-Ji6-Q?Is}E*%%Jnwh z!>qAAz@^Z0=`#!vu#>cbd}}&5rdjH32J8)j*hRBCJg}qFvBOyT3@Zyk7&<8@3ie>S z_6Kp+@aD-nM}yD>Z_i9tHJhI`3$ZVJdF>l0b@HyO-4lk3sC!hF10Dg@XUfsiJ={jS zzLZ=&G|H4*2(laNpPo<^dy+#03jtPGzf4>;H*|zQ!*pI^G{E9;?qeBwOSP1dwiq6# zjew6b5WJM!!Srwd3xEn(i4?o&O|i<-F<3lZ=6CG0^qY}{^JBPC)oj<%<1>IdQ6MFf z$U1|_VL(PT%Fh81CkA4d zARQY>q*<3PD9pM#G9nx2h$eq2i_E4;%ay)c)nhgkv8FnUV71CFNX#Auj5Wo&yd?cC=!3@y=yKA*)vs9T60KE!(M%nKeDM=njpk`r^^mCh&kyecR}!o0v!y`II?nqo&x{sOkIsG2+ah7z!@D7@ z4P<&5;F^!e*rWolH11!s)A+1YRThK12d@+$jK$#M!oQ)5uo#vJ&sIE&=r{g+1&aao zJ@-=T*A`40p#*NJts4Iy(WzgEWdbdd@gcN%6sUtEyp+~cECx1>5>Q}bnKK*-D^s8x z$OGCDj>uFd*s!QnaHL985(R8!q~<=CF&tsVt?(7RLMnG!j|Vsh8D;!j0;)jMVYx6K zThR2-AqG)xU(JiABXGuOx>n#!GJrG1?36fDtoxvw#BmP{fv``_)pp8p3P-a@5&-wlANQ2T)ZQeG#Y5d5qETzq}pVz)lDlt(}W zB!O^7>kZUi5!%9)|0vS%CCR{}l1bb`xjILBt~pK)i?vT(pt_kQN^ZPo){G%mY&20Z zj8B-Xtr)t;)y0C5Yol4*lYB1~P>#Y4W#YCzkOAp1o)a74{%nGheEneC`TKPLAIz>< z9lG?y^dZ-AYnSQ(El+`_q!Z|*vzp@(whBBHfVc#38SnxqcSi?NKuGgU!&r9ctYAfR z+mpNZ)l5fm7$>S_fBd9^AEwlWf1-`>?&!O57N+9wlxlS$xefVm76|0SD@AR+Mg1() zX^+N9tv)(B+Gvil0q2E4$Hn)&0@fT$JANi>!`gpabLP zt%68JS4yBIS5a+rAV0v23OkYZL@tO*3eo#K@*MjqZaRy2f>eSSnd&H;*;ku(Gmo*M zf?}ndO607l+!1^U7s?w-K9;-(T-vDNEy(b%Sk@swSR1`4y^W6wxNY~&M;BIquflmD zHq>ocby3RlQ>k(GfWC$+QhW>ey^^rjK=?pk71X~pwjW00Ku*5y6RC*JOjja@GnuRPe@(STgY*7I@=5HSr$SOq+oO zcjc15CfCQD&o~95j+}r~kb2~VxQHic8a>RkMrkX@IhMvjwON`P9PztZmq)Dp^69+Jw!@-Pex6s0dkj4=gnrO@*^ z-)lj#M~^qMl0}IJTfv2}T}qh8qVK`hG_VL42p2HR>sX@Vb$2sEK&B{2K)*)`BYRPj%XwV?LZye(}hdZ7}Hbx@xa^sjT9Iist zV|*X!8^`iGX@q*?ZcHUQa1_FHHM4IBG*dT4DG!LA-OmN? zwTH{7?5}M4Wq*woK|fFXn+4(Z1;k^wZm$F;pYjJy*MaCexEntZ{bMduH^u)g7qW7F zkPFS+d{gvZ>bOaDysuw}@)+z(ZsQGBabA^v<)ryCyb-T7FEh7c3_bG$>yn0#LvUvH zbkTNFM<4-fHe!Rj02=lF3mPARpPJ`@6o6J5fcmYU#2AbtGG5GbXCuE${*_mM^gjSN zrr9;+p{yws#!7S1kz#~u_WLcxly9P=ChEPv^i2S2Qz+K*(>R^Ml`PFB)#y8`X@6NK zR^iL*Z{qu#SbrZR^$VIx-{BTE*AzcpEm(PPHs+M6a8?U|K=+{@KE#6tnPP5r(zU~l>tc61PDc4 z)%j#O73NEQ4o8#W9+Vffgz_lBLHYczzR2Og0tfmdKc$TNZ8(lEe*V{f2yY%O}{L8;h@Jnz!RBMqmf7Tis$FiY>EoHbz z+YuUC2o2G;Z)JxX*rPx18fYc*tz7O*)HM_-AHhVXPb;a%A*;YkFVz{4!3SWiT$IsE z*`i#*C@-{__^BoNwa@>n6-yZ+nGwLC&|-*Rnc!l%U!&%**neRauCjr@A~k>Mi$A%7 z=85|>F{0rOs!vbE8%`qnzl^fHj_hB{JBa&=)F49%fJOfU8G}uVP88VE(VEh@#>#%5 zQ=VHr{?^Iu&a;cnCo^48w@kDw_#2-gD!`7(B45L1wI`ev= zvs~A4|8j(K_yn$lq))mlosl+N(_yLftP$?y^2mvbY~yK{0-6{U0+wo3g+s6EM0t(l z2FJkV5_|uTeX+1dGe)#SW@Qy4P+$RMM?e!@6*)1O8WV=XlwK+lH%<;}-UTBc^*h8U zkzEC>M4onN6N--M7TxFY^`J)ecMF>H=*G?HjUYVwH;AjPk?x zR5Vdt{Q0O()gXpH0lIGbF3Mn)@{forR(fQ(&V5PP>XpcY z%ZJ(3aXE&bhRgSYo?N~MZB<<`Ceqxl=eTO4`>3w0SZDPdUSTmyl)FEayU!Q=kcZ~K6$@5tBJr5V> zlp9%xiRZyaO5SXc19LWp9n;cywWUoB9b`;+`U9Uo6?_b} z_WPMq7jMoYx)`gAQ~+o{n$bO$6e(j%;uxf-FcOS}e1T~l+Ev~+^yQUr`t)oB7rr67 zh^~F>!2t{dI3PF{v;lCqJ~-wDj-hWMI4qhyUfVD#SUVICg2dH5q6q2tX9h)syP_$ttaIhW7Jh0=~ISDl&%r1Tks@Ggbze zbo?V19wARmi;+?xDtG~PqW~)@i>orYS`yJA7CjElQDwW})@<7rxF9ah`-l50E$neh zwCkkxr}KbWG4tFx(&89Nj%E!>Yh5p`DHYvuT9O@7Oo)@cgS6I_v_=ASFRh{DH)%~J zy|hNj0covUNNX*V)>P0-YpuSt#y3@IO=Tpljk@XH!{%fMQ&l$m>3lh6&S&yOKYRAG zh9t_(GR|3T6#HYCnB3fwxaXKmS#$K1Y3=D0x%5DMs6E;HG3dZQs%DPPP%RFSMa`)bUkKX42Cj_1V+=2MNm8E zF&XN<;;U2MPp$Ax5)&%ZCL4IS5u-32ytc##l|iz1Jlv+~7izgK*;SD3TKVuHI0;Z(b+9~UmUS+w&>H$0oQXcEkopLRB_qIiNh z)@VIfJ*%IpZA#{k{>GmNe&JhJ(|i0V_bm3lkuCatub-EpfHY%bPhC@}}DuHle*rSQ7sSj&R9K_GWX?3kk9Q3I< zvlP6Vvzmb#GS{40O>4{Or`JAWjei4m$~#ky12sHZae`f&Itg4smzI~0LxL2Gj!iU5lV5Z zeJC?KZ#+pqU?_aNUA#}pwdOrn+AdLiULy5&cb%K}sf}yyU2MDFg-g(WiEq~dn)JQ2 zaxMRFvt5JO-Mg750oL{isoz34sGL_$6(-JUi^%1Ut`bhT*g2iBmjA}dyS>UG^~F+O zEJYFF*9C(%CTYZZPpB?Q)F&9&iF*UAdR zTplel(rbe2ZkMYWf70=7*Dhw)l9nEd*(G{{(ihv=wQvUUDYxr(7Ss6naX>}C*kOgr znM${qaaf=+lXBIOU4iErMpEalTO<$K9QT+g9SlfJ0Pd3Zn4uQ`Pw@XV|Lc%EOq}U@ z^Z^WfnB|Np3#o7H(n}8J0h7k*{kZyxwM?$w@~C0UGDksD{7(D3DRTDGPr}9*Q?jQh zn-XxH&gypHTvtnyd|;@m8R7^r-roFUH!GjdD~S4cM|o=w-1i@w2}yDI`}xeXdf?U3p{B#+}^?I(#a#Qg#QA=WIV6CgL| zZCF)DB~y}t*7pSNA1eS|vlw~^r&{zOPI$to?d3;8+sYj#cffc_JA4(ly69b5!XC08 z;j;(-RroA_xd4qlzfcT?JGw#lE#zQ|F-V1B1GF*8Z!C&gf~`_*89VveqEg)%Ir8v{ z%0v2&P1#P`w1HsHjeMBJ>|(abTFWMR2IxjXX)L{{mQHg%9QZSd$F>$9UKIa4bbU`) z_0=dEm2up*EOf)}hPeaM>fXy$K;#!HcN@tBK(nQ$396pTC&(wHK}_c4P-6vc5J7f4 z|5_v+3N><4o}MAG<$oWEB)}RlRVMw36DA{$@{sdxZeK*Kl}<~WRR2#q&ZW_ARKx!V zbmG4VHz2YnnO{$0@e&Lb^E3(BiTc!fJdAH8Qiq;Qr{nDcL$VF(dh9m%wcea-VVXiR z^WWcOBNHyF1M0X;-|71DiOC>5X$7E(4*FQ;0huqClej*xNmzkV3pk3>%$!F-?or=z zO?VBj0C$BrQfjM^tFc}P5EGC;P1e+xB{)r6{b?8Qad2S4Nt5P#(06;ezr2LkN}z#;o#sykFDp6hxvp0W%~^qxS=TPT#2H66=X4ibZ#WKosf`@BtB|q zR*J+1lWuIV0eh`4WjZJULTredM(HhR=Ij{`EYPwwl1+ve)G265#SFAIaF=ab`SB)% zUHh{Ck%3s+WBZ86jm!Cn*a5X9XIEOPAIe5nwluP`rF!1*rHNO#oYI6ss_y2YY{SaB zH>{`|XahJ_B>)95*w1P{jl@LNQjSyrs5xE!nS$|6sdJy_Xjf{{ywIp- zh5B$8vMr6W>GY)PP~Q4jTdR*ggq@1iN7G71 zM6N+_>Z9iuCZdW>=&=`3hY>}JEik**QW(*m6;;(bVBxnsW7`v)nD)3`tej(H41z|r zqf5sTJf4qC^61k*U}miSKF&*kC1P3o6(Y=a*R(w$x>j@_*$@)G0OBS7VX5q`n1yYU z3gUA}GYew-$eH?eoBka3r6rsKe=ZSzROB91@B}E&Iz6h5ATMY;|HVK0#Xpjhc-c(O zSK=g+Q0s`fIE-0liy_t$I#qOq(Q`tQHuI9Gq-->f%aMVHSe)WJG@&qRMfEWB1vgv{-DwLjtU`Qkf~Cha+d`m)PqkE` zGm>FNLo{RZusgV~=IGGD#|R_fd+^cdjzP~;RCGtbh0}JR67W_3z-bTy2(rp%HO{SO zUa@R=p^;JNX&i+a^nPFBl(Swhb!~LadM#+Z-(7lqMC<|!lYN&Ncl@nZUkxGi{P{dFI=6C*?rGhnI zP<>hihm;k_q|GYikzHB`Bp{C5fHmo2D*%WG0~RQMj%YQzGltK5SVWjYJItr)5A%Va zX>zPLQo;ib!Zc`IRx|;^ANU6pJ76^zd9eacwiMjR`x1gPdeiZkIh$B^0iHihvc#|( zQzf=970ftJlK(i#4b)dlQ=-kM{sx|~HGi3lF$ulG!0G97y%CgH3ySu&_B&eqfPhv# z@|)804j7}gXBH6#_MslQQ3cr4!X9Xy=OOF*JVYnjiAp>VLER6xfGbF>=s`>HHg*k& zhpnY_9rXz|5DMq88(RpWlWJguR%f_^A}}=0$g|c*v{o0bZ}P2g>bG9w2;*w&qrS;? z#?R=Aa0CfJ^_v8seFHRLTz5pOCJB;Zpp@%3wbnrC<6UZv56tSazApAkvVkRbnXq4u zF!Q4u(Pg~{sG1f)%i)N!{DyLM8(1(0hJ$>fk)Q*%R>No%tp&>$A^1U|4|J?h92=w< z)ROJ{R)~uf+i&)P1_DtGWISnL>MB-3on$rTl0T>wCoqss{%Xli2uaOP{olnqm3*;$ zr@IS6LiIyLy4gYL0U`Gg%ty{2n1u+uvL3I0&M#XITH>|>LG`uN9 zCKI5vmW=nl%tC@3B`#E#D$ETXh!ob&j!&I{g)8dJ4j3a5XILK2gPkw|6ZfCW7fI(63 z6zVcC3ar#Y>Z>S^fI?ATtJ+DAizVezHKe00N=XMJR0-_hdox@ay&!`!P&@Ym2rWa? zNnihDwSEpkG$2@3mafUft}dy{6hU0_hf6Z`6Z7Ji!o)zD9LcOZBVySVUa#Ci{N8qFC&~M?|0*>n_A*434YVd!7Zy#1* zKNxD*584a+rwse0)fD!V0$loA?j$a0%V?syYz!9F&BEUDGEz@W1jbt6`LOchX;*}< z-n$#Bou2+TKl=Iu{oa_bHgA6ugT^x?OVbd@r5<~b_U0bp8 z_#GJ3+(q<4(M$3jkLy&-Eea|{S9Y?tmLz`BG)edM0>!SPXOQ8vz3IxD)I}XL%Kt&w z76zNBJ%9Z66Bx-DeWe0EmkCLY;1c%u5C2860wB=S2u1D4g%RS@%!w$|9)$j{jZmi&K>cQUkZHEAOhj=sY)O@_FZ)BkPfFHd zS&^2IXz*b774-K0`SzA5p^5NM&{1gCisssbp0P-X63$8 zsL!(rq>O8i(=D9w_<664u)Htz~Xtztl$oo-X_&$>K@aQMr~vf!i5raAPD=p1Bo#A1H6y(gDK zMjJCu#Jni`&Va$X|FWMuf)lFQqN2|F!U0Btl z9=NQ2%=^P#HC&t(d9aY~fH5@P4v{zrEyktI4Wc>wv3dO#XxL|B6X&^?%NgU84ULv0 zCL82t=;x4|0VyUm3segvm44-t@RKdP4?k5ZQAAHt2%IKqnN^Tkn#d)Tm=BdfR~Ao3 z!0DE(#riep!HGrpErGL9?ci!~D$Pp2gT6=X#;ps}`39xt^P+_1GC=gB(1UIdQPOI$(GuBLVICd%hR2irYCiSqPIgU}d5E5HNaJ&Y33V z(Mr}VRi*eiE5c|aX*3%3hR_5i%w}IC0wad(xt~fWnNAMsWN$A1#I6oPLUPDu5n;?I zh)KK%7YwqXU+uJWpnU#x!T9r0$U&b{ZD{%q+f~_eBi6$K3G%(Ng7CUK7-{_u(g}$P zP9Uudv-C_+(Mc>=cyqIc2^-aS`n+KVGtZC|7YM6Shs$QP?!zKZu`Z#Fhhe=ksuH9< zvbFFDo#@8>xtKjC*GFUUk1L07kkH+ZFEO~B(b;a?KZqGdS5D^Iq3bCf5Xbc_3(N6z zx6~Z`JISO%`^?vldvt1wh6HBI4+6(OEleC3!&EkZsURXIClsi4A|*aF zqXqa+UrFvEnSp5rknvohLRduulH;^6l%;r{ ziRZIE3qXPzQko-m>rJ?wk-Bj^gZss}o#b3Bz=)`5ifr1%``#$04VVT?0*_xSA8T%( zEAKW5qc@=xV*#FWTWpufthl@5chC<=wH6d-3rA-#i59zll1mWRa~ma@s9 zmJr{4;9nFARM?bkz`ac8>`>!oSu!?@yKH6;#P@ZxFM+S7uB)MlMf+$A0gJ52$+|;+ z;c}x9houA%Et|}SvzanAI(H~`{bP@z{;~H!rLsEz zv|XNQ2iSoHO`mYdwwdk9Hq#z` z(Dff34m)o)-q+8fJsHl&9X}4R3EwGYEbyC3#|<6&W;*mlXwv9~*RuKj4BHy^greyN zo0G!bD-l{4d8>Q$mkW!Q>DpEmlU669M5YtRRf_?nY1|%(U0y~t@Azs4bM4|0S=1!R z6&zn{-to2O9bboX4C7i<&Hpu&9B_QCdH*6TxhgnJQQVBIpvU;imWW0w{;#F?0RvQy zKJ1kfl7A;K(t}JnVg9+lbZ8cQ@1lJ}kRV&g-DwXRVj85e>L8dA zCSw~S(z{74;2~xa#%>lkK7~`OOHeppPs7o<^t7mu>3`9*j69f9G4w3#?}VXUM-(Mq zx^62Qv{uxV|7LWA zc)0(l+a`R(Qyxm0d>OT4`Bng3mMveK1*)=TM_*6i*C`RU20?f?MIWs z>w924TSbb&+s7&ItN{UERkYc};e*JA+qKAm#t9FU#XD27c$)C9334X>Ixrj5`p03Y8 zVQwwHw;jR^mId@aH8H}vls=$VV0_xX;ch9}?p{q$Qyg#T1adC8Pa)%ZLsI(KPS1QQ zQCFBxrIl&+-6<(h$%=J%9oRAB2%B4O`^TWW`7XX|T+^trS{ zGm@E^E_5K`?CXFlUj3*KCE#swo6!?NKwC8@t@I!1(qXT_6>baVPX-R(~i zsBo%IZ_s5Aq|0R&iaRa~xT0{GPjAu1K|owc{av*maM8P%7xe?xl=Ks7 zU|!)StV8;}keP&I?F`$1eXs@wO~h=Vktg*j%t|9S}_q!EKr;+#%g6{v2*SKA|s* zl7xI?qRXxBdTnUi_!N)Ol>UjmYlUb@+0*g*hjoQ`Y}~!$xqHp&alzQvy$NGCmt_iF zim_Xn3S*~um9fk5Juvpi2N*m3tT6WAH*aeH;5eUy>dbPf_FwO|+#@F2$=10oK5aUW z(7HUW<-v^iGk}p2tYt_6ZbqRJHWlzd){7&kxMq%+MnQs!6_mITVk+ACIa4gHXPFQ$ zTOd*yJr7qDNhVmLNHR8$BFPp86i7|IM}dUsro{1nZ9to6iv!xd^?Ht0&t%;5rdZlq zYUg^!jxlf*+BrA?99AJ?*wASbyA z4?eBl2=bwX7sFW_YzsBy**LT`9JF&ZViG1KESKi64)9d#fJ9GTv>7`ZY}tqz8tl{W zYhehguC4S0EC_zBRf#tO5MB>J_-dSP*7FMic&`Saz7l}_asVP)y)7z-R?k{Ot7kxd zZ2*YE{$iD_gBCWswsAx7U=}d6do7&@BiKNe1Xfuje_s|YC{X6Pt+M`(Rd&57(%)&7 z6)t-3icz-VO824!6G45=FKAGo_ zBpy@GBp!nw#G7BwA4ojLo=W_r+)qwaJzq{dupdrF+e*ZEsZg`?~yrfp^bU-+6__y8N~9(6GGU3N5H5mIUoM)Xq!g*Vqnx zM;e`o*I@Mw?;kJuA#|EQU6!73Hwi^07D}?#j;xfd<(iy7{xAAVQYU}9yxYUbC*2CBrv^HJu86d$CPfT$&tv_&wfE*-CHp=R{DL@Mu6@CJNJ2j4g+d=V&soyqKXr#1_ z#G73&(kUT1+pqy`lCY7sn2Qd3w_%BLCR2lr9;>Bv-OVaj%h%uK-31;v{iz!M0wN*_ zifC~4`Q1_`Fz|GQ9k7%a(%mA9vBUqMje`x5jSa!77=o9dq%QF+)&>FpmG;yI!5iOk z@K56?ZtEy&5$q6MgsPXb&tJ*pN*>4dl<2v!=V3%DWAc z_4@xDdS8ii$Q5zYs98U~hs8L$6A!fiWw}M>ev?A(_Q<0(5 zMWB@E5`1hZ-Y+coZb>Y}2#t6`y4#z)#ZYlqEY}kbf0VNXm7qK^p1YLw0&)lwkc|AC z8!g&Iqrc{cow#(#1|aFJy}CmBh@t$A5)Sf=e674g0Z9tJYg-5Ja0aMH$7S!-|HXfC16j^B0A-!^ z2$io6F_9klYmwr`i0v0Bn}l(}c#w;9n?Ly){1y5t>`P#oLd%*XyMW?7iG*6phy}BajqaLy4#8@X#dX_+q> zm=OFDv>eP@Br3XO?{DkedsU;8uUhKWtoSd{8#`8voz)6QP*q_u1WneoS?_w{nzUy9 zCiaD{Wfr9EBcZcVJ}|eaX{w#JHJT^Wo-!Fw@El}575`Zo5+C6Xq#ct!7pi&AwoNT; zbNdn1ZI8!@iK~6G3=5V&`!l*Txt%a)Q*<$nAzqyNNO9~>N+Lvh!9_}7I?v@@1ln_X zw`}c^+b3ORORmtowNew4HW-XRPgRpse^bu+~iJ*e~b z#^S;8MYl;0rz>@#aomCVp;CryQ1q|PGO`>acr(rQ-mlcM346GV!jA|RQGB2fJg&9g z*8MTaOaa=pp|IJn%T5kcFt*w%AL;~VX{R$(0i;zDG1lzYa$GPrH=@C?CDbJ?2H@%( zsqd~IHXW&B4Z9J21Z2M|vT+-7Nb@f5_96`YP2s@;F`dPK7u&;OVaa&$kQ7qM%gMglSt(nF?df@Zk36Oft-=(JOtBe%asX2j zKdEbC%Gm)-dGy1~EmuniZHrhOV<@=nAMZW=eG37^+C4t{X!Y zyJ2Z-NHC(F{LHTv=W6Qi(ekdIdmK7j;Lz*L>yYbX#0Bng0Vt+8f0g?&=?Fbz$WMVk zIH;EQ4|2c3_3`9k?x)=<6nVLiA{aQ<8tKdZkq%Wy>TL&Mqy-F``Iwj-JNfOkc7Rskfv{FwqM@@vTNFYXL{PnLJ+vrsC-`e6po#xNbO zq-q;rfzJiReqjKyFU11S4y~0(IYNM^kYwRjl$sAWjD(nZPWb|zksU8m^)wyqxNcGu?WaG)?6JC)z zYU;#fck~;w<)MaS-@@G=#u=X3hj1N^bS@nG??@`TB#$@@=kGDi$5I+#;Nq~Thhst< zxt>8!%7)T%-k}8apOL5nKS>!S6oyXivQ8l0bI30#Dv3svnr$gXrc@Ywp*t$BvH>h* z)&-8}wsd_|dxoR39h=N12YI2Aw5(fXyTrFX#ZNqqxMHbDyQ&Y}7j7tI0<`2Z4lS?$ zE~}Sc9?E9Xwsh3tM>*J9M1uQ;azgOrEP9y5G^Vr=q-~<R4<^Y&kGfFBJz^31FPz01 zCEK|58xuWIvK+z?k=qwam-84}S>{iJTZfaCbW*sT7EYy7FuP6K0@vJ}E|ugj7B!-7YFWME$`@5{(5<*lBY_JrFRdPcdGMT zrDT*pRo;QO;$xaA{8V7EuB&ttBl>X{PS>uhkQ_89`NR6=_mwA?EO#h+;1CrCfQ#xw0us2bG4BQ1&bQ*N#X1A`fYO49gLA_;Pgny9 zwO<+?wje2slVW1gyWwL3Ibjr4C;^{gzuO&YE~c;s;jhrd_A7tl^_Jc8+T(e5DgQVg z99ntR3e-uCEqv*<*x|k^hYs9hu=lsIRDAQk==CR#m6^}p|njet=TCM+$e*aIe9ivopNlf8i z&fEp^v*g})UHsfEuPnlEJo#GTb7vEsy0MD1;vvpxf|H1zmD+&?%kK6#(7(cPi*=7Y7~3kX;GTt$U|} zj^o%aMz9>nd?i3P^-cv{ia7|u#@jp&^0d(I`;Qh0(2AaRL~K`crk+Ih{Y=b zIwkjdM+zsci-S%9VV6y?Y;D%%?nJh3FMEQEODdLF>YXU$an9+*P>=)|mk-B_gKkVH z*g(oe)|cXJ%8Wjnj4w!h)KCLD3l2<5bykx1}iht{mH?S?Fojp>oNAg=nSS5Q5 zpOI|^_h`xPL{e)D={JU9139#E1IZ=|L0=KELGxkBJ=st50+qO=VlL7Y6;d&}vWrQ@ z+Gx<0bmL}yKJ#yO?h^;j4`wtdZtk?)vj-^6<{qXHb4&x!R14*kYZN@3_pPq!uSiZR>7}Qn3`B?B`zh_n(?(F z*2>F5YHII=Nvoi?<}x(EA99cn!zVfK*tbvl)Q2R5lF{|=e&raJX=N=M zQAD6@M|IiW325S9g9JiK=LSMbRgg7;iO|@onFu-P2Zk%tqVZXokE?dV0rf$}PAI#q zw?MzSozU~yzMYVMT8cJo+CC@hRQ}00f4s1EV%hKJPnUOcojP@)u+g%9^zXcR)?4f| zoz&KUAO|GY95cR?CuK9fdW>vLW@IyDH_T`afGjtQF3Qh^4+Y*LS~(zJeZpBnW>J@l zLT4@mU!rHtqsx19nXd|Nkn*39Vj~#&S>!Oz`}(X8(zbA-SIhULwL2PPu`9j9ELon? zz^FSqXohx`3u;w=NF@{c)5e4zJj%*GkchvHCEcels^zbIxqw<7sJ&3!dEbv{A$caw zbO49^7=Sqsp2g=7hsg05N3rd--mMpYKLh zm&@)6`L~4T!xYeKay1I{Hd5)mNHTekvR{V@$L$4dsZ2Ot4B}|`S$F`9Ys$y_X0-`J*odcZCt&m6IyyEwzQQ&{RaC5yd7x{uvNJY5MX< zieJTSYm(V@Ue+}4j^^`(czb|9!uQkOQP7RvO7sbya>}IG(B*DWn#(l37|s9ci~s%$ z#DmHCmYBytnDHq2vG7GAUjs#OBqHSBYT=2J@Tdt(Q0c`$rD3n-@kepgA**UVpFg&U zgOGV3>DV!)8z2j$4)7u|<`0qt?@pcEP;(qN$!B*lUuw$uufCOMvH4BP!__==l@1VJ zHtn_}NMHn~N2NSd_h0(dzvh4g>psX}`7zh()X2(C2|}0ADR7qQyJ)9iHXMIPr_k8a zsjf1&jpkqay`T8~0;ZSh)ISJPm+lZJjxaR?kODl)*zm>A|JskN=u{2FqB_9#Q5+eV zy32(UnrtXJOdakN|of3n}Yo1BqDYk-rSvtg$ zeJQmd)W=yy$EzF+M1#Nh{C<+c#2weD$awe}n5ewshTg0q`8mS=#PLFHAc9trJh@M9 zv@jvyN7uw;J@YO533;lQmcDqftG=U-;~tb3TBe0s{`4Og>`plFTzOZnc`URBTi+N= zL7aY#mA9B>vEitgT>7?(2YWwuE&h0U`%Km=`7oxQ6`WTqLW@sI?n@wT$$f#167S~G z7<10QKGVicv(Mh|pG+oC*2xlrhaKdTpPyOE?>(M1=3&$;|B<-k=f~xg%!vtmP>gY}FKxr2z$lckFq zw5&CK;(dGkEIW2SpMs8S_gNNFZE}>UIKGR@iX_DZ0FXzM{XQoU(Gj7&Po!oky?pLs zYjZ?NN=I&|l%*&m%~{!8{&IH_a&U4$m3uQ(us3-@-J_e&>k8X>PS;om4ks`2QhuN)@NG&FTA;)BTGJ9%9CC-*vO&j?W z_T=HplXLdu!OD}f_T*ImiEC`9;(fJ;S>{>ctm`%(v~>QKedYYTQPcc@YK{D1>cBbC z#l2@q?+oqCWRa$m)NnI+rz*V+zaQo4?Op5mIUf52I>ede7he855f(XK5K9{Qd-f(5 z1eVTEmFI$Ni7ua{Z!d2*eBli3ct_&MzrbbMa>zu{@_Rpo zjBU8IgXBCjdN-HN65ws`;K$SN|N7TYe&R#7yN&wswR>-Ov*E{y6S&a&w+gJN_EwVL zdqZy@__uGVw--|DE&3*g-a`=N!l?%*@~_{bOg9F3f5oRbt&g$I;!FW$!PL3IQGUK765(iidGjkFcb$9!pTmuKSn8ZU zVd@y^K4H?Gf-XHMMV7db50It$Y{%0X`62xnQTC}sw2HI0(PUFJIi$q|QVcXnHZ6YX zJP$a!XtE1U4tbh{2Qh^rPP9^%PrNO;onzBT=6yS*j3)65c0-;8M94Gv1~(u|{2F8B zZNa&@PoVTghc0r8q=G=mc&5B{P$yW=q1E{^Aye9dE^`FD@)T=vlnt6q)FJ*1oq%~M z>zfBlAdegUt*B!cwGDkHn|@$FaHQhTWE=T;Gd030baGiq+%IxJ$#dn=Djz=2L%Iih zC{x%p@@ILtj)z1N6d4=XZ`(HzqfIe?K?45nT7*w{{wqAFatqN}pHpbV*5?vhY5N>P zm9D`ZKru0gyLtQdTVT2b(%?bLZ<53V$ix0Q_(h-^sz-ZHPP5zi9UTUE2%7QXkKE2D z+$xI14IMsY^!55vV4`K{HjZg{PMQY8b83d8pv&CbNA_VdTCq0uKBI{imxRNJBlUij zB|@Xl$Yh>~zFKsVC#PVbI^4+B5v)0mQjv1%`zuheWqPfWICbQ27$Xu37b6l2&wtAv zX{pj9EkfbZ`L6LT?wNz$FLo7hLTmRcqR{B|d$@40^1WQv?@hj&%P`S7dz0_v7k$Jj zm)Mf;@ZYPyIu~((kVV`gWI7!P8SJtQ@N5ow_YBFcJ0(3!ZYm$!hyK<73lTQl^#0^y zT*zkeKMZ2=hx=Iky(G=B{4*>_lLT6jW`|!>KK};4aIg>0got`;h^RH8Kt!^1hZXDL z8NXslcOe{P%?kJ~9Av!;cfvu|s&FUpvQC9N!}o`0{DKK2dSAh}Uw?Z1fwh2aqVq^} zdL(PkBHVD4o1VHR5nr6WjFeWFlqNX#er<}g7t1N4f4F6VsE?*#BjA5Tp>vNNA z9_%BeB5UbO<3$wgLD=0q<7ZK|GoWiQM@4`ToN%hcwUuvQ%unuR325DVa$l74AU+T| zt>FB0?2?g^Vc-Z`zpMDgoFsXtI=b#nmOG#uVIm}|Wos?&uB1arKD@XP@tP`KvOPk6 z;8=A+>=gE(5;UiGG*{ch9PN&NBC(Z$bcZbpeEsV``!Oq!-*8i`Gy6s#&(d3Y%4%@S zClEuUJAe7(tUk%dZtb0V3>-=p`SY>k`UUame3Wasi5KaYwx5ZoSQgcpY5?C^1oZio}fI?6$iEg^X!lr*_ z1qW!-;LI$hvw)DOmsw(xBoeZW(4o1EvzCuZLj*5Ugz@f(RJjN$Ed8kHM;}j9ywti! zh$^h7Fts$P0qVT=(KMzB&hzEw<;cT)5Bzg6vuugi=Aaha!hGoXeVL+jQFadWpEQJZ zq(&}G8gXy{?TM2_LR$h-DKFs33OCV<$8~NeYdUG7@!q$y9O9hAufP8HX+QcP5zk49kT%a0f2{YKUiqnc4+Z`e7DD;vg-A&niWG)++4~c0~ zr1nl^RuBDdwKJF{^I}k#te?yimTl{Z{1ZWGjqP~g}q_+BW3i|sW#-_9oe@m!oHb0Y5%9PL$p zZWH}nr{in7dE+=B>*OOAr%PBEnnEO)o{x9|UwYCvDqp=PVK=elrtaisig!LQ?1-CK zyjux9!)9Jl%-OJXs0fsyGPWs79}3ol8bfu+np8;0F)^b>g;t%#;+gKasE#o0s0|K+ zppku+ZtPd53FJs8j(Z)b=T6IRNnd%hy31hMsxFhNlvM;IJM}{#nLjm~JexqC7S@Zx zJ*~4Ej<~5Mw}B>+;F$*AaTu5|k^c|RntadWwp+iX-$fmh{W^g22D&@4ba(w^S%tP# zR>hsOtj2^e+1F}-pVCZr4abC$9l$FP}??#{op4@Ow z1*nU-MiC=@Q7TJEh4jf8?Mc6lh*FR~t&mID@EZ0LsK2u&0cqmc#Rpg``#GJ57%d3{ z*f-{hzwDy#K!u1!{C&b7@`SI*K|1USU;K(rSH7XsCE>G6cd&`Dbh)9mdilO&zS>WH z+heamPghO8tr5%?^Bt2FGr>BOp^}DF8R(Ff$Ef#9tSLyU9i)~&j|eUVS}~kHe_dSH z&K|IMeW?FFkS5AWQUHno460SYBx)4+K1j+YL${pUj_iXDN?QDy)^-jUWQMiR8aX~I zHO26fbr!S}cc5#D@)9ROf5o0Af3;xwv=e$KZ0BdhYRoS+PIuH(ZCN9#;0z{vEvBGh z3HVyG@>BPR^#e%xHA1@wg`~6IgD(T?RfTqt>$Tug6>!o(5JOr%HSMRLjB9fzr$1g- z0PFK8&L}pqtQOlrPtnv6ih{eNk2D=kxuaFLLax z$xV&?l{(q}@*BOjU){2@ukln_12ckv>+zmv@-iDQa$b7fA$&Bf2<)aqMK}e`%fS2|2$Rx)F;dAaWt%-*ItK- z1Mum3z}sF4v^wGQ$ayL@ zqYbcfOaaMV)_AzxHMx`p{JeWMo700)*f1xA?2gW10x%ZL#Gv+UfIU+mmQ7&?3@P)N z{#ZlISn_+Eng)&Ue!loFJX!}5p~EP@g~XzWbkN}w0H4uCC`DSnvefA`<5^82MU?8W zWsW{5TDKa$OBO}Vxk2a=G^YWz9uqZzT4x(nL4Q8gK*ODU9C3wVJP2vtskdhu*#h+} z-|}64nIS{8{2x5;Ib+B~#WG+$C>{|-sftHYJ538t>@wexY@ER$=I}YM5N?1?hkjlA z&t2!Ziy%7-%E+IGoz#iSh&|3Q&ES)1LGx#Mg}f$AfV~<1pXU*5C9p|*^c0UqcvR7* zn`*E2gphtg+6gcpaHglTwD*6a`rEQC^8NMrAO{6HZ*asu-(jHx zH69vGO{imntdyC$r2a2wyH}T8Hk>1RT9ruSh>~7Z3TJ9!YM%)1# zsC(}|-+#o)(Erf&dAiOF?m+e3@48qnSdCWEbv|H6i=&C9`-a=1RbH}&i}$d$KFZ&= zjF=(U)2D=+l29aTy4ne0cvmNqFl8XJM}^(dFK~vc`9K_q9s{)S7x0A|4I9)WaBCZb z8>fKKwl}t z0TG}+54$E@CpxH*_1iaP6JtHFiSg6H#Awb|&5BJ&%TLFMrURaFDV-%uxD`&kzEXAAP#!47DmC$l@?V z9RP&Wg%0poIX{Aup~4lLpB0l+FD56d^bFDPB9jB+cci0R3HOW67AK8po(AWoCR)OD z;3>MuL|Wv9RF@|Wx2X?0ezfq&u+oE@T*Wst(yX!QIiD_~ zr{YWd!al$*BG!W%@dNZt8ngOgh01x3$}Ok4WWti=TkZujhXdb4?ZRgIXm=A6^=gx& zDvQC?A zhz&0De%Q5oPqQA_`Y`PF^nT2i$QK)_ahewz>1NI88%if5ZYhG=k-XYSuh;dJM!K5~ zvL(Ub$Wgc54Y{pui`z_l*STG8r`zFnyX$-ZvSSX$9_Xs??-3F4qmJ3`Lb9*`8Aj&! zqxKq-_^qI@ZB8c&oEG5Tf99N<*KNJH?LCo*rI(*>cRSBysoQco+w3dZN?&%I$+CyC zExx)fgVk+bSzW)bd0QnzUmP`1r8;Dv^gEPZqK3CN5LO)zw(WF!8)uaKIO-|X_{i+~ z(_ygmjz#OR1yY(AfwpcH{NSL1-(_}JTF^oG?G*d~mnsSH+xk$p!;gN)Ap8uR)#>Qh zwNvm5)ogauu+`VF^|EVtivtlN7}^PbpdRouflZsGwXpF|n{Cevwg`tWG}0ZqzSc;0 z>iT*k#hAGx`D!D*&KKL-$J-}ZR93;A!si>sh`D*Xak)cQH_JKT%jc`rJXcm@bsF@t zw4eZW@qEK@Fxy;x_zX*F_5Rt0>%ZSxc?X#m<$8Z4#UzU~XX*MAuwWB+rznTTFYZ+S zoHf}Q+OZYv49nlQi=9crZC#Rs8RdwJiD3i3wb@vFDK;it+Z`QN1%njYhUHZ`6gH1R zhJo>eFOMKTLemHF%iwZzw&{cfN`CIqpW8Byq@L4>?_8&{Sf`xvcvreHKgsOadDY{( zNXNy7E4!AZf+geRp>Bf`BYt(_h|8AWT2Qi{IT*JNem6&8&ZW9rGFggeY>xdS!6^ly zWIVtBv&8`-6!FzzB4g3xvyPfTmSS7Qp~}iZCvyRH>b&octEI7Y@wTVG2)(&^){|}N zW|gDb{HsqEtzm8wpPMm-f!);ICT{H5d0NZQlpVX)ju$5ehe6|T%oI%6;r1FO1vN(M zwjQ||=^tutlk8;-NSmn%%9vh)DU{%TGc&j$Z~gO(Pk=rubbi+aZ?^ktuQ6`Zc@<4Qi%OFdmh++mQ(t7){ zqHP8DsIsE@>O)x3Kp|LB|NYj=yDk0aB`eyTZgXim-)BiCHgNU~*RR#HX$U9sj5)Ap zasUTrB!z+BS$oMUGRZ1hDOp7;C96X9sC|w}Rw4DN-}snqVxVA+#By1y-J8kZrhvAB#*$`Gh;ZtXoo(c6c8^N7B6WUFVR>iC|*L{B2$>7s)PHAm(XqT5?027m+WzSPG|FO z%b9dDLh9BtY36F4y}&=jQm6&6!!X5a1ZZO~Y`t7ku$Lq`$iR4I?8SZ=d$HoM7n}FZ z4?zvv5J+jL=4EDD5VN6jF&k3ZirLgWv#EJO+KRxVRfNre_DoqpHnJKP(o9*SN#Qjw zMRFOyE(Tb|z95B{pQeag*_mwNbhcemBt2j+z=ItRWjiGU*4PkKk(GsZMRomw%{!`L zW2kC2J8Gax)nK2L=krzI)+A!cq1Pd1~9^Bo3_Ds&BGFAAy@?wN&Y+vcdjb^vcacYR>L#i1Ce zgzW+qAhQk2AfuvgZCH>BOtmZ(Xpj)bq*F%UPlj7KxIMkl#PM3h9jsLxEX()aTm`XB zVqN0io2zs$laDM>X0-zUWxkfbEvvtH7s+>^Vhk%>#DXSwBzLE&F1Nv5x!epPaXFlB z*5x1+%VmGMC6D^&Dwb2<+#N-l{d%n)#q^p}H;>|x&G>Xwl>LTG^BNpum~%>gjgJrOr;H?kTqphRnO|X8_-hGpA3BK9uBXGX%rK%s(`g_Yb$~;Q}Qc=aEI( zj&hk*HN4zbzN}|k%a`nd%ilX)`Mmd^ zeux^34W;+A&Y|*Uh6Y{(0UPVW;8@E}dgicVSboP;2hBCUveVmjTis@A$E2b91n9fx|%So9H&zFs-I#uON$Qo4G-YK_E4A0<- zQe-t;y~P*%_W)eSt8g6`#d!;_fp)xa@ipj-9jAK2bsyi!T#n7GI?B z!scogU&{0?z7hHWKeeUBH#`~>E5c>F_}27mviSP<06wFOP>LB9i&C=qM&9BZrx>Xb z?FVb{7GGGuL8!3!TAxxP8-vA{ds%#^u=sA!FtGT>ck1m5i!V>S#n+e)<~?7r)C6yC z@ttNc@u0<5hFL7WBgY+!lH12GFIa6|EiAt6axBm)FU{i1GFVuA!9Q7i4Hx(+i?6~=2Q9u3QH_^k=@gXB zSKegQfz7u>ieU4lPiM7hm*0M6c#3b)vc$}gV5L8Eim@rJHO&ysVsnPg*U#Aso3A8A z8d+`gL=8M)LSP^X{#^%c!Y&PHKfX$am&F0$- zHeZ75t2STG3&7@U2Px^Nj^v>Ag} zOqdyirwU`R%{j(kX3C7g&0q|+*~S=5V>e<9Mn^JW44$YMgVUS^PmKH^S}@A*FO0#S zfYQq{2FIe8a<8W~@^(kMjJ_Gzx9FQPGw_s4R()fi#DyU^zMFnYri$-ef(SBOzC{}=`Sx!j1Vv9nr&rX{8%P1OqeU`$RD)ykVAjl9b58}Xd zn&D7m)Rj#$1oyZ~U5Q@Bx&V)$KQaU(9$*OW;y8#Q*dut?D=Gmh3d(r_*9^f>7>3}k zn?Se6Gc%uXH7jrzmWixHC^mbz%@%DM-S|(~&dwLV<(IM3U4O=56F&1#v21vQM5)Pw z$6++)H2KyUb~#*o+I5*B5hd0R_Q&?N9^Q8J)=Z0PaX?qHWtFaEjixq-rsmgd%HGnH zy|t!Lut@)88nFHbbyZWWp(Q#2ud1QjIuZHt;_amIrlAwt%1)Rb$KFo)w{*hZS|`}L z=+vv!Mb%yMylw|}$&m6Itp%g*JQ||VuG!7eJOSsj2d0q(3JWaMv;lJzHAhw*AxS@X|^H^+urtJ9|8M;MzhOnv}lj3Vbqm@FoHu1 zyhB~XKWtmQ-$i)xFqhvryTG7NLE@0*UI}WGgF5D>(71jJaS~O)8m(1q{9#fI9lRSr zIWuTDq@c8!*`W(F(?=>k*#hS(xx?eEm+&x zW@}w84c2;gy%_d;`V*V&4n4?jC@?B}x617GoOZXnK@~EXcb#$X4kOt|0mHTI#!?M7 z@80ETY#(cje_7AoRlW?hJk!W_hgzO(&>#h?73@|8H~0$r-|nrHd5pbR)RTR$>a0!Z+$CZ+j_sy4scP&=DdGzWyOJK%H)2kffox>qa6vVrPE6xkCoEO+^ znZp8#p~7LI;09}`g|q`0Pzq@WFrX6B4qy$1P`Lv@M|2T~g{11Rkc3S^#bE&^fQ-K- zFoUhO=Rya>;};rPs_To5Y@0B58(hH*;+xONTAhN?TllqEW%gGL&uEwmF-SpnN?{-$ z&{z`Pv==UIk>=K9HZug<+bRE+Wy_>NlLyRBCbzA)DivdaTP+o@(OO}(#>A}VX}2(2 zW0uns>w$?up$Aru9@yJU^kCP@9!Mx$J=leZGL!^CEZKqaylm9OpwOrlp;3EljRF)r zG%$e=3>&M~;IP+jv}msw-<5$df`N7?2>1N+VvxzbCi~VdIq7K${Gv-HtAx?pTSmW4 zrEK)J6^dzWVD$4VM=!0$no#!^c&z2NcQI`9CU^u<+gbFtpP>(alOxk#(FZft$2=J5 z!o2dZccw+6)n0K(aD;AvOr_8eq#N$)0cx_*-ftdw@3@dDj3I&P#-(q6Qj> zMV0-r8jM=n9~*u9Vn#M;$Auv9!3^z67BaRmsAF-Q!BjKj(UKBQH zQG_UXFg~7J6>j?L7Gvex3dTnZI{dbUn+|r!yO>l5g7NX(w>#YQ*AvviZ{u*&l`zf; z%z@Ki4L7|SZhB?IO~=40ELzRr+FOL1_Amp7a9MABEaK;`hMWFZ5N;~R;li{P0+p|Z zn-;cjCGjhb!oopuGGR!wv7)@##2oAn7;ij@M;`uaSVO%rZ^k8b? zrlp1P?Hg{Yw5dKQ;%d05RMA(%O|OQVvL$;fakD5C)($r`eO6tKp^x+4Ft7!c8wuZ&$-j2UyzGaMN?xCHkSLSHn#k>DFqv>A|bvrueO0 z4L4N?>J<(*ty=xAhMS_~M;1~zC`OK};il$M`KFPx2CBlk^p?U+kq~^i={bb<72&2l zDdI-))K#=p(?yHqCb!_xmM#$)fuofnp)yvPQj1~>lWZyO5bKBnBkZ@rLlqDz zvmj=08Dz16k6@nHQ*3Whn#$T&$I+wiP2Y8eYbylQ?3#6KX%^_&&zG=S59*a%FSt{d zUQYOn6u&FMopf&P;7;smc=RceN72*E>Jxd>ss?uYzQWQh7~m5HXe~xl(6EMda)`kv zR;$FK2l$M7_;B&?Aylq_nzv`-bZ~{CmQ|ou*^*#KUK^T&ah2*m!4;wVHiI^iktQ-^ zY#0Q&GB#Afl#&x;0E|j}t#3INQZcv($&tVvgWn z#p7LMs#k^>8QxNo)hl-cVN?Lq@?m$Cy8#D$%i$@Jeb{wh8WA;5D^guGEzK$ zrL3i^57B>Gwg$T)i%XHMVUz~%F9Q*JAzK5zC92J5TK#g7!PaMMpl)SrAUw>6V3M?< z&K+sF0Tq>n%rA8rbd~6kFr1K@p-$=+OU(crEj0sy<8HJ_%|L=>s^L{g&A_jant@-e zmGr`6eV>pU%E>^j3R36vL*-;JvKDn)ax+?2U#$p z;z`0sdd-TYPr87ka1*jR}_(@bUgfO9rhjy9B3l6Yad~nIz&i}L0&_l%3cSm^4YhXm?Bq}Fg9(nM2*uG1D4$XdKwha=7gC`^KuQ)S^0H>wA^Uu<8jnK}n zX!z=J$FxozR>qZx(^3roJ4)9>OO`!S(68OqC1NQB28=slgq>sA1ca3`mb3%N1dG~I z#sLiUsdLXP1r`~`5GiPQ+vb+fMgVj%E^9bz0T0sNscFbjm(e2-<|r^2<{XbvH>6k& z&{s!i3P<|}1VZr2YNu80dI8sk8p)K9DL$1dtlG&VBo3XV+EJ>VBx$p+*0O*Pk!FFS zo=Qe5)y}KhY@k|BY`Xkv%UC$FI#jCNvjhqZ<<|{VJRg!%tqlgtXxInCuA;xNxkYAW zh%|4^-I$Km6`pns{AA8s1%7MUX1qW12L3S*r(uDAOehGPhD-2sfW`p$+psKa7ci6G z$+ruUD%r7SnPxOOI<=^nLDJC=RKXDhA|lQgK@L=$A}BRxRKqz5{yZ;u`BFNZK(-*a z!pGGr$BGc^q?ku%xs`|WPaKBC*6ga5_$)ud8%=8=mNhuqQM4TR!HL- zx5q99sZSV;4js~9fHiCgCO8F?`F&$XMr#<*8DoXk#(<g_c#4xN927TV6lWb}2sU6<7ijGwW7nbuTT19UPU^~XogQV<7!bK8 z{K}6o#tw(I?8#r{@aZ2LvA0^;NUdL#AzJsRd7Dq>kE+m5O|&UBMGWwK)c(SO+NP6@ zPqa7(HGe+g)NM%^)qi$4hjvM|xTYkp#Cy`(p2gmu5ZjqQ%g~4|;H<3e{)jbV+<7EX zXixvqQ#>LM8PV)zsV8`(SZq!nEj~QQBgIZ)QZF8z<46eIz26VMB0d8DxiVed-TBxBT)~2;*<% zr+@rBdk^R}KmP|Dp7>J}5tOANLT+q1dcQZ7sd7grs8}tEBhi8`yA{#OY6t9lUuO~Y z{Xpze(n6EI$U=bgB>j=z0k_SVquwMo+tFBU>gYYulcXT>(*_%vfu8)p=V(8FhD+}c zMW0HdMRp1DXKB2^aDd!&wq-#h1-&g-o)rZ5dekphP#p7Iep`?nXC0Q$3YvSl|4fwj zoqeMF;J(@b(Jg$7h^{_Bbl{eV?i^biQQf1uLUw0$h3+2F6~ZIMQ9*c9Rl@642(J%L z0lg1MkzOB!qP;%wM0|a)iTd6gU@OS)5uvfsA9$=o!Zp=8I_64}AB_b1Q~yDzf({$= z(EvfZMglnox-$AhjV=OAY6tZk64XghMuVhz6b-sU3K~rAaB-T52I~-&cJ5*= z(H*ue@hBgI@~g;EbkQfrV*Ro0T#!*4x*=Vh(Wgj@dINnLVMaRAgj$Stvq&o~?WJ{J zmv*Qx>7|dbsOSE{{OM}K0tn4SDkNn|NhSW=1-3_ zlE<2n+`rb?899=zqExcV)@Iehc4S+z4dvQ$feBtVk9Ez6JWEk&4bO=uiJYg+8Y672 z(F@ujf{2B&K@h zQ~KTR)3|^`y+Aa7YDmYHr!iDN$ z(+Jy}$BD4$V`;-U;*jPxQ~{b+bjha~5ZMWl9CdZ_o6VyNYr(5cXir;7+v{}4q2d}m zJXJWmTvRm`L3~ki%S7b}60^lIN{Ah0MP&zB%DC?Kah5V5CII{aHT;rayE#-QAIGwpykPzy{curUXRwiv}dm|WX47t<{cha`K(}TCAn7nvwLnKgl{qYcl1>vrboSI*5ZPtHVIcty zMK0IBWjrIwxdhruuEOM!8b#;rQfI2^)drgL+Kj_oGnd$J7Z_zIvAF3Yh0v~fjxjCj z+lH1*aw9ZhOp230nF8sIP);R>dPwFVn=m8r9Ok2NL!e`WNuu^@C6#svpV@|fHXBH` zu3$J|*<4|xH+sJxpF^@%N#Vu}We%qbxHBq7K0^XupCZwTdL-D~ViI=a&ynP(&5tG% zI^(R80K4q^fH;kyiCEn$G@>WHqHz!gpi^c?;{$AGelq*@&s_YwU{^N&Z`yb#N8%ZC zXq?ysRh?FL`Mz}JGuuD-q5TIA9zDGnexQAomV0;}r&a86OsEItQHDvH*plu-q0&-@ zcBc?5b~A!JKAf)I{?Nn8+D^gm|M2$3{3{QAI9+=upJ$j~iJGzY2cE=UmoRQAf>TWQ zE2S2cDewck~$C^zwaOo%1%m8qhNw zY)98{L(noho?f62IQw*WI^_G{ZLhlzIBbBr`d;9~?b>e5>ds)iw0KlA z;9Az$#clHz4w%kDXbh{e4Mz^Iw@iE%wVKCaHm(wIspVgI8#7WFh%0-u#(R;X|C;kW z=c!XBHKw_n`jGNkD;;@e`NN)JUzWw~+55B{pu`!&Bak|9W}a zkn$Y=DaTN06pbk1lv(uQ_`sxYW*lq^nB;WQP}sW~wHqUBO;Y37Ld?$+^f5_gpW28$ z=$7gB8fe?tjHtA2Q5|HEZC_C)+XQ}62^gN`0oh)w?oEc^gv>CY=Cya)_*7GXJG#OqP8*`YQehuXXQ@xtY%il~ z4FY8|_++cn@yy&`;tgXOW67-JFAWTeDj8E-7Tm7h>}ii>YqNrc5s_HwVvWlbaPkUh zrMbY%SRHlG7iHDZ00lYi{2G9)GdaDtIgCa)s#z_DlUSOa<(QJV$#&)9PGu# z2inB_RYYJ>E~RF_7tCk(!wTm)y*nkMn=Fr5^7qh249}qDe%UZw>J)S81sjz@ZW#+@Ba3$|NQ^{^xp^jAG84SM!;|2 z1y0%(nKUx&D-AVRL+K5E74am=XrnM8v}yqCpK-Gl7X(P4eg~8^GwI zCZCyDzLv!q7al_pN+=Z>x*@>2A&nnNm~?GHgf@E1jGtf!VkKjUH0cIZrgIqIV!^dg95nO%&CLPx1Hnn=f{{G`$mT#xg}_PX0+2b@46ILTZvljon=9L%`>+}5UHwT1N)^|zff{l6pjhh3Vx&fSIF4&ak-?TZj2=ijzD5L*LIG8f#K=f81taHGplG8gQ~^Y7TK z)i~uRnG1I2`FCy(cDnpK9eW?fP~HB8dwVMExi3}Y3)F9d3iu-(isSnwgY&{05b(3Vfl9Qy2_o7k2>`pibbiniA|S2;`x8gI$kpmab1Z4 zh9xJa4t&G-e_2(weDHs*6G|iDbfrN$35CJwW$}eCbGELAC!Z0QA93T?K!m`A1gS^3 zTe^j1ktR;*NWA1qC)uA5z$Y!OVB-v&P;F-PM5<3a5TR3>C0m~?b^to2z)Df5%zB)fjAGKu%9Xqgu)9btq5_19GOi~PdA0dW@5zH%npXIY;nmg@-^Jrp5sKTNNleXYC&?zh{tBrkzTDv2&oo=tRw-g!ITb%7$ zNv1K7F_RiiWT|n7-Vj_}tVT;43`7}L+=|6$Zmgv6QC2BzMFTuwR$}SMPTvdC4Y^f{ z^`mQ?Rq9#;tyexRB0)X#i%$m~w30pWAxkV*c~$1Y(|atNDX~)mg7|M-VY1ZE zz~#_4He*8zw`)#ykVO$io*`PZX0NaO(GUJfT|5ysEjJ}scv=ytQ+$wARLxS!l}ne* zyDIrlzVHhl#(S=-WEN-Nf|9@Tr_cOEZ^@Ofv*aPy8@|qe{JTH?m%Y7FHP=Qu_x0iJ z_#4EV(LyiYSj+}*Mzd2%WO!S&nqtyZ??%~Qoc-}nSxt%C;_xP|(y;b=Yx+~&H5E~5 z*+x7>t<^2v0=+?`N!UZCvH*#)TyydHv5}I$Xs=Prh#Ga1>5|PgGIh;8*xlR<=l=bh zZSGUu&Gk*pYpTBMuIcL@P@aVRfclHy{dHHR7b2Ft1ttHX{*^Z+R=NwN>m^bC+si-M*L`nf01Pbj z|NY_*|3)v%eI}x-f|8LkeRYKgHq1Lz0_MuTh-0z1d>?eeJrlfp|-Zh3l2h8)O%**<~5DV#zYh zwc)u+W=Bl1N_(NwMqQkXUa`}|xHvh49=TixgR*0zGT&WCe@hj3OlFqRFoes zklpO7ikPu8H_qlzJ*pdmtMEL>j{2K6A}zT{SG-CS@C8g@92%he zsrhTdfPD~bKhf%-91A_Sb|nwp(EQ*6DPZ6|7`bNAhZk zJk^#0Y}R<1#JHy{{*CI*!sYPZr1pI&`u6Vzx3ZCdYH*-fleboJvB;7pt=pBIy6nBJ zw$8rh5EWcLlJW0LVB+;#rEMp{4Iy^kEW9D4^$Em;R~CCWbSy9&RZ!_JNkdl?dX7RL zF_p(tq5>Asi~{rNq_?8n!FEWx z<}(;1YaW;Mf_(1ADfkj7O(wMTiqna5tkHQ^pP}9O*C#_em^6Y<57LpyBezbo+L6>< zZVZt+?76+1Y+!X#*TWD{3u?!4Q$8o@ifm|?dVhtM>_&HjB*4V;xLs%aK$KF>aCXt+ zKa5G(!?&lWLW!W4{pS#=3=~FirG&$9j$08%AmKT|n`} zww$B()I9JI21n{$jGQXwUfzKR?~Iqz(1 z#lq*r)IP_SNS~9Z??K)Ix>MU6p!T`w-ZspPGKU4mFW!|&?`kV08wWN=7zcHd^&ANc zL$f3>=Y$08%#biWnIc+mRZ|LUP%{Oz!(ET0yt7l+wwScVT_x8=ce|^p>!Lf|ReT*y zVs$?oL(|$Q9`%CT)T zOI?giTv0Y-!X{nsUfrozjgMU)9P+Oo^yvYmyu4#`^oTBbYi@J2pUdoUYhG(<;4M4a zxjf!t2Zz=&YT3yS^x4kkbDa!lI?$IomuEW}Uh7=G+R5-rajEYggh45N+NaO^^c>t` zU9=62fn*2jBCXhnRzI+ss(E>Ct|JKcdk-?==~$?$Rq zdaiT%N+-j`4s>y}Q-{&KW@^|XVo2!`pPusRi$1+TnjI@%$&PdiI@Gy5-N|sO13l5X ze7Te1#SZj^&gI2Uh6^3&`Of8`vAo*ZBV$El_7|6G#3QKfN{{>WIiH^O>1(9fiB3Vs zi?_3*#ia^3jPq3XLMP*M#nbHB;!+t;D&zT1#uN6Ey`gI~h+GPqS0Sr81sS z#&eyFFBea<7mG_}d_fr(CyF{=ES_c;ic4iYuZ%}J84pbqjd`TFRL1=iWIWZ$c%pck z9WO4G@u)Js*va@p@icp`xKzexmGMF+y%U zQrQsfjekX2uPT3Lm=}}s$9#f79G}|6vH2092h&xO)11t$bX$9+lFw>y0O|Jjl#=H{ ze6C4vY_C%C1tRGyy_wwwB+rNJARyh*UIVRg@kMLkt-CNa&1Z{Wt>Esr?}qSJ9}(JXzHPy%FeT$y5(?JJ1!$${wg_e0eh21APb3iDX$1bSqG%&^=I2 zRmMg!)&td=E>2@T(9J+GU-dvY0mU(>2l_2QA+;W;0wY8DJ&WPA=;-Ks92tEnbaeC{M@HvDM@Qf5$mq+V zqoeyA8J!PvI{H&<;e1c)G0hF2qx*pN)6w?=?Wd#n0PUxv?*ZCRNB08lr=xcR?Wd!A zfcDeTcL43Dqq~9j)6u^G+D}J!0qv)wZwJ~>M|T44r=#0}_S4Z$p?8;~qgOl~z1Pvv zS3Div@95})r=vZNj{d^a(OySKuX;M#=jiCKJRLpY=;)%SqaSc|^f#W49&~i{nx~@= zI6C^Or=vyBj}{|GNFVYH=|hpDr2U>F?T;KOeZ;e*k3^1^4tSn)Aj*m9qc%)F8un1r zN1?F+0_sRw}e)6_no z{WP@~Xg^Ku0oqSf_XF*xsrLfyr>R|{sYkqwdqfhP`$xTydsNmj?hkq?cQA77=`kPxVsQQ$lsW&^s=0Kezg% z!2Q&9OyGWg^$CIdsq3h~{nYhwf%{n&s>~qz`k27|1a?^9ehPa+;C>Rrwlavu9uv5q z$gsc+qB7i$2ay@Z8P?%uQcJ37WP!HkP*zd3_nK7}gm5EY5tNi9nh2y6La*7Yhw%H7 zO7_BdIPy@mt@}@A)PI&@=arhwoZv<%6eG}1nRPJ>!T$hmOGjn-#=P6mhwIKB&!&XG zm+#dG-s%__W%*(mOqR;}4dIQp2g(IJaVt?+$!vq0FIQSMU)TssC6R{5hql8}snAx2 z1wb6gIv-`5r86yK1;bRY&$Mm`x4YJ$a_(R#AvBt_;32IO(gh83wf|?pSl|eDTOquGr#!P8#6)yji%e9zyA+&IWiFFJ zT^>8WJogTbjJEOc{kLK9u3=1PGBd1ezL96XmXEWAW-s%Z=Ds~`3k<<5$7K&y_ATt2 z-lEBcR_H3begD(V9uEoDP<4vBoy0IftcirFgU5o3DX^gCg}cGHT${V8L3ir;AZ6wy zV=UJ)8oR4zswvRO1P#{22nxaED@R0iq}F^l1ed23<=w!x5Q|v3;L$uuf$SB>N~a>@ zJGdepNh<1l&2Ur_v`eHws&|^+uVmGIS(NdBH9YnFbXvcjttCBV$5`J&UsUzl&!OKJGJCr*{dp>Ue{Ns)#y% z(%P%Kvc{9wRTYhdGk*5%`HZbTR7ZwOrT=fPbU76DmTnIIMd>UFSWiT9@^?MqN}p2c zZZ@%ZkA2AQxSHCxXOI6RL_xqq45B@j6$?q+rD8f3(6 zC?Qfl;tr82xd)NulFNvkGcK>zcOg`UsQFMr ze5_Q;B(ZqrvwPXW;&xd_+Y;1;(~+W%R6$!s@(vCe?SmchG$ zP?49WSA1*0d6E1CK@w<#77L_y_g!+5(dFVlfA=)WL*IHx!4N`&SPtc%S~@?}+?{`N z>HLU=?)-9PSfW0#sqXx2IbSM2cdzdN(0r+&6 zOX73O()s)FxoPSAefZqCbpAeku3tKTA3o=n&fka6*`@RM;WJq}za{`3RHEt%MRbBt zxx%4nC0ROHyv${xUzO!@mxXO3HOVTgog`@(+))zcV;UVy)qWpxGYz=EQyk>?RGB~ zAH=h1B*&t=ToQ{9ESfz0D3G+cj=5fZt+6{9;bv;@o;`cmzK`M%BC$2mGRG(qZLb>jWlWaJm@b!* zcFI7sx0(4dX|obYRKl1qVMVEgnQ{qhJ0;*nVc(ekvb#UZ?r&5X>4rf85$OKbx{Jjz&7Nf1UB26Gx~3{v$x51 zYzzIVK<@G}EEUK~wJT81QVLI+H>blE>Q3S7hS?m5~UH;~7-Q4YOW_2^`Z{~C}_nmFb zOzKfOd|>}ni~gbW`ub3f9ijs@sp>Al@IWuCxOgZIWT^wO zxxc}rBL4l>s7_F^VZ`tqrumiLQDi|W-s(Z5hzZ{egGeDRFFlA9ZM16`Jy04(4~Rzc zVYC>!ail0lHjo|^(LGomNDnj3-w-@(q{UETIgQ(oR z!R--u%aB&3^rs=)BwT1{yB=_5ZQU^~?idAO#OQ@Pw3{MwUD<0g5%M~GNs$pR^QBPL zOP9jK5&T&wMOXH=9DEtSv&m)mDB>z*VM8f~Wozki>AU>WKR@F^$ah8dWM%k>-~tclr93a(Ss~?piLfRsRM0WU|bSO z`S;ZtnSt2$Z&6fOD`@yjk)1+T!}lz+(n$PT;2yRlkg+3jS}J;~!`B@1@@z?{EjGQJUiVKUG~&0VmEhZjuQtroGlSeTOYBR(%Ib4oJ~ zAxv$eU{bHIGKHaDD-~{1U)DzVkz`aPiy?H(QjU&6QUzL%K*>j4PK-xfVj9Avg|#P% z4XzaCgk(k&6R633PIRwUGajt_aNvM7!F=fHICTo_2L6N-P3Z6%5;^cVFvtfP>qjAs4;{k6&XR+a%jH>Acev0v2ja zfQzf5G828d0vyL%<``0UBhj?}g1S$v(Kmkm~nP$hVAp4$bT zeyIM?%!wHpJ7_W#6=qLNJLz{Tgb8CsYt?AyFWND72u2#5zi4Y2BQPv3iWgPlfCp3Z zl^FD3_EHZvUh2WtOFclAUE(9)=@L(1dMW*dl%bh)ARa%JOlK6C7q&D*Q48HEJ&X05 zupQET33|H8j(E}7K+?!VJV=KX6HKDp1wX<6_qs}~5JHOoJp5!nJ(914-m=-!2vDYz z)`L9IGNe?5_&?ZsvY{dpDx){!lHNO z?uxjGrFpv}U>K0;6=8N2P#njRHYUlbev0j`q-XXrM%E{GY$|k z;)J!Z$R?E|VzlaWYMRy-eG!iN5rA)xIt3uS%*HjBu^M`PfQ@kj;Wc4xx{Db!RYnW7 z*Fd7|ragjHAmKEQ0I}tjZ({L*gSS(RuXTAxp zBh=BtIq5q&RWyuW=rji;syPfgHNYukR&?U(pob}SLlE$s({IGm_o?xhdfES z^;pK@kFwdvBa`YAKhfyLA+LB+D~v&osJeeuFYIlinxuNi^jI29gwe#rM5Q`Wo2WM? zCYs#vZ)mtR;;u$!8R)rI4ELjLBgE|%GHbuqJ2UC(8{LE~&GF2kC2{M#L3AOc*cy5EFM5zV8_ z6!D(S2LH=&tOah9gb`h`V1iR|~4I1Q2>8%mfEe-O{Ql)0V94(g)KS_|@ zWO#dc_w-m~Rse7@D}>DGC(+Lod|{$u(R&5bn6q&;OW_<@p*~}4tA6Z7jQ!+t@7SY_ z4z=GP!rZuoG6#{wKWbA&o1-ynl%0+kz!+|F6q~(B)7$Kcr#~BHho0f*GeLHeG(Mvk zx!5}nFxA!&KfvUV%lOBdnt{qN?dH$Vh3Wz(Q_qQd%K)2D%bpEqtBB{Ab=xK@`&f6U66{R4S>Aw}T)LM_c@^|sRs(y+H|yCI z9A%MR;XcGDIYHeHL@KG`aDKH zXPXEmM_m#01vx&jo7p6n8-nLmd;05nRmkk~Y7gB-9!xH3v99XO>SY1oU4oAlRb8rj zRv9FMRcBB=%=o0L@evqFL~9^dZ}%ecvp!OpH6pWVV9d~>m{7}+I?U16R!?4A>8qT&;!i^x8v+`tp5Ep=8h-#+ zX=Ec!2hoEjylQ$_Hs`2Y(puhhW@o;TQpIGb^ks7Z$k$yqLf4gzL@#yfY+iQqCdy`0 zlq);wDtlvI_WCzb_FP`Jj1M_6uqG#D6*d7GTsNj*<)<2BwV+aoLKsaY{w*2QPZK8L zFW9-CapqtAW4c)6xIcrOCFWl({;LMqh49YS-C?hx!=9*8RvM4@TKw2J-&4kO<&2Y_ zv?s+R48L$_yj+B76%?~Wh;ytMqGrUKnvWk}M%}V=eA}qRdqR{3h?rOy|FEO_m+E;h zzF04HV{Vm*oV815RzPxQ8iS`evu!tVouSqaOD|$`D!OtmdYK0zWiMvoak`Un48r85 zV;s{XGmf!x!xI_D;O5RMQORDeYnzr$A8brx{GyB~FT+nC^oTOprQ*wAKT)pJQCNTt z*1Dyu`^EcT827yY*@mm{jr!ImeNy$^RIV@P6eOoT;#=!s%&xv0%k_O03%1pFo8v(Y z=5l?(pZ34``fe%eyY*|s5bWTNrVtcfE;7kEm@@)OJ?2Ehbid zCL*7o)``{_0G-BB5IrI?JJq-(nK@?kJC4jwQq2J!A5g@}#(aEn)YJYE@~Jav+0jO7 z2$Sa-dW+dSRpC?sF3dVnb_n_UA2q+aHyN?&_Nu?!S3Pk2=&Q7Kv7eLm^n~_)^3j?^v|9PQON$I&hn9PQN(M{_;@3Xg2ei)y}J z7X8ZWV$s?8E-7-p{AIY=04X9u$5YNk(1HBo6c;9)I35?lrLlXW3HeI2&9ZLiS2rq- z-vh)YPt;9&xw+A;`JAjFXDqS@tdBg7$tcX~EVIHdcQ0~77BQ2^_- zE?T#O1o;Y5b9~0?5sUQ)e&TaMwk|tK691MK?Vodi3pv0m(^?!6j>LwjG!sTOt+9|r zI{T^;{|C1KguZQyb@@Y%vhoir;7=1VPM0fnP1ZtRqj`F6{BGBx)F)f0s%XY+_c^VigNQ+>QIyuL?95kah zZHbNiI`q!+L(Yz0Pbqt@bhst9K&M_UO}sE4*Ums+r6p|Dg(1715km%aw_lZ|?QSjC z;!Oo|@`=BSMoE@?E|KMBvK(FBYQ2zF>JL?%5BQ=2mg2#RtM3-PH-=qU_2GoA@s~N^ zi~~+lGjo%P+9R5PAgT5exS6}kwuI9_btnx71I;HbreEzAaYShY+3>*N{5RUw{ghGWj+Vin5{MO_;{ zh)Y25&fV!4!xCJwQD$-bq1X5Tr4z_{{C&yz!ZX|Bk0i}!w)6If(|CJ$V1E)X;z+U} zZz1&IX^!h52G%K-%jIw1h(E=WIO1NYAfLxs$iAKh@h?{NePVlx z*;G0D7cMcJo#S}PVcf^O2gz{uiUHh#X>gO_>{SCe|3L83=NCC9 zZ8$r@_n;8X=;Oz!z+7}hPT!;#8;)TPud6IJ)zWp915GwMk{;xcS!Fq_0nS%l5DYwe z286hpJhqD?l%hcnk0b& zx5t$`&8A=y+L3HzAp9@iV(wgYUoRP`GdBd^rJs9*d|!;Gfmszlr=Ruu*{GjQ`q`3W z|IgkK_M5FwA%~YgF87t))9ngpM|-9>v`ZI9?BF6@9YS~7>rbu!1+PZ|CRSf{Z=zFn zng3X3V{3BOdtO6U14^j|l+xDb=#)}$cv9&w>elAyS$BCEmm2UIaBX>lv5lsdYgS_E zR1SSTV)@NZBJA7!GtaUx<8F$mQ3*04?s=gFe-xY#m)5pP0)#1us3f>$B5Q}@-?71ZqLz*391*c1Z#0X$iBRH< zWp`wy#oZj1G%5gyXOmP8Ak8+A>V`9>_{>NWY#$Z(R%NgpVTXmNOT=$P5Ahp?i$RIf z8TKaEhBHyTjgMh0RBmV(@rQ$mKk_6`Y9vo;Bu{E2PwGgXixSC4B6&@k=_rzscAdh( zV+0bMVx+30y3lc4pno7Cbh;p<>~+ytSCbn8NFM7e9m-JDgdp!x=Qiy-wB(d+{TYF- za(70k%Dy*I?FRLxto$MvH!9KXF!ghq9!o$rDv$P$O4Ub=RwyVp^i)%;t{P^|bk1W* z^tBqsj7!7X6AOoFb5~GHH9O3>E}BD_)@pXJ3^70F0|i9d>)<9e^W6Dbdf))&Z?1vc$_?CBZs2LQNOX_A zGQ>5pfGpWYa+SEt>5^0}L@3&xiOm#Nk9MTx+&R>^TzbZjBF*NIRp?j4QiEACMFZQD zJxAp5KvTFxCY~t2N4_{2f>VU{M6h=?{tT~f2oSz&Bz%Xn3nGiIOBISL>)vF`JQJxyv@GLJ`~Wd`)U-*C3_~-%$hIJcbD_ z%gIb%twOw;SI9m{g-)qLHp6(`6>^$FsZtsRst{kYDFE8OHg%vjXbQmGZ~G4}E-t$9 ztJRxd#LzQoSe1sX85RmNDi8CkA+Xu{z40h!e}&TymPs`RaWZw8$-*7#D=%~u^nKb7 zpN(U=AZgqN5Dt;e@_UVffRUz3cgTn{3jvT!r4_bW+mY}Myv#ARl53^UJzV4-ak;UH z<+(9Xy^jU7dCsQ_ve|4Z(d%REhOw6@M%x6~V(;#EBx7_7g^VaE3K^c}L@oEiXr6K0 zWgP9w*d*MO%g}B>XJbLJ4q*Xc2hpt}@!RxsGr6aI@N@$4@mM<*dW|%VoYk)ntsexE_9tt=0q_{mY0s%cRg{6Zpc_LShbU@*lH^qtx=T3Dv^|8uD@sEHwZ5*#k5vHp0svP;C1){~ zK1SpdwNPHHh{r(+lBSUYuIqZ6}F ztRA#}Kr6yJ#yOQ5$ZBKl7P0=0_CW*ujyH9*<+#It+zr}!WDt0NC(pp${5$t(nQEDt zF~(c*7g=&T8i&jH0Or|Z*Ce+ag4?s21}MULBWNL6Z`uJ%>1$w}e;nwz(uyJ-P%BQN zgF7hrEPHb>48@SVwiNTtIrLeMVPxZDWw$L@p_^g4p63>HYj|t+pqaX-xrhz*+!t7h zR5N`8f~eV$tFpjnhY!AvcTtxkHh&jovseJYwlS*5B5e%j7dBXAdBq?sG!+oHzxzPg zfgECiGs>7n7kQ3SJ;I`hUg@VSaXldW-Rtdv$xdO`10$XL!95UkdT)t$tp{v-rfab4 zflZylT@Rcqwb%6k>u5#mTn{`_YOm`7&eAFJlzKqxU=R_H9GB;c2pR%dFb+~&Q?enE zkOpCkP@(2lN}@nd?PZm1GP>GdA2Bjmx`?saAx0R6&af1HAu+f{skjBwZ{D$I6y}u- zDFinXdw~Gk+9mJEmaW(cjsrtP4v^RZN{)0)bXUy?OO)N+9w)7M=u9SFHAnV>S@D zYMNGaX)hfBYoyIDQLA*|Zo+>WSkO~bwB3V@Z=%f=gwG zq-#DFNKu5Sm_v_@W(%Omww6Jv)V2VgGT^h_z_SKC*9|8jFya@mpE=q$F0Rg=Q<4um*J-G8B|mKtP+H^> zCLwCyqi?9m7`YVkJIkv=e2`g|-iLZk(wKwd8p+e0}1NSHyC7 zE352d#=OOJBsATu2FvHg__5VJ2Cxl`7JQKhJS1-Dv!bLDhk%bd?+CYE7`Aad#oJ>MOpKPXph4YHD~n1rBx)#KemV>$)T7%t#66I9hcYPTm7`hN z0JgkM6WW|FO#3)nTk#+?g=)QP+D=8|AZ$$_R0}2$ni8Ay3GO~xEk`>iM3TNe!sqtzJt#97jZrzvvIlpgDaOePV$abC<_^C5^&!OlQ1(@P)|o9(iQ);&Lr?y$ z{|T~HPhRVNg1uBaPXluUUxi#CoODP`jssmJw^vYyAvI=rK{>E@GVx^sOPz)adZJDX zJk;cy#t^%b*{&5q7$z|T)&^x_z&mQ07z1KzQmv@RFEP()^E_*t%11wVzM;iS4UzyGo zln)U)iy=Zmz5OFh7F9LErW1=sIzD>I9_kDc(+q{QF%1z-Uk8j10;Wh87OR5jI039P zR>V2c5|dgnRuocJLk5YUal%t(^aTDEWqcu7 z=7!2RTH@;-DmF^k6oA4z)@H1rLop~|8zpJVVgZ~2d2#I5{@6KqWCys)mS=|qcw(8e z?U~a{rrwkcZZX)jbk_RZvgk5**7zCxgI9QoEq&S|O=0z4Jk7_RNmnnXYm(I;C_%sd z856!AlwSqjBtNtq#372UF5y0LL)JQiJEmKl?6>AYo!Cc!wFRB)r$B2Q&bA`^LEIu& zWgO8}*+wJBxe~QZ+?Gxo<|oLwD;>jDkW4(2u5pV8$pmwT)e;-2p<=T#bL0aeGT z6!w3$Qq1MFTY9|v(aC2m%G=rJUm+8N}#c^>&xmpG9N&un2v1MXd zEFZr>L}mO}W=+6^^>74Vt*0&QU~C-qVrrhVAf zaIdX$J6%uAYR|t`1K;sJU+tC(wL+w0s|1*!v_|yGzO0d~T}ksMvE+B{q4t>6r8BVp z%HQa1OnXvk?~H#}7TjE2&1noUS9m24MyEMvz%+C;eN+qz9Mu#RQ=n@=Oi##Eld=>A z!`4bnIx2?E4-DHg<0(FBJY*#aR(a(bQ7(SSHIsS4Ga|N5p2R~WfGD`b(+QApOc7L8x7b(TkTPL$`Ksj4Nyo#XjrWJ2yI1n zMw@b4e=rfQFbnq6QL!w-;DKhaL7Xs~S)$965p+vBYayrc{-{uAZSX*c;}WE4MK{wRl zNv$`>dg0*(PY*jCJ^ZFWeff5W|1%!{zwPn=qR0R5di=lS@&5-N|2PJZsnkCT9sbXH z{C~;g|C~R9`OE(7<@0truHN|50rUW^GvxYu>OR1e{owBfJb-%M19$-Sd=KCO)N?Q3 z0o3zuzyqk~9>4>rN5^mvpq{${51<|$#65s|?gBi3dUO=`0P49DFj7jC(CEeU?L1>~ zd>{?%RNueiX<(P5fv@3!Vo4!qdR3o(BHP)4)Yf1ApUb;5APJU-dMw=ub-i zPy}=2Yn-DE54bku{A|3-uMIgjdj&P*>Z79Po?-FPkY@vk;DF!*sNf@l4=`um|t}O1K~Jr6}RO zJi8Pn>=FhzMcbr@U+0G*G3DRc5Sr4Dc7DBo*F<4PYEXjXyLfv18Cusf)AjDV}cK$ zg--}RfEJDlK7bZJF8Baqz%6tjHP}(t1IU4}Sp(_84!a&e5Cq&BND+42^#GC}%Gaf7 z;!!=k6j40F!}8hJW~o8!6bD=DF&d6kA}-pfD%_M$!ft(ZBAR{3jz&6*2FQ*W={5PE zt$1a}&~Pm-Jn9st*Bio5_~6TTn+_CS3&0-XB)|%GHTz9cR*tnlw>^du8Ybhc^r-?U z_2_82V%xA5Br2Knwt%jPRAP&I?UmW8b~CSneS#gqb|66-WtT57WgxkY>CnPhtrmnt zEN*4f7zRjYNLWE#ZDxXO4GC%$xn-Y}4)(uWG13(|2O&FLEC;fCJ9{|PDaJR1JLzs) zTa#h8!l^u&b9F#fV!hB_Vbn3wRQ7;}xCd!!NL|&@sw`(KHL}a4K38SZy^Ocq+O!_) zW2|Y~$Ml6o`vS{AmA!3VMWKVa$b^3zCCQfGU@2KszSmrDO>p^YFwYSd?YD7x!9l0E zJnHItIhRM=y5T{c0cJZE13MWu*=5VMi$UOE|_3C{5NjG!B&=QA&CwGC?v`t^o@jY+gihLhE{Jm;EOy*AvblVS0yT35MK&gOXgeWF1> zwd7Gy<1^vBBQ7pwxn7e@$_;Og^UISy^@0p=QL;kdC|POUtKx9ew&SG;a0whU4!*OB z<)4rYYs+htylo3xUZZ5nV%#KDX5@xo9YwN!avA7yn0ouXIZvqm`10u}E>t%~s)>?S z3R)H=mv4@4Bq78*_(*214{jxyz9HN}LPhYL+eD!(TCetnj?)it*HQEDkn34TNz{$rj_a@nkBrXU+l_44b@Jc38I%gokyZabR ziZ(xr|1YY+TrI_ghK|-g{ju!Dd;rUmMn7*>{_c?spQK)OhDoFme$>3jHb-}ZE!s~Q zZB+|TZdF?)*QmlggSMtY%ZNSRfGqgjlns2$zMP6+M5NT%hi_fSt6kx?N}D2^XlBxPXROhYKv{ZZH>c1Ctf@UM`^6Rh|os z!Ue4Cf(vvWGPyQC=BUgC>Tm&8x$FcIjm5!SU?Wd@xj+Nr8^8xN`S5%I*Wv+uK*QYg z0S$A{2Uc_G`M?@3Js-G&OV0kB@R&>uMQut7tDA(`|fyrVr!B_OG7siX@RH;NzS!pN8rJjxgbDYTj~ zAh0dKN;-T+dqE7>Rp_L0|gQu zx4SqC#{&>OOPpnW!C6+7IZNt$1UG-jQf3@4;VVhOHrD2E=#USwpVf}Fh=AuF`s29M_3Bl1 z%%R*6Gs$E-#kN(u8kdhNlr+;sJCCaI?<`N+N%B}aoiy|if03)%=ZHvX&g_+Q-u?zct|ByLC>$?BnTGojbEH^2Mq48hv;Y zq1;TalE`W%S8A7jGnrMel4i1Y3%)3vtWs^SqYs`BnT2DLH!h_K$7utw`%KD<0C+)> zg9D0kfINk?<*)!8HvpRreU8L-9MF+`txbdq{27yUOLRtQvL$*!=@na|=asJ65*=sD zN;0!0I;M2pb{)%Iw$ zd$jej@8WCdhA$FcC%Y~?{sp4!JV89uY>}&MMRrPn9}(d2DXtWNM+-!JTpztlAEi65 z=I{;oQOt|3HF%Mywvhn z3t<0OTPftj?5CNm+QN1?(mJQ1B*S}>npPF6tgzMqu1?lGwuKlFT+-!S673;?!7$Gv z{>}Bl=OIJ{LGU@|vKxZ`1@{n(g@j5q7k=;N;5pH0?Rp-I9$^~Hzwxfbr>>cOZfrh& ziBXl|F{o7qq%6oj8Cn?*sfR1ps&~7)mGdos-JYtZiOOQhbIobJ^xWL8ey67b}kn1Cr>!R1x7stovx4VxQ z<1`<)`8f9DTG@_jyrL6o+$PNp!6|gx!{cant`{|Sv3x!kI^#SF9I;8oHo4N-w;l>-_X#i8_s(bAw67}P=F8)YJeJF^-&xws7AFMAx5|>^fB|4y3mSby}C~OgfY4j?4>j=h!$d8ka}zGr=F!ag;aRfUE+OAcm=yA z2!n$$yL6qOoq;Zf3N8#rM~DYBhA=$H(nnx%609xhc>L2VC>YErFxz@5+ltGMBRJSW z-jgJniNh>QYPlu@X!2UgS)ruKbev>F?v~;~D>a9l)^#<#OxMn(V?Mh;>}UElUFUWP zGp)}qdeGGlg#6AHf5zo{y942_njP?<^$rC3eG#~(`8SjK#J?~%pZI)dyT5n7^DV}- zUfe-KNvsa!{wk;c`E?Z;^jAN!g$WqEnRFvPrx*>P)CduT3#uEk*5$|)>J_1;Q%0(l z4yh8*!(46)QneV9sK*7V5+uQL6r>tz=1q=*R6|9c15l7EzHLUFvWIAkq227@CY1Uc z#J`883c@&ax#}HkE1;YhTvQf$YKzd!Dv+UUer7%y?$sP4tG)|A&n(VP2l3~b1L$cU zT@V5Vy*7!|RTJNwx?35mOT(4rK~6$8WZ4ka!cPQuXKjcUzEpMUR@7f@UKhe&q8-Bs|t36$%ZmUggVzO_e-Vd80hxhL~v85fC&9`T|js@2If zR}fe-0dPwsii_=PCptOH<&-Xji^B8^2Jr!&nux78IlhRx%E?!;if1uF#XlR93$Ebz z!n0t1>S@%BFT|)BFbOn2iJ5JWC2vD`dZi0dA9(1TK}`iSzTmVkIIfsW&PW z&sMlL-o)&{cvDj%ui!~PJHfT*RWn>38P{NGSO`ffZVbklf@vo>Rmp$AMUd^A*eoqx8l6ZVSWo)b zxgn}v{N+h56l)HvYu%d0&>&*5K^`eVP8;M<2|@@XavrJYW3iIGXprNjJTDvML!bDSq8IFXL+~s!_4CF_XH!D< zgko-+QcLKxJIr!=rta00AXigtK9sY)mw0S5rW`;#a!1oSfY7o4N}TV40ZOd#q5(R* z6Pnh`r`bBnn^27Io;U4D@nEH=9nG4$b+xifK&DFpW7d#WZFxYpS;CEo)8H zKEjrRDd-Fb5mQ_nxG4R z8OL+NTK+NvLLC*+{oc2x!J?GA%)%D3G?qX5s0x*DAYVg2(n*o%M|C(F>eZ+@8EO^K ze_6I7?YsvazED4^05d?-W%32$r|g6Zp?a^I3NL9dy7ReFPdlF*u5h*?n+7N=k>2l< zsWQ`{9dv+BO=bhQHu3Vh(5mw;Q`-=jGb41n4x`qF8i>)cDbz&dmA}!+(<$npeAsf3 ztTqH<+K9HmRbt8j%451xjfz>nkke{fW6ec?nG~z7>VA0w2!jkhGH$EBg(gtX$SKTN zINidfZ6a^6M=tUmQDs_|cLe=8pRUTb=$^AZXzeP9ftxlo);6aa3d9cv)84IeBoYQ% zpa3g6d@8A%%?{m0rhaUm(sZbx77Ji(GWcNvRxYMK3Dp_FdB}I7X*A32i8$UyIrl4@ zZqS9siW{W;L~%pMaX7N&(>bgtmynO#j#5H~dXHSHIhssspOm97Q55v0fEdu%G7{)Z zj>V|!4ey0#IYr)T3*q7~No4qc=)%BebgAd`GZbln4zosKw`9 zg@!MTNFa)A=u12pM?Bq)l<1BkJkhO|mv{@}9&vi+)fhJhbPP1rb4O9g2e1mK9H+wejLcw02oeYvMxMQ<2jgQv!v) z(S|lzbM2}0BTaaA>8=zhVU)>}92fcVZi_Wh5kF87u{xZ;yhbQ4kc=^MAxJrjSh?hk zJ*~^atA1Oeap)mtKa2@3Kc2L7d#G-8Niueozs^ZuoOs_&N zD4{EtOjhPTZ&3;;sq;z(Dp^g~f~NQ4 zV|S4ab#j%hWcBPso(HAHp%<&9JY`vCVNkO-J0zCIqQd2l>-JEHKD69|0bmN)jm`p) zv$D;~p5X(=fG4&9O&iw#+omDtn9iSs0$DLDFSRNs+eam=kYg9}irA>5Zn69aIGrl* zfEE-%-bU7tc1;{-(UU^9z;xM~!Vw-~j*5Nrzbk5MraIP}Muv#kW|q)?$Y|C2 zq!GRbHvCj%UlrpF(GYJE&A-b?$5T6Tcyv1CaqV>1o(WI(P&FdelRa-zYtCAO7i&8w zH>`iujee`w2Xc-n7=y6|UUmg|Vx|C@JI@xC4mXI;zms~0b3vH>=Rf+T|5Sgc$#(e{ zv9u&@$v%_4jX_3vt^%PX8%9THdO>keXs`T67?h^6*Rp@NQ5)fZXyZHieRSg;yVX2R zYbO`@B11ycum**keJvlIRFtCf(X74yDF=01WJK}8Gew;>s5^IgeH5z|S&SV%JE5`q z=x`vODSKg)4stSz$liSC#dCCcaxd&}w}r!+(q5ohHm8l~GyNqYYnVyI&pmJyZg3sq z$KP$zHfnOw-K`-DSgCDZz4jhhyBpnEIag{mpS?AfM#GvqTDf?kkinp-k2QM3p+t0tT_`+y3I zs8cn~fdCY&<)>UVvT|S8Pqlu=ePM(|z*x>HzO6-0 zVSt{OdHOC3Mgc^QnG``pBQ}&UB6!Bqfsn}6CjG4UJP&>;m6{ z_|o&8`q`m{azleeo(6kOizQ5o8wxvzTYyFv5#D6nfQih*xZ?Q7=`zhoE#wfhUE(9C z8RS%ifue!d&+sD9U~XxeMN5b5ZK=2iUxpmzTQ_XWM5k38h=yPScQOuFO;e&p!w`fW zO9n93W0--p5P*R@YKBzppxTjX|C^2z2jztS;v+@_4sW%Yy|F2)=bi9@Zj( z&~XJsfpqF}70MyZbE6xtLMjf&Isa|S)3n{#sj#lDdV)Cj7 zi^wAE`q@KCgCKnLo|eOigtQkHSu?-c;5LXX_nYM{=Jg$SNSO&RaapWNtsaBsnY{Io zm1xJJLW*%Po)t(&F1BXNg1V?*UI`7Js@#-qU`gN8z@mn9f_tX^mcJ%-CpFU^k9DAh zmdPK&u8|J}z-H;9Yt^@uBi1-Bo10r#GwKN3-)K?dp3>=rx%cX^4&Rj-5#Z z7rIdA4|NS(P5a%Kxgsi$isp(lUUeme`!)caEFRAcdc3VD=f)x-(IO$#ckLn-Z_bN`zEij}~b|V1OWQ?CqDs23t}LBZNT$WjdLQ`EW^@ z`!FNObPe&Wf0DV*Z26BOb03Bs{m&tDeM|pI<|4Pg0W#NkMxV$vs3fjd82ZI5j!hjg z`&*{}!ZiP3#q5-ry%)1dmzdSA5P~lwZ6#)PCZC++3o-j!usbK~FDYiR50}O4q!+V@ zbbI#_VwM=pT2y6IJ@eh8;eh$B z11>3OC9yT%CDCM8?)YWFJNq>R@29H&BzV6ulig3Dx&O~0czsL%N$`Fn1n&^qzc@>u zjyj)maPV4k#&%HE)pBc`KvG z*_^RIBZ09pOAv46f)AnaVLs9coiaC9FgKgeu`@SU<6nuq1f~o8)ED}l5fSI+W{o!U zcsEb@+}sRQ=4PT_DQ|*hr(y<1ZZ-+x&CMZh0=9>fr5R^#maj$0+^m%nZ*G=(g*Atg zx!KM}2v`}!jE^ctqPN{fdowr7R*$(kKi|Rh=G@s7Z4QgBb7i8hwfED^&GN32{iiTD zbELfi3UhNs=H`+Ik9js#JLYC{d(uWd*#Il*(K%;OD(*2iYj1acFDni?adELe5(emb zk)1a;<6?^S7n@WYQ&&6E0*uVf79x?WO}fp^&0Jz`=66;%@!(K_P>^F&Mv5*`FS}f^W4hDyQq;EZxX`zV>(ka=vOoA=MA)ej$~y6 z!*bNb$|mo9nVZWk^%9uXI!$7lT~;emlg@NCgBqtl>I&k8Ak5s%<5-WqUoVes5V^pz} z+7bbU3R+O5a%OWTvO#WhW>thbs&=Y`CcL59oC#yVq1d6F9l=A))$W;W&T67;&N^C; zX*Q4DWY{$z9}R$6NJLy{H1I_~8jN5fKinA&<{^P|gInTaG}I_pHfQ@vUNtX4=YxHVm*nL?muD(FooaK8(Zxzc%JoaXFNyb5sy%^^4jYT;- z2W9j|bu!-L+Jdw#y2{y{RbPn81}w`&FbG642_jiSPnQzPB=|6^X5oUo2DIh^4k^to zx#^MH93Z|UKR?8j+0}WXgZrvDrgNx=EHUyQ;8ojqfhVojJ31I7`>d^BVla ze|)3+_@>Usx4iWqHxJ_OcC#SoS*sl&g+E4zpt8h?bBHj+>l_K9?Q0Bhobz=j^093x zw%^)|8uT0|@fD~7W^J1VoMbZPrlSF9b?}1LA8@TlfV+So5Ts+`(6L=SIT2n#>_Uqy zh@T-bqpogu?K11wIM!dQ%Z?JIHdGrp+Z7iEW%->M?!5jzBVK#Av>dSMGV>VLVDWADLWdn z_nrIC`gmoZ2(=l;Vij7fs$(I0lDYpv=weoVGSn`aYeQlevc>A!@OX%X4)9gf%~LEZ zPUzx`y@AVa1r>@Vm5`(NnKpYVo>X$hLV86{z_XiqrkGO6nuT;t&$AnOrYK{{>V~Ww6T&!)i7B>DDj?W-@>!1p5ZMs z+4F28&lDsvS-wE5LoRyaAS#wvvW)nnJnN(VLj=^`CUM3NiVj)9Vq*1dP=wxD30zu(A$9$#k&W(Q$0*%L&NP?JZ>*| z+!D*sa7!#h!!5B44YwCPZi!`RxFwdM;r6Q@w~Ic$9kC28z8$d)jb{+c(Bj(>%Mc!+ zFMl-Z!|g}m7z2=dK;VAtenjAY^zIk9AHN?GxF5lb0{3J113>#R{2(F?VAzl0y+Hdhya#AMhVKX3kKy+M?Z@yg^)nF*4Z}n#G-*SGLX$E?C^QTok_S2Y zp1?;|DG>@yiV&gDFieC(!!QvFO%(nWB8cB{TTj)!2K9LDsVrBEi_?2ijOFFKaM{pa6ghQE@3~GpHS|8G+R`{ zemq-H!hS>(m17X5EgE4zs)^mfcEcHoS}Dn)oU6G~6i4S!*;a8A%%im>1{_QTXf60( zhn-M`=a)tGhm!ZNWv&c(+kAHK?vxptX|t4dI?|R`ofKGc!N!@!*me=njbCg;TsY}V zP*s#g`_TProC=-&8)D`C|LnaDlwH+z=XpO~z4z*U)vGV5q!Mu7MYc;IS?Vkz(TK5i z6%YtvV7$C~F=@ul@^~ezDuqQ!6*b8MscZ|P_Rv^k(}|p>lQN+V;|5!kkRE1CPh=0t zC}~zgcUZGX59!EmmYYtKj@;uobWH5|{r5TdzE|&-3OgUOGHVSi)p_^abI-@#`|Q2X zKKtxr3v1f^ksw%pKYs!kz*Ta5NnA*l|5lz$`nml656_aE_>sr1rI$nHXVx!2Gh813 zllAJymaw`%-Sx|(i?1%68iEL!Vi1p)YUKi9?Q zwdz6O#bL;x$OZZIIFE57QMi-*@QU&7)&>z6NK@uTaPFJbZS^~*aVY9J z6s4=Mc%>_{)fIV{EAno{CBho%+kRWi=fz}-w7z5^>yuf2clo{iHirsR9%hz~967Q( z02W-N;mS`Al;1p9zSl1gEVvram7f?WziF_1(k~Azus*r+^?~xEgXJ6j^2nKF+?Ssj zD1XIZ`FHorL(oarm)|&0{_?@{@9LL_7Lzewe#1cd%LdDD?U#oZla4PxJy3qjVEHTi zjot4I6QWSSUA5yfQ0PH&;ELlb-Lde!y*@>xkuSaz0;u=ee}Ep zsMoq;=~||BEl2!}QM+(g{gbXY32k?}IXXH8cZTnEsyV8Fen4gu>Yw?sjTBYW%bwuXWlx+Pv9{u5(H}O1+tiu6MdTy1iM7-iPq#8&rHV z6zy}G{YvEQMEjkBkHT+eq8pr^kKT_WxidWAj6YcIKfqOgZo4 zotcN$Fy&m&J2MZhVakyS$jn1)m~vzSGV{Im(}9D&^eI{B6qZ8*l|XnVZ)9y0u38>oDpc)u;YwC!-gGa z1R6H%+(948OYhqR%EJy__d0al=g@V;p-TrgD00l9>wbr>pLXbaz@h87L)Wr5+J)!=^g2BqD}wb!Oxt&g2l0nUQC#WS27|Pp4#<`?%_Emk_wD z>{bbb$H^t)+E9xRklQF>P(~iE1RN?WBTv0#nv;=-$TmY|W#p-rOmi~wjFe1s_mNvE zna7-wXLuSOrYw-g05JSUHUZmRFxhh2Tl&~dF#8X?2xk9b_rUBw>>3#U*)1^p54!|r z|6zB)>_02s!uN!?-ahGx0PDZmf3W&v|51w=d4`crmXJO+1QWCSlnp#)_i2}qZg!tm z3F%L$?q&(;E6Q$^kZyLL(Gu3p?o%(J-RwTK65h@3Q!OFh>^>tU%$wau7f2w@2JG%; z_bFR)yxnJ*Ma%9Z0}pa^wSlLbs~Q?FJLanmJZRQ^1JCnUN0aU<(iJXi<8Uaaz(=Ef ziS5zizXAuqH-vLzb^tpefEIDlxEqvc18lXT>p%&7p(AqgvXEh@{C|e_BYz8wKnn;J zJOnsQOf+2`t+S14R2^-E6C;2F`y&duMc@GC$Tl$-t>7SFD;MTlqx&dkB#cWVx*3i$ zhU|S3e=(~Z$zHTO56HuNKmkIEXU;b-ODSW6{B60Lu+4WtsZP&X1CNL4xSe>rsDLca zXNhgT2sa!0?KTD3e7B%WYydNF><~7)cDk`*Ig&qnz58{BuXD_3iuLSrgm=-Ly z&&jCSQ}-F&t!dL8y0ycaZg_Ui{SJXYsj({Kc{`ns@{q7NV5sgr z@mX52{!>1?@Wb+RIYWnwxLj`_$}OIgyqfgUSw%t9=(AV1xmqcr;5vDM3lw;Ho8Ftd z4|Voylb_Y9+Fm9g>CKegDrCAl3=Et2Ly?gvyigH5ja}N^(Y5rVBj0imx;vU90V~@{ zI3Ifh2~_Z*Js2X1USl3s3Mn+UTX@&a6xY)fH&r506jG^_4PZ*A$?O_ z1T3#B1uhp{mtP0ayQ7QlK>>K*;HM}tWnOUF#?FY~_O?y{OI)GAdI9Hy{J;5uBAo{0Zn+SV55ly^VMywzysi=--q~9pQSp z3H!}5+~yGXS6b9=YXkj#YvG}noq}uBypxg9HgPbg_QcCk7B+hd-SMW)7K*vhQsjKg za{Mea*95$$47p64Wr9w>F6=>VG7mka_}y)hf(8Xk&NsJ z&Wt!-%X#*iPHnFsJQOY0p_Ai&+u}1LLgxjYMSYTJj&|ZH$T*ZDMQanm@zHv8`1=gu zD^Y~Z9ml&`1^ABe+0!Gr2MIJck}1gMNcJ|Uk(VXkS|HAm>{|ml3f9Si`+*!PA?Njh z9QsPmYyBMc;uY8?0X0nwW?}bd38B3scu_%Nw1JZ`8;$Zu5Rlw5l8kr%ueL7Nh+y_4 zL=PeQP!*2UkS`R{^OU(cT6KJKirF}Ba`fL&@f>4^VByH!pVapT&gOwx1ikJ=2{OB~ z6Ra_D#BT^y0@)foav&QJ0rHA~9K9;J`uzggkTnQsX$cx0vy7iO2~eB7?uaRB5geXW zN*ri$uNC`EkX)qzB?p;By6D=$^;~2E42(C~$11vtE6P4bu%A$AS9ToIPR{82NVkWHLMydzV&=%;VP_`NWCf z0jDggUThm6QgxYQiT?x&&faqMJ0nM ze*W1pHKeBi#~g8CvMmI(BTg4uJM4QCTXmEx+=*2k&qeeK#h2!}a3>Dspa-GvB6Ano zhG;U}5!|(kBx$-J7x7v3jjAGt3ga0_dbZ3&;v{a zs_tj30b*MKJzX#XKx{cH13Bj{=gdG3RnqU%13AxI&Z&NmftY@s^&{+q*x#J89vRpB zAdU^haQXto-2%kjK8U$>!a%H4ASTU0tgN%l?{z?om$qjfoRo+h?;sG(AQg89K@1OF z4dQ;r8Xz{_@^ry>0kP$*4CI`*oHGMC=Pc*+K+f})bE= zct#N!h?4;jCk2R;K8OS}b7mmtoaLMz$a&s!PW5vP#PsW|A7LNF$vZQOqz~fIKzt5$ zq9EgQK>=hml7d(4qx0ewN(E)o9LmZ%doEZP%=;E{O5Rt1dEaUum;JB}(#2^uiJSFCVP1}mwb&qxg&R#nl- zOg%+?mU@b6T=$x*Y+RSnR55#@ibu1B#)?j48Y`)3tfZ!~k~)o*9|Ia|D2-Lt6w_L< z0HJVp|I`9A;RPl&4E3^5!%!*)NYB(Tr-Y#ax40Zt6#)iS;FKwVX!^tQu>PRWq+yr8 zGP!6Had}uG>ZAIa9~q}N9=|-yy9XW(lfPFb)Y8`Vp$bo8Q9s}2hl zrZIc@TΠ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

kO_a@`e7^2xVDcU)7SxUz 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 B0GLoW*XyhRl;ZnLyB%xWzS9M$qm17BDg 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

    kJ&A&NiJSW5Payw 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%0D*b60~Cy)&a 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 rX0-Ks7U;2u@&lp3|sL< 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

    s<(>gA2Ldv(+X~9qV(mE{o!}_Ci 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 X$%lRT3j(~$z-h$`#eZB1HS_&HNDcm$ delta 492871 zcmd4437B0~neV-aGY@t487rwIWS^4?RAn#$AqfdcHWCI2GHP>`ZosSEbQRh_bGyCw z(|ZaC5FkM002Lr=ff1qt1_Kx&K!5;2gG51%c7Ol@f&>W25H&6L_g{OTN`;}@d++x= z-&cWJYp=cb8s7EJ>s_mEdgF2Xn(dFx%oK-nCPVuund#$`}bVnk}zBK-Dr!_8-Y&9EQQFC9OoNRsH&#io{ z@wnT1AX9G*m6I#i?l}-X&{=C>&sgh>Nu$h5YOh=|X>r2dYh|$aV@dbP>Yc6P{+^H=O!o4X73*y+oU`Aa)2xK$ z*w$edL(I5m$E=>X>)KAtp?YF(cP1}g`|#G5IeQ)E{43>$yV>kl-FVEh_O{)un^rIL z7!reyW#hceW@GzU|J{mDkEx6?WoKEAWivzTe|Vl1+q-q#0X-dVhW7c7I`rIHc)&Qf z7w|1V;NzuzJifEtcp@H8Qo*(otyd42Voz#y9XP?WQciNd<2v-Zhnq|$Tk{VbH!aB$ zSh<+PRNRCcvzWExm_mWTvK%+&I@75`!#%9Pk^}3#H0!!?o7pkMeV8Ti@zfmyax-?u zil1-Cs+QZ^diKCh`lTgJCov_Fw(IE}?b`N~0@W=mo=Cdcy<1~G@h|p1tt&pUZ%lo* ze)@@H%C5^Q0L?s=M^#PRt!W4O&9Y;a89YN&GY%uz-*zt1XO=aA5~b~TPRw=~JnLbV z9aSIxaYs3BvQ*-=6@HO-3vSX82!Jf;z>+vlJ?&0P@L?ua18U}{_49-Lc!kfb)*lYq zFOgQxX?<*N&!pZo501tEu|;jJ<$!Q8P_Q)> zAmlm@rRFE>jAgMjM2cGL=FZL2FKffx9lJ8BRSiDd&EV~{ZO{5x%&OXsK?ndxH9!;n zSc9jY2Z62#rjOAi)SBM8PCcyPI>{bp^XK5eFVLF%X**eQQ!I^TPnwjpud)-RtL;Hs zy)r&N1}+)?cjECSfS*pQl*Jtz2s18I#ZFcQZaRoNQ+lV&*KqjZqt-PC?|Gb)40|@k zETCh#mQ#*jV_$1(#o9lBM_J|^PnZTNpb=HFto^iZ>M!ZU?N;fN2epQRMC-&)PEW=} zcBAap<)1ucOe#}3ejHVj$+(xWYwj$o9LqXq*ndv{x%Jv7M`bqu?p-U}w1O?0Tizix z`>obqhxEDkl@Dw^GjD3;OQ+8GuV47W{?otoU-$ju7eBuLbnDON#et6XO3OcVuKV#Q z-8V}2i_#e;J=hLw%?eB7`&+M6OuMa7hZSqBO7Y~A|Md(1_?0jI!zT^2uNN>e*j9>nmq{>F-ZI`_wO-{-u+{3Y%MJ95&6qrZsffIQ#Y1 zy@!o$KH1@oXy>2LKJ~1#t!HcLKbLJQu(N+kug?El?Z)!Msq=D=S1+nuQ2k5C&hqQ| z6{Ri38!PwZAItx|(8F0Cp(Qhc!ZqteRK4@+x`x0Y@xeZTaB z(sxS_6n|ZPuKL^R^VPM*hl{tBZZF+Y+Fboz^%vErsyCItQ(RKpSXFb6R^{2slG=vK=E^(Sr@d=Bp37~h z{-m_2`eMiO@*fM|DeWx(to(NA+0u`_RbH$3T@@(<*nFFap3C-+R{nacI? zP0sSlwcabah1I{rU$BQ_Z@BN}pRPPzx!n6x@_Uu*Y8O{7s_rOk$ZSg8S`6RZm;Yl@ zrSC?iHZNVQG79 zOKoZGn$&fr?^k|Mxvcv0)H|hLm)0e|S-ZG;f8nL-*6N1xrTKN0ODdbaf2%I4+*x{| zbU}4X>5a;pm3J%KD}Sh5TY9=#z013%^i1`qh1bi!slHUYxOfTc`mPzsdhep*&6RJI z|5R91Tv&Xxu&unl{LR|p>Me!k=_|?`IyQN)B=1OmvvzOszT*AGhl($he%JAA^~vfF zyo)N|E^IFk6>ciMSh~Epm=<@K7JuPgR=kB4|6KS+@zUa}Mcd;9akLDjY%lCuqRq<_= z^GdH(Uaf3ucD&GWS>etOt=|gPROt()TMJJ*H&maEy;lwq=R4Ic)fcMIR~{%_R{dcy zEPJtfS@q?#K)MwKyri-=|AKd2?Y7D{s&CZ(RB2W3FMq4{+w!{dgZbZAU#?zP`%&cz z-mJ-fr*LEGz0xD)Kc!!n2zxU#q@`{&A^E59!-E&U~ZW#O{aj_mjH zzbmgOyj#4z_-N*(?6T_i*t^-~`QI0Ro4c)WP5Qgl>)mrY?(<$Qy_WuCrd9Y}^66&o z7adm>E>HcWFj%-Yd3FBA>Q$*971w2MD?XLFx^PYM;p|P-AI85`zQ?;L`*iKQxo_9L zQF%POC3{8X+U&Z@<<)`8U3ROo+P=eHX}?jv-TtwCr@hL)+y03?2set9QS5B~v{oy(Y6N^UL_Hx#oN68*?|r*HzvuURK=!ZQfXVwYskIRPD~n zYbO11Xp^k zYcKO}Ywg9_8~l3-zVb}%&DsmKq1rFYZzh)HTi*7n{{6hHf9s3uo5l6T-*#MHeLeR| zX`s5Jx~ytFf!&Po{sDxz4@T`BUO|&Ku76 zom-ukoz2dj&MnS1=Q-zg=Spvbv(&lU`GNCY=TG(@?VFuv?LRl|&GujH=iLi3*Tx@A zZ?FfPn-e!@evo-6{&4)<)Y{CCQjf-;b^cJh&E4)^kXRb~L*kLxt+Aox_Qb{UTVfj% zKT2E_UzdG5{+N4x?D5!jv2&b@ocr9R?iH~coHg!uoTu#9>{spUogwFH=hej9v90mf z?I-MCr=Lh%oxIe2G|~Ki=CRC6sTWhr5>LmsW&T_}7jFH7;vZ^%s9hTWrE|XX&Db@W zq4eE}CC<&&d(w9m?kN7KaBKCJ>ZbI$iE|5gb-a~(J9l2;?vC@yKdIc3y(PObzcDX) zva@=B`jXNMxu?s|l%MW+npQ8X-ch@=c5C{Q%6rwnRR2;uuQFKuL$mf{{+&~Mtu#=( zv2bl^W8spHpVc0$t*zZvd#v_I?f%-T+P$?kwFhbs)z;PSt36u#Y3-ic`r1!wch?@S zt*$Mo+*Ej?JKbiY&;rHHx?3&z`^6v_dn-rk^{(Ro7t1e{pD#C`FQ4bVT|UQquKaE9`SS9@mh!cQUzfKQ&ab>x{#j*x<*~}o zE0-sp>3FGpzW1B*vhvm5m8JKJ*JXZW-(z1^z1M!Lc6s%J%I1!D%GWW&7fKh_c2pPE zuIqTCcwzbc+JeFbwd+a?%Wsr^TYSZPrM5YCQ*L?gj@p~qhdb`ht;jywEWJ{@qxM#M zsB~TFqS{OFzUxZgF8w^Ww*0HiGo=l+hw=}Wf1G`~^o!c|;_JnA9gFkVRu{!zbKXv? z{!nRk<(>4_%wO^s*A~?-D&JeVzp|!sU*(?4{mzTIhn-({48~SFS7h(-uJN9?Z^~Yk z{z3A_*rwdp+|Nqa<(|!NcAL-TFN%FX_xr-;+NI?uvd`xqb6(1Au3eYAGWD(*)Frjm zl?%KJy;lkgy=~sB-fP~r!bRTe-o@S}-m8UY3X8ng3a=NQ@3_?4UU;Lhu<$_T;_{1` z^Qzlw7pLEH2D6LOZ#$Qix0YWnKULiBy;-`h@>uDYwM)|v+RxaF%8xXir_#SkY)O94 zHJ}^r^3vL6waaTm`IkYZKh&01E-P=WUV{v~0U`Rcj<=BmmshVxx;=<^y`*{ps@c7T zw+eS;9;|+|@~z6+>Jzn4N<38kX7$|CQV{MJ4{|}EB;F{(Rq2^pVZ@cjhG(~+_U@ql>c$R*Smb>#*^=M`0&av ze8%;*ZQo)AlY<>QU$nfdOIEfuaMh<)e)8}4vh5dI{r}Kyzp!%gKQtWsij^z>v0&$~ z$oYw>)=5F_(EL)nwfX!#TCet6c%!x2*hc zU%A}oA$~^PzJ29hXZ$&qx;&rt-G1+dm6QJ4X-WJ3m94MsLtiT&{`y@mEu3@C@pOF4 z10QR-=Uz|lyXVg0<$#4ymIpS^I{C=%!@YI*%8BOJ#+r}mHn6eIF`RV zsWG0WF@84Ny6xO?E9Wo%x?{iA`so$t0-2*)i#rkv&)I5ukF0$D`p?)=zV(Uk{C7sP z>O23Ewr^=oyJa7Kf8mxl8PaENUCE2|57U`&WGjQ8Opm3)7CftU?CS28e`2~dba(H{ z$M0F*Zg1t+@2k08_b2MEZ%?%@bmG2~4eT@hnD6>&P5+h$9@V`6b?uj9BrmKxPH>v_ z)ARX#?@ur0_Ypt4jNco7_Rsc~*8KI`fZzU)o$O#UZ2QHYt@|GDv#(h>X~X@tDVV;; zId8Cz8P3jxZ~0E^yT6;$-2UbRR z-rU*N&B{%Cm&qBXpJ?)*^(ujnc-|! z>dtUBk{&sE|tQqjZg4o-;5ou;C$Vrn5?=i(Y?#3Y+wT?7)yc(^<;{ zZdb%^G z={p@Vt<}WzzIse>w)-3LZcE@XAOez!zQ#TcIb|A!*j!g|EA%+IhSTGm)=15Z%?;)R z2dF6JJBN&v36!Gi-$Qg zST@JIFO&7t!R-Eq6Y%J#j~YYHIW*5)oWSnS+v>>6I}E}dJSLbhzUg{b6d+jv(aHvM z=%4AtgH&JGPrHYL`H&`NI37J5pe1FAt=U#AI3OH!j1ieHqi=Zcr!}lNQ0eQo{6vp4 zU0*uB&&9I-J_b%F>|@|mFo4z)2}BL&{XjDF zO=-Ej2v!9_#fd-y*jdDWkj8-+!j_w1%lnuWP|Ifev3aajEEv`2yWX=I0|Dj}LBMT8 zfT9r)fccDsz(n65oxz$t!-2BSvA*__x*^gu|1+o@1~~qM*~SFu51p+hqj$T&X=j0e znV+NOb%f!Ae-}(BP96zfKZJ!~Y!HWbg0&AplaGof6Rqi+M>kV6)H~HWgjN!Qk}yRi z6*EM`L(#TWVP9e0*m_LKD7aFuxYWMD&+qMVrZgO7pj|%Dhq8<;yB z!5JRC0Z$@eRwL)L+?naFVg3g%fY$Gi<~o zB3a7{7@V7xKXit(R$}^g#Pl$*j38faN+N)&q{q3v_0seCnnYHx)yNN_BZ5jbTbV7B zI^C!|u+8X_$dFn6=m|$|>Fr4}MjPG-1Z-C1;O6-U51e!Ef`yBR)(kYytIzjsF>HT+ zYw4CfoAXD!J61wyWWo9M`K-S`|9{*Rxc?F?Y@Xg1?6CU1>s8DCx<6lyCE=nCkSi-; zv%@;QfnL!UoMXd!jeJ1Y)JGv~SXXAZlttG&vQvf_x&Uiyk$l?#R?BJ%$N*Q0L%0q; z^Eq!!(F&F&`@9`!u~x7y-skg9by3l=ky?@KvaN~T4o2i2`Hxb_t9 zz=K1cWd%DES`%<}Y~2pAbu0j}Y0V(ySYc_!yn-+*zL^b0qkOw{)JqfX^{sz?X$E86 zww1lWmrBPmu@Iiy5+SS&v#vC3BiHyoutl+q)C;V`l?(Il@sm6gQ7esO6r(K(l#pg) z@RURM=o~!bX+KtQyl)7Vjc(+4@3fBDy4M7$A;3J==e?OEv#_06MdsU-^f;?qb6>8u zez-NxFz?tJ4t7#HT7hjSwJ%o^+d)f~X8)J#+33Yaw~=gp>E$U$>e&f$T0Z1~6L`vn zika{u7U2gl3Gssr_(8G34=1Pyz9B-kHoe@HirU|rQ+2G97_=FJp(PsD@w>@&3h z$Uv0{_Z;t)l+-Vy`5Fz_nc=L-ii#8MN-(~$Xt%dZk7m7BkQ%*s4QjDbu2H&BUl^3s zVDEvr8C^Dzu1MLMY!j`;yNJc(0cFr|<~9coR+$n6*Ko-Sv>}A7WLLXvSni~pCfc3X z-^lDzjJ6wUw*y#_C88x5T)oQ9ClkT#)c$qrbFWTlCW71?TQ-vB zb(g3QTBAKs!#gjOP|M>8=6v31@)9OgZ}^t_fHQ-MwTf_<2`|22JsoV4VXV_Bc!IMkjm({I~0QUUWum@JhQ5Hq@(r zXJBh3^ICTEG7rb;l=YMKyM*GSNJi0?cTW}dqYD_LI)As|cZuy~lz5PRSKte=zgY@4 zGaij6$+Z3u9!F?jp;H z*rXy$QKd0or+j|~hDplL zscy|sKbB=P*!CENeO=j5|ETw|f!as20U=MWo>G#kBSZXZI2OaZwd$Z4{D539jH%5I zs6BrdH(=a@_x{cJOtULkZ@~~yc+%eM>H4X@=fir}*rfRx&T6GRuOdAd(Gl}KAgP>X z3fxf?_$q)~dIzJ7?{sInE5h3Dk|-TeD=^A)hzYmpL%EG*jdgoYGP&MIR99R=ksv9$sMlTB42f{2)LN)PWj-^*xl$QSy~TO~SpmRV#`aS00aQi7^j!ra)~=vuxBw_S z{!Hf-3I~jvV}d`S^@}&hG`(JTz^)%f8=LGtKkHBMJ((W-I>g(SkiWy~ZxmpV*-445=US);-?z*TQj(HlwmrX<>$3m=kK zZzjM=GOJ)+``J?Ko;YeoTr6f9q>FB{v6;?T5?ZeyR5J4T#M*R;mqkm6(I42t=8S3e z0(uGU&y1{!HF|Eg3wrv3RU<3-eZh*6&!hT+`GV*!x@B{o3xw*y) zNylsF{F)EKG+EaIBp7V?<67g{{Iqbi0C3RYzDCNA>dfkE zB-`a;u9^13-2O%^tR#B(v-~mux0(zi8)o61VKYj6H~=I^7Vy=AwPZhSvMM>iuW21m z;>DA^hzqKw+9FUWGyJrGr`Yh*PHHBHpWfmr#Zxoh{n@xG&Vb3I-&m2SCBVs*rNy%1?J)B78vyRFv!$Cn2S zx1{6JWX6M7jjW)UH1J9XS+s~Cj`|Wq{oqZi5ewo+h}ZyT94DadG6@3}(8*LNXu?cA z%>rTLR*zta=n4!fC&#W|0NJP#ISQ((&3mUAszG5*md&NcygU&1t7c~?NpspEa#Syu z&Z@H*D%oNTd9i@{y!^-gx}bQXU-!-UA?)Tq_cbbWSbM(`8}*vBbPXO9u78@5FnghC zUC0Det*L&~kA)DZXzJ!QpViEqqc^l-rV`h84Lt1n&S;tiKW-MRQ%YYx1U8tc1)%rN z-C!Q|M>PY=Y*e5A)v<9&#G`)WBVi6=M~^~a9?)j{%y=+FPHa@<&$a4~_!$YL2cYq= zYbh3@06QR<{`nO%Qs1B!It?R)0oIf3Lm15BV^FM)0)$dFjA0csozqx{aM(ZYBCoCP#M z(P~s^L-6_CW*>}`X;IiH$zd*`6;IE9%MiO-~n@eW>(UPq>lB_i4 z(Heft9}Q>W7jBRzC_}{*19U`pF~|;T9xZ-1;;ZHdKa0^BpN`5lLTo_gWzvnV5!lei zEg{ukhZFe`IMEs5M4p9gf)tTu_J4;HT@g;?``YbQJ`5)q?Jlgad>?W!Xt4_`z$#4e zXmvkyY5yY(0ZBs?{3|h}2|CLT+I_)eBT-}|D*V+L!b-FH4P*6;?sa?MhZOiw8p+sb zh3j>{$zugB5b_qG7FO2!6*v?23l`jl$?-V$B|t;0S)s@h>0~7n&=@0`P?K#{6K0_o z*8@#}3;Uk)yZOLhY)qVp^}8)?uP`iJc(DAVZaJ^Ke6L5hwoI1Rca2O_OF?)vVv` z7erItepNKpEkd)Ps_AG!0*@FG9yWmFU6!%1*?JrcW-%V1bm@0aoG>gL7Mt+(F-=3qMaiuZ8jwf*h`U3pwG6Nl0{*OX=uAC zxzdTWA!JI5dKlrCYJ|nv)*_`xVdZ5=yh zZ@X(C?@+{2v>!3(*2NBXjs@>s@0Pee!Gj=GHA!zqZ&@)J6t%MY2XHW)Z&s=u8#ND# zEYmZl@Q_lFwQq@D&`CW~Q~hBzbqOBY?odtjrKWbjPfbPoMQW-q0ax<7r6V0hJv`M= zYCWU1L-}2t?{8Gzr>^=^S8?Q^^cx5dtE;$6fR=IoxS_i0q8kq@6EOi1vnZ;7RF|;; z{R`Y}g%#uwQK?jnSgH{W24jIN3M>3Ml<;w(k!;BA?&+cS6r&CgQUu&E6IC647Gjvi z5%Up^-#S@?iLr!;LX(RJD<+4w?f7~5z0u5p`|`=C^=7K5%SH$7GM{z@xuf&wC1%l2 zX9m4^E$T;`0tVYiMMwj$?b1XtOk}*uz_X3~&V)1RGE*=w%?yTL(6H&J5^RO0tA;R| z5Ou{+OEnm>F;LAM;cgh$lI`q?_Op0KwtsL!(>^ev^!w){BRy@vH)1|%0UsyjhDpsx znKl#B%7hbg&4eO&hZ8a{qX`W!MEV00`tbRT7h-`+Scm@jES^aTjHC2Lj?9w%h5T;Qy{kXJz(DG&gHLT@#8H&t@T0R^G-T+YwMP)u3 z!Zu2NSkyKoaRWn{Sc6(;*#P~~oi6u~ z<+BZD(20zgBlOwE`{)@1w%BI+8F@Ky{beGcvMg;N<;`T8kkX3DvUBsO^2Yt3G189) zCcV^js;R(Kwf-6+*IUlNl?S_r~}PV;Oj8`_ux3mNS& znrQ)qX33C+Oz`6d&D52OL9-Q;nYMP3FhM~#QHCL>H}?vTyuh`v%JCGzw=^#5b(zi;VjrZKGMDDunu|v_|7< zcG|9Hd7R)-MnMV{UHrEF=uCz;g+q{k$3to@=7(-(_H~V$nN3{#pU~UQW@D(l zORz$*Js?WtXT}i70zI6Zb~f1dE3DT#8^oA~gKWY}Yu>83iE{^H(85}~Z>mKg^V$D0 z+gg%P-%MoJ&axY<`R$;cx5p}Gp?6+B@AtO1Qiu|iS#?O5%vGX@Ia5gVp`Hjk3}e)K z0}wgIlUV|@_l}`Lgo@dN#E{gwuzg-*TuVT0#}7kk?oyodX1mX~<^|gmecsP9Ja5!< zurS`X0*UP~B?BW$7}zq0$bjA%&UTv=gxp3t_-vj?2Bspno(Ub)PSLp-cM8IgW!#Rp zAO?vcEef3V1?w^{&XU$U!Xd+gqZGLv!B; zVr)q!o4HgrST!O%hVTv+N+kdU+w4CGkBKTpoAo9`e$y z6q%k7#AY~a9KWd-OA9S-nF$nD69Yq!r^04&5GObC+)jit0y8tH27uMIpH?XRiGY$Ogx8Lf-nadaRG zMQS>0qhU7WE^)p4(zF8)-XMUpZ?|6QakHFtq}mGu;B6SgBp9jC*({8SXe%Z(j!lG( zrXr_CBG|%Hb@*u;PnqGT=36Ghblvc7kHPJw#Gy-8EK$lJmMLWrLrN39AjVARNyS1% zevBR3?XrT6giOxY@-i~-hU^H~t)~~Wt;gIz8a|Bx!?v6Wxd9?;)gW(aSd9^GXc~J% zsul2Ft_>s8A8Q*Htxbiojl5k);DsT72oXv%p^X%p!vg9_%fFMi1GL zBlNZ;jQCiR%;oe2G~)eHc*(1!#5ig&WS7Mouw2viV%aVr=fXI?3SJGx&9-99)dq*y zio$msZ6CnwA#2=lynt_G>p`~3GaGE&x#O6%*f&!*jN1fh^*2&RXRi)ERkHVIdx{So zsjPl-aN6B=ty#q=L3c&vGbR^~I3MqZYM{$yRXrmyqbae^vjNm}ctrBzIBDuLtea^)gSm{IpeZ~zO%({bF3LpKJ z!h;FJ1Y%8=d4*zo%$J8TVZ$p3>LL@!lxs>zD|rLDin%E&jJWD>nZx2la8BsT zU#h)f1d4A&Lu?EVp*92K@&Pe-v3*>3jdqp3w95F;chCxd=7 z=vVD(bJ=fp1!aY-UnOnUF92nOs3hX^V<~~qM)_GIe8rwTirC;6?|q{1#|m{pNS}NuYP`XWu|%|{3zyaPHCwP z!Kw?^kT-NooDR8O*Bg%0VH9HrZ~L3^orh)#;Z$Uf+4@Tx2fc!Bn}KDvqseKR?{Ob^AZ;c3K3t^^`-C-7${C^CyeLU~&RQ=TdZE$7U;POu${7$DmxtaFk zKlV@k&YiPRD$;%`%A>Tfxh51b5fI`q+fnU;{etDciS{RUNka+7NJb(UO_(>D6~?tW z&tU}v#ESnujFem;V|lZM`Rht>j3WL)f}{e0f_eCW1N>%?Yv}TXT6!TY1=_elw~=Sr zcKCr*IqeNBU0-RL@Ya88T zAkElOB!7^U!nXc(pu9)0jl}5DD0u6l$erbSRi?QWxxQfIJ3Opbt>+3fYvm+k2sVWwA8{UvCQG75lLrp;JOy&SN#2RiGw2OF9;2>d(v#kz+RI!%4tB8iobaTcn z?Lvm41LQ~;O3#AnL$hwV&>w3G-~*Ir{aN(FVLWrJ2)-W+tWU!-Vqp{{(kaX+LRfAa zJOf6CpXH|qzt&}sX_~JwkVg5bquN8>JUO~69_Gbx)x5hTBL%TjS473MPq~U7&IwCl%RrakQ;7*@AaWjnZQdm zgW_zOk=xlAuI3l|MHv0BkSlh$P%jEWN8dkwRBZE-ds+8~tza{$*(Rj=M?^xiy8`y4 z+U7wRK`mTEfnyil&}F?e_?Izu53<&RPs))EYA%bDCmllP$g+0mBqo$e!W;a7UjAwY z1PuCU{Ub;nk+*2y=^1>fWKZjmCPL%8=^{AB|07+*&w#EY6cP44SBXJs+cenN&anvp zhssIm1KLTboQ&J0oJ6}@2yp>Xk}^5?n{oCY&GCSE5nEQ1Na0QO38bjPguaGR;_P(Q ztIQWjqXQyt4EI3?B+$qT8C2k$MNElNP?Rn5CBf%-8pG9*va*ntebOsQwVmYv&}u#H z&J6@gk#Ic|Du@{hjCzT}*;Wv#qV-|aMn44Sr5AYM+EknC9`6q8d(W;Co#zqY~sF!d5Y6h9X0kB;?|; z>eA*Sb9Hr?U!Fm_tkUGOp|ZpnG6MX9WCQcc(td-~)q*+670MIN39k{%li_>8L?5~4 z5e-J8X7`Nk3^=B?u+UByz&=blz;%WHVN*$3k`U8DDe4Fs1*%DQn&yo!P|ZQ3!~Jo3 zg5}E?wbnNdu`DrQjG?Y}q3A@Tpho2rlsAA2;#Hv4~uHp<5M-i@pdnxQ?@>?XGAfKX_%D89RT zW+U0xYgvt>{iDur%;!*?KY#Gqdm5vg^N%r%pnc5EXA>}WrVhI`GEyJ?V~%6c(RsJV z{9_opfgdwks>=lua+xi$#hsdf55=={vOB~Hh{^79eou0a@m70UV8TH-3dpDklt;g5 zAMt6j!I5PHn{k}k-BnxqQ}Hirq| zrNBJI{}sd{n_*Q}HbW2QEkgykQ>}l`dUrBrOcmEd4@ft)hSV5hGgE0eDW;Tw){J?- z9IXa3Ox~eo%4R@gQ#1qKp;z#9rT8@$D@C>}ic)!Bm{yt-5vTL{m9m?)CkS0hNO4YL zMpKpDX|_zVoIJVgiu*-g;=fM@zYb5&)A$@)5DVFr(q(b!2nJk2Q8s1OqO>vwg%V*g zVFEdJwEz_^mal+#2jfGaMeL@$7EOlJc^ztX5zJ8Om2CTkHerSLn9L0#c5?VbNYG#U z8Twc14W?!QjTtKH3IEAG%Heo+hyy8M$s@;LuDgzlP{`Fgjs7oP#2iwjTGQzl<2z~$ zq(Z={g(Q#FJLPZaWCNpJP0r9X)ClePDbzpaYvfgmeSgzUoNgS8%P@g(3~L7f(D>*h zX?yfxXTj4Y?4=S>9IBk+wx|V7_0D7oG+t2*zn=ABUmx(HU+?xYeWapKyAD;yN1Vm5 zSym1Eecgs1!uy+e!i62wVgP1d=93SZ0PzqXGJ*FGq5J)#7qkf)|AwW~+qz+!X_L~zx_uLR4sqmGq= z_1ptu)OFKrqTRvLkibmWQG1GDB06|*Vz=G94=k4sLzyl!1{#o<&p?_E{X_&QHsZhq z;UbyAfo{86M~v-8K*kXPVMT3t=MCQ3ZFd(r{cf3t!W~_2!QdxW^H zZj3h+4Nn(_aT~|A1cD9|JLd2(905Esr-~7L9NQPM!-h61h5c+Y#2;QfBFUo@m)e;z zeiGS1p?}Yx-+bR;Xmczb<^ppwti$;G88szzT_=*aPp6aX-iSc#M zWnBiYL%Kf0pteFNggo=OHiIO<{|336W6iph1u$8`4@d#4e3gVVgbpQHdCPewt_RmP zvAD6bj6)0!%A(b2Mp%e%Zy-2}Rois~g{jVX$Ftp5@+bKdu$Xd;9(4fEB_2jcN8|9( z)TCE%@ord9haD5Z+Rw|cfXciE9l)&Fj<=KJj`~J=pJ>K4G?N9-Vt!T=G*kg5?5>x_ z2eUWWUHgnsCymL>0sQ|&ogDn~kbT(Tw{EaI@GUyTp^eZT(R(>PBSLQb8>@;|`4HXF zC_C6rq3$RRu`Ttk!fx}UF&7@@EOv#2Q?2hOkpPLot9qe*aT}db1^3$(Bxp@AA*@ZxYD|IfAPge( zVjKBISUblNghnalK%Jz$6mk2M4E;> zjA$V6dKAx0WIus=#ztdUcC^75A|O6(4i$?z9V6y6g#E#~R!xjWTuWmj_#8qp>fHEI z2N%L|`WY!F=%8k#eieOPr)|u(C*oFkphyf6!7=7wP-DKfLXY|Ehn=_q(FGa1A2Gub z=HZ>tDwrllcMOURW)ZzG!xe77h?>daQVVVlpNOSM`Nt^Ute>9K!7{f$+(}Bae_*o5 zfyHSQd=z+QOLjX!6FR>n<<|%Yw!n-DiGE~Ma{rjY+pg*=H*ewfVj|z)GTPaiECdv`aJ*EFNBRT9DorJN@1x+GPTljKlXBPVcg1<7mr9+G9Wb zC+Yo?0I2X(71|WFky&40FM}L@4(Xoq;2E703|oVeZS+($Z(XipbX3}M1faIMj$1fQ zG(-PnGwF5h%#eXfq___@r`QwIqef1=q8m|D3MAmaPx}UXOKyce^e&DE_0}l)_bR~z89&S1{G2{ z5vDSka8NQ+pf7e6Deh#OB4B9|Na4t<6H4;N|5fJ8CuQO?a zwWZ*MyQm1aU|U>TmwZ&RlduUfn6(8dH>r9<{|dvW|jv+y~Hr zwj@4T#zZhgAmF5t1pe=>7&ENbZDL-rMIw95I4a34;Bc`dm^gv{$+|w%?+IduVk~Il4d<}4&XNXH4;-Ui zNe01N-me8EF()MqMrc9EFRatx9LcL7#c}7RtUA8k!WlNEg63m1A~d7jG$JVOBQ>HA zSBPZowH2b+N2@}t{|9v;U~LXQYAJ?wA!$i#d9xQsVWx&k&|aYuM92e5&?uIRD!Y`R zQM)TaiH5Or*Q4G_N07!1uoG(&U}JJ{V7lEUuPO6Ppqh+SVBBEOqfzif7UudsIk3{( zZfxCE* z9r`njQk)_%L@yWe+X*_k_D;S)#)TsgLWhfVTH;oBVij@u&r3=&W`CM z^M)O370aM**q&%VO!p7MC0>CQnYtCfMmzGG8|?z!mEbNmekxK7#t+jbQEnDCxAsQH z55BQ(?5L*WkN4g+`h{pUyz+`#Cm5@W%!!fJM)s6;*%ukGd$zILV)fB#oHe8ZMquFJ z)d`#@y$gYgYRi{l^$S`n}q z6%EWcfFXlO)mo_R#CF+9BY|c16AZ9W`ue5$4X;nr01ETc9l1L12NW%zy*r#Zr_|$o zwGAhYSSJ-nkMs(J3nRua$We8v9hy2N7;U2@1Io%Cf=$P@9Bq`J;+H}+?g=3siImY( zQ7OPVd45Rb`7t;=8U+q0wJ|yA-J~$BVQhC%Av@%V62JgyxN1%9V43qyS^~L*z(aY2 zq!X{@lGcJw(To!F-9M1pezWdw626ZP4lo^P z>zjwU($dUI3hSboX>T-TLdMC&>J4FE`=8$cD!XRb1oq(+YXMW#K{*`H?;8N6(G(eR zqdi6Q0VeB*d_ZY5MMk_^2RacqvIAiLv1^JJ54)zQR|6nS!`LJ3uy%pl z?97Y~wiw{kWYX{%;GycHVQUzZC1XK@H$Dx-#43Hi(F_r1Y@0-p33$C@sPiGfpt|`t z3H?zpPuzt-W3>h}Uy0IDQEE0|VW(WVgq2SP^CDzjqX~i-UM#bc-F-dS7u0Gu=Z|bI zYQ`)pF1-j}Au{g;k*#qK>_$2k1s{rY$BY z{j^R+I5i9HziLLSLi^wmDIF5K^yeG?8qhpz#KLMqohzecT5wSXLHL%u@ zdqvNp~hUyHTOLS-{_DPE$^Gj{aDvn}I!-zsIrZGqp>LFox zQ}`?c{4}0AZcx*T#?$SsXGlFx50E9{E}+4;_O~ZC#X9SJfLKRsjVyeQ4RSAq5xQs% zJf(-9*74N7y&~cF;C@Fnk+!igG1D1Ro;!7JS2~6$&gk9tI5Boad|=LG*@aMaQM|t~ zw%c-=EQVPSY{xXVpytMz1EVQxKc7SLfUf>=C6)RoeMdL5oS>&%9i2=<3CEM*fF3y2 zny#JYNpRH9sIUjf^3+{FUWs3q)yu~I7M-8dC1I#W%`k3@FsQ|xjk~vgtQK0SpPJ-6 zN#MvyEE3s`u7A9;L~|Xc=S{?toFqRx`OU8Xna}6pnrK>EU4N{9{N4L=4bNErI8x6a zE4)qd_!%CjlO9Xj6BheT@S~1Nx+^}}4i@kLb8FNpnZaczDkbn#M~9Uq z)Mf?}R#~6id{U5XF3I9B5Ru%hK80$H#m9|EmwG)mSmL5ZM%QN^BQV`5KO~+6jd*SHkhbG1wiB_}o}=)8#CoX1T-^;JW0I zu**ox9xI_s$&e1FMrF;h+7*EG&CDBepi2t|YQ64aZeK!0;nk>i6M_)NX;eu^Tq)rC zqISV5Q@{mBBbvd%yjZA3i$2z}+G7Re59Wd}F@Lk&VdUlT#eL)G)-h%s`UeCvFTaOM zKt_-}m>%9VVia)W^Vg7Fu?%O|U|g1+XTLobS3FI(NJe8*sE&?r4o^o;FDhW2o56>7 z`)O=A=Gu4rE&AC5JW9#i?Gi8zDiusOF(jab(Ul!x6rzIZf}o1cBirj9GaQ=-+MU!G z{L$ZxudxU?6V1I@tnyX?E6zo;3`OVJVD=+u%nIHjKJf!IW(V6w1Tq3IMx+uAKcq1h zwOta?*h!Q?-zWO?PYSIBOALi|4^vH>)+DG!Nwc6N_`wJg(%eSUkVtu$hG0m44PG{P zA!ZwKMq*{VY!^nVg#rVOUQ~vR@#I;k3{d-bOhHeM}k!0!2+yB=*=X3wB@Eob@ z|G!{<=y8Tk!Z}CqLhj7r19bP3Q?5YQxFu56f{&C-?D6Bk*hkc1Pn{SBTsHp42Pe<7#|&QoN&Dbt!T)&Q z6l{n4nxD$U;y;e2Xt5=`ic>}20>yI0=&2pawwjA^4T@?YT)J6cYm1?5q;efwOxglP zM;e4;NHpY|Ym3Z?qSx*2<|DJ6xhMNvrZV41n5qJf^I=UVivKQYiLsV!DoYiAtqf}-X{q1oBgVYDz7)Zs)LtA~t` zVNnay&jToC?P%3%p{-#~B6Nm%l)BKv>T`RKKK7%JO&WcS^?z9SJvVHqUma!W48jLm zNQgh++h`t&s+S~^0^MC3eVsvg{{|DQMcKnZz)rR@qB%twGbbF1oxwGZA`_?{X2LOD z+b#?C5TeOM8HUEn0CkZagGIyWjB<{GYdZ;|2;pPWlgzBWsK}&YQ6(xe zX;?HWDl%ysuzvC^!6pwC4OR}fi=Eg82B~AA!o^mH``0O5NOn}LdR+8jbS1D!ete@D zgx^a6!m1(9(KR=Y+7%knK0k^qH&d zam~oUPzN~-z;*+TMo?=UG26O{dTP#Vd@6*F9&(K%L+I)u)d1uROtq<2wXtWg#X6n; zIWE1-Rx_RT4mB~=YE~MfW?MH#psWY5BcK0g^8bNGy3{`tCdYvWi!nFLn7Cyv*(j1V z=-6iILCN8bqH-YByN#nR)eWZM_ zkc~O=mBuuJw(A74O$+jh_?;G*D|@D~sP!yQR8-K(6w_IKZ+$bWA#DW{nS83gAAYo^ zKc&s|#)O{SX{s^CPhw9?;&dW?bToXDNO+1^9)(l5OGsjA3T%%%LuzE;W7L`=CP(Z@Ve85X4Vb+WcK5w$=h9O3zK51o)2pj$H2b8=3^J>b0^jF7+rbK&<8ebUCWzG{!0WLHT5;Q+~rT0Cr{#mZK zq>5pA!=;U5ciIf^Sc5C4j4_$!C;x6tFzReo|EziY+4fsy%vysA61N&AQ%)r7$b!NFwNI z$`4UWbjNh1i|l&0Qntb7ftKW!+QzM123h?%?Kl=?`h@P~*|Pt~`g5{$QTDsVD-ErJ zZQXSTR8u&%?u_Y#w{Z0+cR6W6fYJ29uN-anPE7jKgBbn?*2J9)F(xIx1N~W!ubzpN zMv;{xboJ4kLZ7Zxaj4Hsl14GL4$rgY(Jt0)79Uc~jXdpQiET#RU~1FB0ou|YzoW1O z#>CxgXuD`))~9({18H?>xdLc3)ns$3Wd{eTs1}Srvo6i*WOE9JO*!8^%T#s3s#7h8 zn)nD3XREvxoFSAW)I7+U0xo7T6|+IGX^r0uB;&t$x2Qlo;INiCX* zYJT`k#y8tjshO#$W;m7FM@@z1wTRk~uLUOhrvm@>i}EUJ*U{U`b}mJrbC83Q0KX3J zpt}pTVX;DO2!OFSiUdXHQ0BmDAkgV2*+f*hMV`}7h#=w2NDsHlODX3S(C4#2IA^lY-|a!_Y~ z<3N>jQ`=0b+$Hu`r1+P`3Q#?a<{;^AFmvEX0|VH(YxIl;u&P}y^=Cy?d1;hNJuptb zvUbFXH}mnK4+halhNJ zFYH?JoA5P>0!VqCj9$wc=Y2PLMJQFU;@6QiP8Vh~1a`*|nhvvd`lZB|Wl(@okbfxd z7~mEg7?5tY6*H^_cKrji2hz=kawzWj%T{J@nZGl*`?}nf(w8gTx~u6Zk3&!7Z1(GU z^iBV_^S2Xq3f^}$T#6OeK(d~4bUcGHJ}!WVM*QrW>m+nsd@!hqvT<{E!p{#L0nvb(V;jT6MUg* z&^#~Xfh32|4~I;+Uux)xz*EWhx||g*a++AS#^g{?3vDF3%ne1Om00z9h{f&SDg*)g zJL0zE#^NYSlX0i=H+U%b^{d-^E}_Vw$wAj_>qI#wCI@4b14NU9vC4tolY?=}G1$q$ z9^}kB8i5_rsf55}?v!`9%%Y-3r?ctfo{dtUGiO%qBMEHVslVlMq=GZz;Qy356(_MJ zcC6}G>gd1VV4Z;N9%o;`$JN;fApKE_KY<7#IjEk6jZ6YD1Fn$|%-yOn06Zyllo2`V z_&Le3$r8QOH6CpvP7XH}b3kd5Ace4+v@_F`#yDC2yy=<=v^807XYpQ5#oV1gSzNYV zB9+eL3zWYc$>d(=#F^-cll|;$>u*RpXIq~o$(1d9QOl*&77J{kwsVwkFkm`M;wm3F|-e?>-CRZ$5? zd6aB&u!%@FI`vt3Wi~jNFQn_7`ZT3aIt(8(tBq}jL>r3&)Hu%q0`@6GtQ%9bRIFLAx)fum?%+qpU4Qg&q1X`v&V>UuEWjK5x6j#s z*Q{fyb^2^y4-cJz^Ef>4qsG+t&iZ@L%cG!!v5h11i7sYgz^W5Bl$sGIF+}6+se(pu zCJf3G^>>WIYX*TVhe?N$b%GCobOT_bT!Sg6b4^B;lpXOLsv@Uk`uG9EU%KGX%TpIsCOHb5AQHW1=#2VXe3sj~K&5o%x!no>`)! z#xSYzW3nvS-hQ4xDl6pME3}QL`WzfL$jvz`-OA5<*h37yLV`~|=&Bb7fAP2W)Mim5 zA6qZNwSsXZEbbm8w)pI5IU~jee?rX0gUNDH&bBNHHh}-}Ft$YuY#cnt>YZa7GJ%)v zJJZFlOh?njfE3;&W5l#MqEM*T;~Wgtfi5%?jD~`kw()+C6APh_VJ1l*hDn~H2D=;k z0l~V<>P_?a8@%uD?9ojyX9QC;Ed7vrUA5Y8`2qqWP<>F&T7P zh8V3Oo3z7#tcm&iiN*{g_*%#`zN)FtaL)Gk11^#}+(zu1t8cSSUSR-#Ez1Yz0WSl! zVSwTP3Bd;NLx^!{^MJU@BzfJ&iJzWGGHx0`oMWA%H`oaamGUQl@P_e@tB%=d!J{}% zyz@IsF~@IE+TeowA!XpQXr1F^apRmCCk}cB2|W08e+EvMy%+{N)%?cWA`a;N5r$^C z7b6$nU9Qha(Rf3isJ)hPQnfI=#|6J4-Iw%sQW#lqEr!Z|q@NnVGT#eQ`T+lA!V&*m zrgatrrM`ISPP+OKUOM>gQ|!@G46ihzogCZ~n)FS`hT*H5&|C3M^J={VtJ!jjU2k*B z?5Su8kPg@b&Gk93L@_b>5F)%LufsgciBLz?gI#fXP*HVocM0)Hn27Wbdc|QT6y?4R zqylzSf1@ad1nL#}GCIqnO5(un{|8rW?&=)8|5UsDSp!Ti(lU6&Hj9xykcYW=x6@ct_~jb1-sbnTy?8jFt$7e1U|7Wz{9s!< z7glVBHA8A1CgzBcp=nOGLRE%#Eqs#%2e`Yanm)Q42Z@$v0nn^}P+RGlHLUco>gZc= zTsI_do+^k^5|Ow5?62sX^TeWWH>7WpBe@N=>CY0!N1V6mo0~#H`bMR9{ezId;ks-P z(qs9qSBk)2XA@pL*Pq4zE+jrHv>e9y2N7@%f?Q6T3x^zp-|Oz+aKGj6i+91_ zFCuz*B8>|(q<7SuC)GIgj(KPZ{${06+9st?`9`JeWxuNw%6>;_V~+inQV4syQV4sS zQV4sk($RRz)6axUB`BQeRNtl6z2&@cwz<64aZX;BT z6eFXTm_|B{^aQg2fFbg|A(GBuU&JBuX_|dQvLRt_yka`TWvnQwpyUg|Q81+rgZR=7 z-uog!JBU{iSN=$pkVf>khYN?tCK(dM#JwALY|D5^`9&QR2AP zKcJ!;Q1^%rm4!PaeHEz&0gwq9IXS3=WWW|Ph!Y}-`^Ysu0__YNU&XcpqFCTyzRiiybILXkvp{s^{Sr>;yfCB(lGIOx`58R4!03F$8BMAo=E4Apz6!}Q58>3WpP5BA<-I`RN9DoxKX56(Z0>xlfLw6W~7V|Ad<`HPuh9Y3n0 zz_W1J-i5|vf%1k$;0H1%kWg%I>)eG~?!r`Hw&?|8s`$nMq56Lv3{*21YKFIzhJP4IPDEea!v|8EJ zuX$QC{6tS9h8zyhtZRE2hlgq0Mj9p%OB&z;I5XO!@5{4P;t$ps!jj=BU#mP7zT01? z{|FW9j#!)bEw1Nd_SzbQ6#L<5j7v?iCR$mcSP>>BlF{Hdj2LdwfX#SDoFMK!&7UDL zZ3&YP8~lIz?CSnq-ZyYo35Rg5a1mjc1IMBxXYv`XpnN1_qw4K;$k)nJ(+ppuaM|$p zsCW@u6aGI*m4CyHL6xtzdpLs%ITIlHR}t|i2vULUzXCK1{wHKK6o?e6Hw}!~27zXb zCMR|9N3s)LALCz?BW^StCvK%5n>8sfxLqmwlJ+mfaT_iLj@ z5y6-3S_2eiDI!3#U4$2!Ls}DYn+_Mj(y`1YDl#&v#Rhq=Q>~&Amd8!hc((oS~lv8&&D7&ORRPHD=UYC+$HfzHV)47qx zJTxs@P-y({tGWGn+(HHh1ZP|Q0P^tJ*aL)B&bI!MgoxbJNFcZ`lCU{<3JJ=_NhHJz zok()REawE0j@j1nB<$%QN3!Q^>sS&t9fPzxCPHKb%K4^bheTYT_t}GYF5E z9db|91Tlw+1ZikxOGZawcbr2din-ziR$#D|XiBo%NW5Qg+cO{}25HLR-b==J?yHbA z6Clj3eezlU%PO*{zi)NFnM^p`%JFc&b67_wq?2mC@BD|QTrC_;`79Qo5Hpj$&6z{36@LIve8@X}zN)*?| zo-8{^?g;RTlbR*x##+(rvuyAyh|2`k_cRxSdYL$gR}A(Am?Id3_Ny2>6eb<9rrGEqp)CB%{rZb<+*16(E{SmqHRqiae4>fzL1 z)X!3*rMiy%rt2h)SwLeJ$d{D*oZOckJTK>YxhA>vis5pj|;2Ag0p8B73{W zBp97&ECjr%u;W$Yk|zx5VE|cj(LOY-r3%K*+Xa-N3SydVW12lh_LM#7BQt=aU-`dl z%`SNxR%(A^_8x(4FJ0C{NJshx(jG!GgUd-}B@(ki6p{YsjM?B(!kHcJ9i=HPhRho@ zsm<|@!FN<}@IE0fOypp8z$E1e7t2sbGX7!2FG18ZSC+@{uS7p+lhWFb;6N!t*DU)Q zmQPJCmvZb{j=U!D%;G1R^%@%D5D5mRBXE3+RVrt#k@dGl2{rtvP^h}!xze+3?GudA z=wPrfA}3^7KA}+ZDQQO}AX@X~I4mhAID8ih#paX*)*e&6;nQOCN{$rAY&ykc4tY#F z3K_Ra6&R@k)2Z;VqmQr$b(SJHye@}qO3PbbS$NS?T(SnQ`b z-4cqQ?h&~0(2{v3{D(?s~91LR@eldV$@)_P-n>uGGv04_!eY z0u0yW2sqVayBHIc zsJkj~OXnJ3YmhPwMsEiF&tDp&bN}leW=Ec1xF)sAwou|#}R!>a~ zn^t!SyQPlH{NC|=)&@O{ENsTvkF{>`_)kV&_xK$Uc_Q$7RMVSCinu$C?BTB9W z8d10uMV>P#QU^KephX>osDlds6%t@UC)dF1wN~(3o@xg^$wDq8kCp=aij`4lLN)wx z7?OZQQWRGxjzpyv&g|jwF(gZZBlMZmhPq8W6AvX5 zQ$C!eY&Q>9LRM1QUI?=?Y{e4qc(;y^pCIdNcI#w+quVDN@!Dd{x4M*i@=X7U z@;pD9k+M@l9bz@QjM*ns6P)}$--ktj|Xqo&jbA5@nGAwOO)l@2D6oO&dvj1QOL z?hqPY5OOrFl+2o%84(bNelYb*2K^8;PIp7rVEt-yn!>=-+^psV@*qfbib0~2>mU&mA*)_lNF>%HRN+6wA*it65U6Bu2#*kHBuZCa z3i`KLbg0qIuH>(FUVGDd$*kTJNLbd1IyHn;?T<&CrvV2~N#o%D7-Z4?8bLG`O--CE z1|1qz+E0z28el9&SJKdAjzLsdkM9sq0g;u=m3s<>D~=)$VRhg%al$yM_I9qs5GD1Ajd~su^qy|s3P`wjIS}$6)nTwG; z=(~|!(0qZ!ME;lsm#ZBQ4tHwdvX3$jc(xy~zrpo$(yIXLA{T0(J5@+9DrD(rOWGXl z%n@BeqSRy2tV}fO5UOG}mU4U(^ha8V0S8;&BV~&;5U~P5f3Pvf-!9u$S!P?~ZwW?{ z6Y{ZH`Yfy(d1m9rIH=P$Uu5bPubzRTHC>jpDw9VF3vHTAoA#X%>@j7#223j^7aJjxQGdZiESj0%Qv|Mgqf6iPoM>!81{-%W{M# zWTAA;#n(g!y4$9^LEmzE8{7X;ypY{ZEcQl@>^8h|+GV;6wz?_%1E72 zWBY}niYr=9t<9t}jo`3El$g09VlE*&Tu*2oAL^jJ4e8lpBs!d-5Odpd(H_PK=r4SF z3cFaK3y))7C33UI5|rYMCWcxyC!Xua;-E-8h7c<}NO(1~@ACBFB-e|Jp^}K2=u0zs zL=q(j6X~zYL5Rw{6GhcVQ%51j^%$@Ovrtrku`R4(P-jY}W4nSR?2L><4B|&7;BW|g zYqA@Mo^ovix_Mh0ntGhe#@C3~LW)OtDqEWRLHS+>ue^;N6){!^A$R4Vy`g4k$%#Q# zh4cp-q;ju`ekuis8nX#lAcaUe?EGX`l}Tv!OmbNo3B`z7FouA{vO>lLmCD&Ip)GFV zrLh8phUlhs-7*M4vNyYy+f6c)}yTJaOnK-8TSZnb+)n_Iq8foq$OD!LX!}n>F{>qiky~ z!U5BbJafGszpR?Uy3*WVp&_h&83-GF(y;H$Ktwa$TqF@02torOTeBc6ra<5i(-)>Q zW82t6kC2midU~VzXg&aGzy^tf(A5FT0lHdnRhuz{j%Qk=Z8v2Ci)c7~epxDAzm2U2T@Z~*c1nlIIWD9BaDx6!=!7jzn!Wu8h6zL71e!2R$IAj_Bl zdFr(PYxBMEg^Y!yz_(YBcEtW^<@F_**JBQN-B`LB3tQoNbXTX%%4Dqr8)POmt}~Ow zFhPpOCJ2_8qZd*%k4W0&DQi*zEXllXda&31GZ_ucBs}3@32{0X6mw3U*V;voIJ6Uf zgc^fKdUoc^BH2IC6UYLd(>6;Q>cIwYaY>uw(*= zxiodo!B*ELX@RMs__7frdv+6AAW+~{9E}EC)3C-II81VmSZCeX-*Cr9=XetG&V_G? zwY7UA=p;B&q@$Rti)geh_DjQ9Yr~=?!9$-fy`~F9!XeguO~iFT<9uq+co3n zMMPC0ZDZw!?qZL%CQK}e7?fz~sMrpND4LBJKd~p}9i7B4Bulyq+d%2nBL!r{qzSl? zp!0?%PCSBzRCnYQOOBLwVk9e&AWBzaO2RyVArf0Xp3Ggh^<_mlk%P|i4mMU7pcR4I z?V{EK>JsWBqA@!$t?^^aC||JbGVJoyX_Sn-JW+c_veIPPUTWGtP1-Aj6{?_d1TmHk z$fK=A5)cGU9OJG~PX28Enizhm1q8IMtTw`J$18+GfJPV;sK}sz%%lo49Gb!83m5g3 z(8LNeCJC*#68vu=o2mK)9Ac>?h;q;r^^rEpYa*y&Ni9ScC08NkBs`53au|iE*k9)x ztRN7qAP}q|04rcRX4;oA&kQF8W?hol9qXol?VBM49Y{38^P}f7y0{mbl(L;pSGvv%rR8nO0ZJuzS z+Upo!dd$TnZ!?R72a`^`D!o`Bp%kHD3$Y|4xz{3CUp>L9^d_){;6Yb10}mPp30erg z^acjHrWr?Z!-sKQz69*D1p~qbc%2nh82UQ|jbh^@_w5jqW{KM{Ly{^?ccIwE27t@& zpa~M-%!}*syrS5%Q5|K{%1fkm@QU!_K*DMjh_1(Mn{g&LB@i* z3va7=B?_P-zSx`SB}}W0AuOgctpDag??_mI%#H+REvS)fP~tH)b~5f{p(e88vBmRT z*-!~hDz4nR6C9iU?&FCq1);wPwA!mA3NnhS~5>Fi=kuL z9N-6yUSW4EV=XUp{b~xzycM6_)t)R*OdYQ4Hoab{xgmIxup@hf`lHmFSCv&Xd^Udt zj8cnhsHA6K#n{pZt~#t;qqhC(z8cJwcGz$xPLm*-(0Ye{)2Bl5iH8!WNRX4zS~>v_ zQ@7t?wQ5vfM4Q9B;W5e02?}yl(>ri#QU`RE97q#xSIW^^>;;MFn?#&B0}uw~4IFQb#_Y=lN2d0XKm&XF)WHGZyyp~QI-t6tffYYVS@B>e0{2SF4{*Z*f8%LRG0BH$XYjFQskHvzaa(h&*OdKlxYcbmi;^- zrYC!6Ofx+cI&TmzBedpp#Gos+1D@)BJdp?w2%|+9;s{cT<*+mrahGg3jJsnDGCeOq zZ4ook_>7B0yz2|bz}sq{v0#ANs7PkCD%~Ik%}-{x=rnJ*Y0;Uy(G5X-ty?x+wr6X=GE1(%?1@(g5d-kIL% zQ>7<|duMoOO5cvK|AcFzVN<=cytCyEk+Xm&PMelEl}e77mKZ*r?mc<>H1BNh^y$-% z3iY3T^lup6(^y|lWmOr@@QjcGJP4AfFDml1_>?$o1wA9A<>Vvfm~WZ~^Ue_YX@Gp5 z5L>WINX%JedN!G!PNrw($MHZMG{Q)$WW$~5Z7DD(4p;SW3J%I;S5JxA1?$1oWkc{n zdRS#af!R%uH+fS&Gzv#UY#`;Q1VdazjAgM6kTWr?l(k!PLHxq_2%iRN z*r*p5eW91Kfzd8_f{FDZasJHlFGITh1q86GUhYMq^Z>cf9;Api>Zo zivCq^dHJ1dKpF!72t<0$$V!Dtbac4YVXuCvH>3cYCpt`)QMP)l zs+y0M;gb`_#u-~{K9E%9)W^3@@Yk<@DMaNUb*+V=w2?!E@bM}eH(JXcOPVW{q)MQy z=?c8=@fdjB(|Z)so?~nbn`fNv85U94Lh-p%XjUgd7cU_P2zjWC$Gt*X7)!lEFD@dD zLNSPsd4+=ESO-Rt{S)B81cd@MgK84w(2<40L9wG3jv=!Y!}?<5hA7a>9nGpt8wzD7 zDZN!FkG&Ht*6q#nk%@%(NY(JJvX!mm8F`pT*3j{pmI)^i%1VC&0Uuv)pu?#)Vh5cq zte2KS7V7DclsS6=iD}2x1Ts5VdE1VWBE|q-N{GnQ2zrO$ zoJ9C&0T{YR|D26^5ZOE&<-3yV(c!7WOrXC71RLDs0v~OlD9E^g0f8NqUR6v=2}zl zD1%8FmK4N295`l|d3<@$je#71Pdsiuq(h73^HR#seBN}w&%L7K%i~xOQd3ut6dzQ| zr-Z$O5Ns923lO}lAT#lt%ZKRE@@O8-k#(mPlbN=n3r0pI_$$uxK}c6i2VCwaDD$~I zLXZq;6GE#nf5yJLN$epx z6e#FKB@k3K^;(HWu9SdklmJASP>kSDn*p}caw-q#ZbrD0Q1*IYYEp3S5`)$}T^8>k zfH88YH^A!F0Y3NlS_?MEybglR$yS2Sd>C(Sj2?rx{$~9LH>Bk7k>yqs^!p%&P9kZ{_{P?rnTU{?X_xS zm&k5gi*{SF{(yXpPc)BFLyRjQT;^4J9pnQoR+fMTM9{U&6o}|v1bGy619}}@9)m44 ziF4A8@G|h5U<|5Bf=dE4=h36tg66F?nghUA%77$crwp1C0j~l)CDML>JPoj%R~j94 zU-nP57?h;lEhMiavp?^>|K}}0^VT#fjplqXFc^_@f^fLh%$2Opke|UBG@q;`n*o|5 zh=vkrJZEBS`hPG)r%JNCm4m}A_$%`A*<3O+iiJirMqq0YA5@1-;H zsh*)s7BQKYVb=3Q+!#eI_Qw7OT)o9!7srwK&(ed(9MM~#+E)xeqb{O1sADzGJJ?dR z5{^U2ymV@j1V?%$u`A;j$F}BD+lyOS{#N6)j!C;Yc(V1A5#Sa2IZ4glLa1!!PQs>A zlx;XT@{{}M351p-1Y`ly=Q|699f43Jmc=jdqRZkJ(kF@0%iPUuX#iw~*^<7!H@NqG@A=yfpE(9Ilbr1z~;RkI&IgO=26di6EI4SBs=0DJL;x z*Q*%|Xv$*CpFiv6kl)TL6cb2)xO{!Pw%K&lDnq7RU8x}+22yI!x*)z`~84d8G- z#>b8j22YiDNVM#y?=$i$1VD;FRm78=RMNXV;paK=3tq^H*=Sk5X1_Vc!m(Vgf4lL1 zxu3_MO6RV~vUKIro2hrM4tZ7nY`Md72FCneju@885a#iyZ%ELGa&2u~#&!YM_*?@f zC?T3`##OM6JAs96hPZ5rRY`7am7Al$NS%B~#4C+0K#^Z6vaN`}Ge4lY01$vh{Oy^m z7e&zxU~3kD>@WM#+3V$)(mHWNdOyG?uwB zh@LmSvses=gR7wR4Ool+yE}wFp8pbN$YLn$2X`LnYlN5+vkUb#-(1B7DufeZ6`%V* zU#EEbL2pvr@3!>+uWwRp`=8&WsGgW-c|G@mOQMj38bE|tv9t+N9%Cc-3T`(?keFoB znRTvA|9|Rg%ElB6Va^lp6N}q%Bia9-uBH?h_6<3@#TIo#Te&3gz`H7C{X=vcNR34q zNbmI*{1>OXnV#g1#gKIB+~9J>;FiLMT&gImlDTX#&v%YK$X$?m!CjE-lQCR`r5erX ziT^jO{Wy@|$VH2etWHGjL< z{Umpg1*b#hInUhM7F-0jSwO5Cn+ONIa`#KsOQ9Qbdzr!t%#;5WvH=z$l_N6`b zW?#?9RfsS&6YH^1Jdm%2U8iU(K7Zf0FNOSu{@h*kkAj+;F*aAcGs0YP7u=WFK?a;V z+OsZ9BrTSjhrTj#@c#F->)R6bJ?$I=LTqw~d)g5PjKcDOYYT<{u5*22-+JaE<)u>J zLyl)$&?#n^3R#Bu-^{L?3kA8U zW^zG}Ur)GS^d+O&qiHE~R#Bz~TogTDF1my*NnmdUebL)r_y|nn#foUIGa4d;lvmCY zN-g8X10ZQoP!R^<1T$XAUj=_T{CWJ<=MUX$G@gJdUu`68(_TEKq58u@t9O2RM=O+w zOaH~yiwmtbjY_34p=k4bZktJre1W^5LZ=jzs6~x&fy#c;NYw~LVOQ$IEawn#n<|EW zp0msw`QWqSm=*0;@EnactCLkiXN3=k{j-AS%g!p5l~wQ@jW!kDEK-{=TCVM=M9^od zL|^j|#nZn&=olCN4_p!jReVrE0}oTwkcaz^q~}VSm zEhmbAjcGYzV}-hefrNEX&!T;n5vbEY&)GS z%4(z0CIhH1fFK8{xM^&}4@=vkvN4U~V1wG^rmNG)RHuZcNRGeH&&*jyrAL`k3{Yv4jc^{M+V6n7Lni1vjD^(cCmB@v{?u&IN6Hc}dB6r5p z(C;9Z_KM1)&~GdWy6Sj{xR?`ihTGFmp_WpQay~*yE^UN_%Stm2q8z-Uzjb^@+EYpsz@G_%D`bpdYIaLuT2?;;BWbmo< z$tV=r{b2-rnE8eiw?j#e#o-AAP{P3kg>9S#upr36)gzBtiDER&qkR!8`elev`+Jq@ zFOT8M_`s?SF3^6zbP)%y8`hl&Ko4ENU#(t2o$8IEe!flD3Arfr9n*Jjprd5$MctzNVHwLo3qXx)2Ob|=D0FnwbB+itwDjR8?WBeF7OE|eTc@4~uPawz zIIIR}{kQ`Jdej~jRcqs*xV4nYRE2Wsa|mw6L2+wwP@L(f}?+)0yPfdL$-4M>|(muPwEzlHj>0=#k<$K6hIEYVYhJrgi+CeZUq zAK^DFd4w}U`~-scCCp~%(AapD2sm5JS@{3}vxO{K{BhD1?N zbL4f@9tmSd>&AFVruGxj0?gGj$47LltN$QTk&;6%Wk!gYN)$%S089_AjhD#VBu?8a zB(JGf+>=V*Wnl$I2y+{i5a;bsa87|U=QNsAd{Zq3D+*fFAw&^He%WX}*TctpMf#!1 zv0~()zf8-IiUO=B)O~?kf`d2GcJ6GDj}RqQX(kkr7Gl+S>j5G5523_zCzc`kAU$W>wkTyLlmy9i0(u8?-e8UFQaQt8UWLLv)6iw4hO_; zl`~hjjvr57nHs=H1l5un2Gx>!A5mLbqbDdlhjUi`O=`!Yg2yn^5Hzf-$0`08d480oHa1jS(d7W{;r4 z-U;LL>E%*!fri~yQVb%6u|ugO1S8hVDkP}v87d%%_YBF5g{D9^yw<%em|3sNunFYe zyL+e!0Gz6|CxAJtB0%X|Nzka5l^}r3#|iMy=0SP54S=!i*F7X`aE07!kSrBP6}^e( z^bGYUaC=$Z32>CsjezyBD?vh5%Tlqy8%hA-Je8oW9KJ2}yk6FE1oeBe{&L1$RxB-t zq@W{ALDaNs_fQAk^3=Y2$R`0(QK?GDVv_i|!c(CAhrdr$DLZAgGz@{U$Iga%rduHJ<;TW|%IXB}-LzZh{-dk{v7@b zUJ)s;xP@Sct9QGoCy=Vo{RTNq1GC@sr$}Ul2-rU>l8WK>8?Oc8Xa}h^e(v~`E}(8m zH5sXpJc@)kc$^zZX&flN=R)2ZB3rYOO0rWt!q8p0GD}PG8ZxFh&&<(Bcm-jg4#?-1 zwu3^j{*O-NXqjq5^>KZx2P-*M>9eF1=MtgYqmiI&CGZN>CDn^U< z5QXYvH*Bc1{LX*6ov5QoA{A&T>GI;*u|5I(n3N7p)XDW~x|-*! zPxqB}O6DLY2r4R>?{qF>h2Ji64}|ull%r;R7}ib6^rnd2O>atWwiXwm0kq->F`y|6 zku&%kqM{+Oi}L-|&jb=Df5L!ZHIs-*j?(;71nXqXX>9TtOu;`)yx-&%}9Ozf)#FL@BBu6^{#cT^`08J(Hdi|SMO}J z?y){qm%V3=vtCi(yl1tFe2Sbj_Gzl@eXE6Ktx5I#z_J_4hkbYO;rr^D9oE&>8>;Qs z)@PM}WfLjWI`*mVU%GTfYdy~aq~D}EePhieBem{Z>k+CocBl1(^?j;hm(|IlUc?^HzRoh(qYU?#MC)d8mTB&;E+2gEj>iImoRb(ZnXJRW;f6KG^@WoVV z)PA@`K3whb;oIu)I3K<)A9hlk;`Wo3+o&AJdqc66T1SLiL@E~m(r;3aCG3Tg+ZmO1 zBg^_Jbwx{~)Es)b+P)_ysNrE|+Mx$Ug7Bl5*9wTTPbACTCsn>egWI@p)jesOOHyK8d&8(#;f zP7#tsIReeSI(}Us>ePvc*>P)I>Wahc zOD*de^iU4&bt2ibYf55Wj^(aU{ck% zsU;26%1-vI$kRHR`b}qhX=3`T=?q+Caq}}7L?k=?PIc~L-%+wA^G4EbG&#K6#eTo^ zA8d)_D3AE|7L!J~b|y9|_LX|^aJwq9&7}PHaC?zXDQIKJV7NBrlU;2;Vmey=*wwzL ze0Qd_2o#B>Pi}Y#5-jOvf7W<@rrP)!7J1CmrmhdeMnty0E^SiVyW1+V(ZJvC8fl=W z>~tEW7Qx0?k=+`cdge&GvlW@XR???-^sq0oE*!5Trbom6T%DDP<@!rAF*%L7IujE^ zDHfOwwE!=V{{>h+6!_aRRTY`?6rDz;dfCGwTVK=V7WTH!vyR=VBT^D40Q~B7R{nh8 zKE&Mr1UGHdC9*+M>SYJ|h)xojvH57c&O$xY$6jRpphot!8|N%XwF?nitE&6jjY^hF zPi7mDV?mL6w6Fb;)n@lPGL@Yy>2v+3vpOM%g+eQRA@`$VKdI~c+k;Dgf@9&tH2zR! zbmXGpZIZ#OYEysvC~y~#q>n^4>Ri-1->&Tb4AUadi(*d1!YD@_Lb!65FQmngyO~xZ zMdNPxdAL4%|BV-w}&`y3K=o)|V zQxvFI#MwMGHCmi{vWsG+EvH**iI>`atliSOKUH>|UC*-KOeK!D+qkSvCk(OsS##9n zA@+^L@`u`ITJusD4z)`xqJJ1>-_6U|$@cappVbq#0*BBXBuX?ELvaoYdpxM(Q|v8# zwCfbRn`2E>txsomzNzjw-M-n{tLmR&pVDc(l$$@BC4OpNEO7&9jwQ+J zooSa-*9m8`GA3rGGtr-AVoc{e-^A2WXWOevCP%2J?eFAPH~6o1&h~=XtW=|O?2m10 zXKMF(_AaX|^RYi+AvM!IXr9zBBM_O*QN2dma}LO9XOx`28EH?nXzumr+eL(G!UfFA zIjO@gv~QE>@{8;~1V3D4|DkmL>{o;$V3l2f5>6ruqz#{?CSGhGYej+*H(g>+txZa` zvy-ZuF11fM$lGN)BlSg{x0zysx5tgLhem21rN}$f+)?(C49Ro~I}xyelR zrsM}@Zr8aApKOcu;*@;M3kqrzWg22KN*#3> zH=tUx)Q&4?#C+9qjNLi>egW*aieFk9x3;AIG{)X(S-aKcSK3{y&(*vu?Kr;m)tW2q z>#S9(_f_`e^-;!(K`rikL6(>Ke5l$ z@mC|jpbl5tr(0{)z-tI@PR+i?{=|x)-3rB?P}SGj<@M)^Q(LW0#qAlQjMyZ#v46#^>!kwxDJOXZigxEvFq*DEyhO^A+WhXb|t3P-{o>#3ukvj|nIW#3cqTWaX4BBpS zpoqI5bsTjXOSGtEM7WiG+JJueQ0+RuRl}AOKCC@nAm<$NeSuWjA&ouyY%6d;qUpgMG~DkwNNnqE>98`uPvYuf{3sS-Y{i_m6gh#qfnc z+MOGZD~VguWTAJK>}J8o6|cRd=H6nLs3teqP4Y3qhmS&(7`sjNxxsD?mww?5b{m>_ z=M8q-qB@t`&;3C8lkA2aW@g=HkHs^U7l{G2*f=%pKD&Xlj|OzU({89b-e{L* z8jwQ+uA8YF@P`|91LogoU(_`-M)m&VG1{u`+UzxIwJGZah#4Y*WDgOZy*H^pB<4uNF>3?hZ4%6^$2Q*wD zbsN6zzi;?}@w(wN#?$aO(hWc3mP0grak}A~|82ty-bb6j4qJP_uaX(sv|YlA-NKX~ zRL~kOhPvxHkEZt)PH|S*J2B#~vUj_&pBqG5>krywK4_KwLFm8_)T6iBEea%a=t&_A z$;NNg`+)K>*lZ;t(c;*BGyZJ%X9oFywokGaswscA&+3TnBfM}lFzx9CUxB{8KWDQl zLkCic>P>p0wc2yQL=#B%M8go~12uet-QIdhjhkRMuqMuU#O|TqoM1Nr*S0eC)Grgv zOu`VeJ#!txEHkO|L}t z%e%#iW|0`FR;s1f+m%g&{-aMU>{ru09@~|>Bv!3{o@hT3ezzz;T9KxoHMfDOE7W(l z+5PL%n3+M-r(_|ap)mrmzf*U~hj-d7>UPQc_stTq=Kb3B%?zJF z>UK#0VTm|<0!GFN#A0=jc*PXERo3|RY4$(^Du~R8+iZ|-iXle`3u@6lhGHAl@Vhuf z&w|HF7qnlR?pyv2vtE>_Id|F7$Yf1V)Dw5vjb!bh)LJxW@!NZsUB%R`z{t3;&f+&) z^`B%nI(mB6!fhHRknDybhA@!KShH=DvDN|1g1>2xdU>kdRfd|07YwzV9_nT_zr<1el;+{la>c021;)qV!k_(wHjhTTR+2U9dNIzwjZwyc*L9+mU=YWTf&`+%EjL*3127;b7d0{}NwqMo}~kH8oA>JccLWw$-p z2wXSI?pC)E7Q{T@rjX>lbpixB7NE&g(Y6gy7ySdAv z@sJub7XhkspIr=AHM}p=4z`2ZaY>phT)!@8#~<(0?Rexq`|w6TVm>6kwb;dr6vp4d zIn~%Kwevo^nHjH$L9eZg*(1mr7tw2+?;MjKo0KQEX@>Rx8soC`(sjfAC=C~~M zzUTlps!=;rLQ zM;Xdn)cud{PYibkbY%Mh#84pFb?2zf`@cpl@|Zo%Dt(89mPv?VWJc)FKyq!0x;l1?dUUBhpkvmawn;|l zzzzs0(m_NHEA+0e!_!$a)nKqdvN5=xA3zcqjV0N%FVMT%4$VY=32S`l&F$~Z;@^pV z?T9CBW!0!XPugvv*A-9MT>>r@UAzYyB?Gf-X}7ag;(csAyVPJblr zFFj=+cidh`x;PTSD~yAUmf0P`)4G1C(PScE&<_VNv*}&4W-79F%{M;HxYM;y+db>f z{rP4;Jo|v26G+{jo4rXL_6#&`<{ov@Glp)%Ck3osJz?#HPg2j=m(o>;X$I0c!u!YA))bcrn@w{pvw@2T+thjo+onDpFp!J0W~@Oifz<7w#lOlxE?%Z*td?I@ zEVnzD-sv7Nkm8WykT^3ryGu4NPj|_9_b%@GQJrVm4GzY}ta{FFWdGbC^}%!YR=bI8 z&I@BNa;jf;76>-QyGkXOMX3!h*he4^%~@&pupUSaSZN<)QMcPx*>71tq|RDxM{Gox zBVU4~?Mh91$u6_3N$Rnec{?d};~EsSymeo-hjv;0IHJY4zm34?=a;VHNz2O*@k-tL zs@=SNGslbRNG!qz$%B?#Y{Il)RBV@8^{TzfTBnk0Au=zj*=rH|eWg~dwYxI^cCSTA zu~C)P5L=rXQe!`29hb#7TmA;t1KA(KcyK;jpF#x%{hF+W!?-;Whzw;|?*%M0H8mfo z{;%2XJ?H^Gfi)8+b{U3=(~eAPw1I(|{F*(?yCWC-E^ROoEP$ky1(3ijfIq#aSM8Rs zBg+3lUGX~9_f_@a>-L!L8HqAd1#BSTWyI)uKiP7DzmsbMwS4*MZ|Kv{Qpc`G=fR+P zZ`#LJt`&cPNN*G9m{4hj<-Ab{@wGNFFg(HJ)a`HD&shm=mO)p?g$iSu0V<0{V1Bd^ zQy49bR+nzj4E?kX_ESh1j(HnecZ=Hcw%yozQ2p?>eYCYf^?k=ar3&ve`P!kJ+#c=< z=ec+yT8lEjhQRoC!2v6b&GL8bu~5x+@7g1J3l`SX0u-@D(UKx@)u9O;(w3Mx`@Mbw z8O6BVImpPj)ZTaPrahM3_GyTUYlpg&Ph;YaKR*CE#=6h}Jadvs2k1(o zizlUqY_w0XY1!=e?fb2FQ@uXmE+qTOT%|s=E99p#5_H6iAL8cVUAhvxaquz+Y1oZk zhyjf^9$;|;1P(A`PpV;mu}3ueK1U1-^e|x^%qbWu5L=&G`xkK1x=mfNnc-iXQk(5R z3tQIW6NvFfb^Isx80%kZg~ZmX?Vs4^6mM*R4?!&H*!t5OZfv?bb&K7)M0!np3u=#X z{?iX&0JBa_*#bRTt9EX&e@CaBw-q_uYih|>lwkAJx~+Cafxi7K9Nhku`f;mmSx965 z^{KtEmn_#-og>Zqv=9VbAIYY_1KVF1a}F12I}tIUXM(6LpV=oTYprr>7BlNTe>;Jh z^#`}X2^KEK$q?{m<1yyx=IuMhkq`4m8$tWs>$@XUB9+pcJtPMF5Xqj{hH5H)_2VJ=so~`1HYbMj}N--w#&jj zOv$2qZZyE<9CH*kKBMZD`B`s#S0*|ZfJuKf*V&N^D*FjPVmXslhN6rCsjv3hUs!bu z+Pu&Ht9yDTJCn&~oe3Irc!Cnld9{67J^$G}TwJ&C>$X}}0BN_p5{G2j?N+6>)1^@+ zbJR(-bx)13oo8}_K2hcQ&V|-r)VO@74F*$-@}0Y}(fC}tos$>uS&`N2iM*=fh0e|C z7mpS?8!hY0R5IqY620id#mvk22Axop{~^<5#rb~qN=Bp|mKx+SL*+DZj!EZnY6Isj z8%w6|8#@Ek#f{{JDd_1&Fc-I^-jl$*f3(3ZY<@6mz_+B=z&Ev1Pn9~|B0J4D-;_F^ zQJ=S)I8$uvOGVf~UDMQQ+H!8DP7N?FEeXDrVJ)Cf{VDo&>e;5w8#e72-@<8YbzYkJ zCNv-{Lyt+nT+NjAb=`KQws_8~^oM#l;oM}pvXSan;WVH>PpNQDYc)lhFGd1r^X0am zWh(@&r55<{Azq{7>a7Z=(c#ruX``blH1jb@x#a*SEu0L*RinH!iB)N(b5dmd9bbsj z_p(Z-(t1eURq0%8-K2i3bS7HMQ#ZGCCfJcFleR-~QpHtHPc+fDNkk26<4m@0SD&I0b=Txcub`0_}DV|-+p8rqvMSEwyHB(LM;9L?}YI6FqgR{XZeT8$t ztoIyMgtTZYT7IIS%%>flS>4#-Hy1xS4Vd}IvTS3m;ja$)yKk4;N*CzpHi>F8ikO$+ z(+DRhA{+HL>WwbWeb&m<=);}kY-?fa*=|n0Mf|1i&OhwN zrWeSUf{LY};G^|DoWqLd(&jccXNE&aPiEexa(g=C@|L;rGobaS#9l~E@9C6Tg?EJU zKR5iSkBW|6J)K#wqL=fMWo=f;KF(b0BjxpVKD0Kc_V#sVS&Z+D{?1j_BE@O#&#cc= zU;EA}!e;pc9a#*1H_$n`b(s}C zoO961^gP-*-2OR|y5eZ3iQW7?TdY^Plofh}%U`O_aW-088sm+nI$K2z)$@Znt@LQ> zf@7VFr0TC9=X`GOt5!>ocUsxAzgC-$ckYNR{`qgzD|Oik&doN~4?9nCF15Z&oi)U% zO80bX!cb>aUK+9XoaLNF{f;{a{K8K89H%kCl(Si#*Q$jE_`%sug4o`(ogoCjmh#PW zbjn@lIE~Wz7o6*iw5F*m&UNm_3?zJ>^G{gCf1T&7!d&wC-#A~h9xfa~#_y^hM>yA# z|Co_{@UD7iBvk5MwcxkTTLjmiuYY7WOZUl=iak>(0H9^Bmb?mrHo$k<{ zahEz*T8GOLk1s7x{3{byik3q^Re2%b((WNKaW3*#W2sW)FAd|7T(uhQoD$iju|y?D zJ4b16*CrD@Xa$+*r%Wc2#fD-wmRQybE`#UUXr%!L@y$$d_D`XwM%_=ULB;KNf)l69 zwA9&Fy9QDV;7SciQVWRf4}gcLn}WI-NNVO~&RVNv&(G|8-_(fvss>_{|N-{N; z&iA+Lbm~3cL}r>)ZLV<6tE|RS54{wIvGAa97d4su#6#&O9ZM~`!a3b4sre~gR4#IX zO**Euwc*?&>|E7lEUV6T15J)~F0iWAwz1B+R&{E?l@M>Dr(MOI|BL$JDp0>#oqV;^ zJB_W=uXfr-7MN6B5jMdE;(5bGR~dHzN9x}L1|=#SLM>lu>?W_+Hx-WeB}X?nf)@0}N{ z$O04jOFd#UGqHS;3geC2uNobf>l4@>s@HCHRwZjQlZMr1)&TjJ&;_yd@t0-ti4vQT z?i!kig1>giiPzgNj_=(%?+b`$x`gy))_wqqn~qV52@u~osqZE@Y*iMi-4j_rl}g>_ zwALHP`V0oAf&AbWY;j1Huz>Lp(_0tNLVPZc^?hZmW1-%^-2KDac}9jbTWQ`guBw(>!(9-K@y-)G2p6M-oiF8$N!XdiHMT zeD;%NGn_@%?$oO@STSMq`pk4H2u99yzAo9LTPV1R?XY0h*w}9M+k2dMPsTHF8?U3+ zbz0)6N#0>zw`qxilf+$ccTC~CPMhQ%2}lFFcpWBro%G}3ylX!#LHtOs?X-je9^ti{ z>pq5XeXN6djg{kyty zj?*==phkuvwRn!867s6K&gbyyQ|CFS$o}Jt2b`O1+H>hcPV0i(X$TiE#$`;Ey6+*U zd;lPvhKM+Y3&-|Whk5O!57hXF*`~}<4?XP6jm*@QRmVT#d|O_pHjJbUTu1LP zDNvodz-eFfV~#k^s0B`0;9LVqk1TZhumBBQ2-jWygH~bpPB?yWQ5UCi(j#^Lqi{cO zs#hL$PK(sML|xaZ_#&rqvEVi$TGByr^t3s4kyGAYz+C9YtYXseke*Zq+&&HNBok0g zS>!ZIe{n+k)zDyW^6?pI*g5H``OYF|@yQ#(Z%%wV+TMt#`3@_G6wLEtpmO+*$EASH ztiwl%pP5|TY9X8Kh`*M3qbJygdu4)k@AKcxD{|m=Gvj`pVP^^-wZyqB3(1x(ae8M( ze_G<4oE1IeF{fu%bk<|eLAoPH7>wan?DIxchS2a(IjVd*kHOeRAy99IgdxbbI6M9&`yP=DQUM>wl zizPH54(;!PmJ%1z`KVg{oYRaF#o@tmpm<^Su35wNbmx{ zYu3b6j}_FNS}%FQxkH-NXQgw5gpaRuT1tA|P9nF~e+n;tk}K}AU{W0}J5k51a=Hv( zK~)URFFO@_XTl7ww$dZbssGFrpA1xVLnPqdhx8Pu zI$1p>!#MkY?kVe0_0-GGVD^4v*FYxMs-A0{=2|5}ncAXEmtGnao=){RsYv|h8ria@ zzF6Z#tw;@mDK_{0I=E$4gXdWRL}-6EN>5JYb0+64^-I1xz}lrAKg{i8@5@o+qV5b% z>h-O0`Y{UgzHkPnI{w`$vaQ8x-oWq~s_pAefBV%U)&Fa!i@N`HXEJ9F)UKv(OXVFM zZkD=yz0=ULQfk+?&UyCpIjLbgo#F`g;TuZbfmChYMyHcg$du6!1?Hg(FZ>@I2|jJ& zN-(as+l`OLyayi?q?I$neAGr2z0Wi^HB$v2IAVO>d9O2!U}sZz7r~TuPAhfTht3+F zN9<#@t#QmZQ6U##cVs+R7tkv-DAuc~>D+ugcHebY01SY*2frRIF+L_Z z)AMIU5mxQ|m7!G(?OE0%rpuQ8-8sQLUr8`#2iyEd)X*aL>_ZRz&O;5owJ|kxr&B+o z{`symC_RM#*|@7K-gAyhe{!%W7;5l49u8LS4AhfSF7G%I2WYHGdqzP&*^I1Np5B8yuZQw zMV9?p#16rHU2(vC-Js{I+W4i@*Z!&QZ2hyIt$Ik+iq39pB?D@nrWCrt)0ocgf|GJeUe&voI|!v_Fn5E80{Ac>U3j7A?m9Df*U8+yKApP6Bnl>NP(hir z>T%!ARXz4P$C{@-P2Df$skNTC`YAQSch^|f3NvX7e#x4&|4`PM;RdSr$4*|lzx2da zH+|wPZ;;(-W}r8inY+Qv+!bc#ZkLYMGxx&CCjA^tSu+A=$_Arhrfhb`W_UN~Df@w? zTXt)mlFiU=2&QWGsAADt{iQQpUA*?!$MDW|jv2!gPXnxjjamu<{MYn=S?|l7J3W%_ z{6f?Dot$4As%~oY4@|dUP!Bm?sZM*HGS_}Tm~;QpXjt#dyrWhNEpU!+&FuQG`}w9icDzbQ1@|+IW8aO_KR$qpymEsuW>(8 zcbx1tj%=E!wE~Myc7L{7ZP#ywH)WrMa!WV_D>j4kp*Dsr{49fE8K>SF?ly|lh@5Yo z`ulLVFQvCV)oo{eqE0*2eKF^QJoA}qahls2iR&?^xlOIuvhh-ooaJAjpHf$y?q29( z3HagJ?p@S!^f~Us)=jDU=emWcCv>e;tMjP&P3ryg+(p)h>Zaeg=SBA1nr^@_b;Jm_ zA2RvtMsWG^OargJ%JtMIBiu38tkjT^Zp4o4xkZ0)_W5pCHcV5_cM}}xdP;(iQtzMd zuCO9A|EyEYypZMfP4(u5?tsVy10uKPs_RAWm6Y7G$;J;)@u#=SRD_ynJP}jFIjirJF(_7w2Jj zXmfAdsMu^Z>oT`*WTr{5ZvRM)DJych zJ4I5v>g@~Np855-FwS3=X-;$X)8)FEW3Hf?6Ebk!nHQ-(W86Gzwi-N!{I;78rS!4t zmoco`Z>kPsY2j=&lpygD7nF#je(ee4`h=z3M(BD4`16sPKGt0VLJqi+#@1vI@^tmd zmG11w_Dt8m7pXC5aQ4-3U*ptwS2JVAsaeJ@`bBvyI#)c;4Q7Yz0xiUgk~rbCVqe0C z1i9!(lgXFxkwG4CwZGsqiG%{!hsadY?@Mj^y?b5Q+Ny>o-6jO1l1zuKsi{f#_xXg; zNiOa*H@@`GU`J8x7yFyPB&G$W`NVdlV^l}t`y?I+-yMV$nQyUMkj?bwzx^}hAK_2_ zie%!3z@NVzKzDz32C(%D08RbLl95i4Tq=-e{ytI~$as@vM+P8FdsbQgFVq+V5nZ^> zL&S;M3Fy%}@v3x88n(BGQ<*IOQ_hVQA*3l*s>ki_C;7?6xqP#lTM2~!AC(Mmr{IBLhUNwwPVxN z%zHqx#j4}I?&KWaVJ-o1`z&{g0gjJV;~tpm$%pJS>b$%~{XE;PXD`ZC_uS)7R8#JA zBj&4{>X{X9QOdjD&Cg5jp3c@xkFWS3-Zmp0D`sx&zB?V0%O`7N+*+O$!^v7!te{Se znV0oWh<0r*c@j%zil7ZyFL4x;6~o<4Rt#n#D;BO3qj6d9FqO)RG5@n-(!lXElG!aB zpJ~0cur?+wtc^(vYh%*F+L*MkHYP2sjY$J*W70rkrw5%cEv$VhEv$`63u|N2!rGX$ zur?+wG%@wdLid?w>5Z-{&ad-)JXm`<`2CFXT=SKB`%$-~=iG7D_#+$-p+z2=l_mNz z3*y_YxOu~$@!MQ+RJ((Rv;;szi~x2vIxASDR$fa`M* z>T~Iq4xFnK7%uLgQa$!>zjuE1`>iY7#tjaZvApPVkQeE?A&gAq(=HP~9t9a%hn+jr zHBY*G=!DCkawl6Izl@4kyafI>5hT-RH^lQr)UxyMbh{Sj6`_mA^v&NYknyQD&$u`i z`cfVHtec9=C2v@zNXqRQnPQ%gPPx+}OXYdI+LCe)Z@pSS8?j)~D4b~etCGhRu%M7J zh!piVn8ZDnxwFal{blY-g1eWyv#4X|=iE2UKt=UHMNwgxfogm)S0+hcXTM0dTn`8L zZpz@$3I8r-?f4xerHo(WJ_mVGr?A=~JV;8#NEyV+uB?W3K2#Ix4xSekRi7EYQ(th~ z(nHlRxLYGnZ_@*C(MtCeEKi?X>Govb`OlT^pvGn{^iSlQsW4FY1VWWztK9i)x4&QI zwkTaAUyqleNsmRf8H*09-HNu`cj`!3TAHy- zjd{%-uTH9gZ+t?XQBw=*v`&M1uFHV(8gayG5Y!Tn1x;TQaEgz=tydD1GGU4-#iY3E z9rqObc~^~n&F!l`c-QTk*M-m9nwJCLt$mr|<*DIF%~Za=Lv{Jk{fJF+;a@<^WvMp^ z@~wZRCVzp14g1JP{^tHf@X&U5H_&^(bdS$TPM;Yp2mX_YRsFo!7vhIPE-=`gSsGYy z{qZWjY_Pxdr)OelI{f*WnA~CfbSB0nVE&p+j4SQ@jhR>xv2E%VM>e__f9H-u^8LnJTF#ZWH2bf9At`ER%G_7sso@483r-s^Ty zL-x9Dn`*HvS92KoY#w~9hU5rD-M!a6x-=^nu_WjbAOj6<0%1m3itJ-Qg-+d!f`Hm@n_ExdH+sn8SB~(35{@kdjQ&23))(k7LIQ%6ybO?CmaaJ zr9^3#DRI6N&IrfL+;A)FF6F!7&hg!dmt!lmGc@4^xdj6DA2~RaQ@{F`n&yTVGP-@j z;md=zrrURTq=wd{+d423zCc^_Fj$+Ngcv;Yv{h_50+ln-?U@(cjc%-=)kl%=h0qSa zUbr;<{q}n4?|0zlFKsl<_Z#xlFE+Bi>WtN7J}eJ#Xglvkok!9gl0eL@)v9{o!&@vb z!JSzKakw5CEsXz9`dMR6jT!TPIpIPh>c?}H#+N$UfvQoldy2K!MeGcCgsuHh{V^we zx#`UY>ZhD={I^+MD#jyv1nP9D*4xp;G3sPvEvXF5?-8Z>6>}6Cq>KK zd%7159%4&XNpbinzKhk@CCn<;C45?}O9+6c)Ym0ER6l$iDRkDWVv;=8G`U#da z;%|FWv<+g)fMJSyb8Mt}s!fY<^NcFsL@!+5!YFn`Jlxm5!&OTUb9*0LsiVcj_IHlD zYaMzrkrW4!u@a8m;plK!k$aYrJI>7Le1115JlVQKlr61NsC>Ev+MeF(&SS#G)`Zl& zW5T%+`$tE;U5X>~T%)!*Q0ddj!SRNXAZN5cqV|z5$G(S|wSL*j$<_dzsD9rmd|8(A z=WNw%Som!0EI#cV9wuL+h^kWObP3l;L^lmZP_Ei>a`<^t#kztaym-QQm#Fo_!y8d_ z<#rF3+xO-wbO}vUe>pW=FVmS?q-?!|5VMy$^9&mOQ0k-};SN@Nqw|qbz^lP{i;FEe zjDuUsATH1`@^f|U-rO10jZ4(?J?W-8+98E@sEztf_i%-}yjS@5X*JQ;y}}oz)kM3_ z2FDr8WS{UD>b|p2_#bC ze)IpWj@NTR-PkAmUeNm~ra-^CxDRhDWKVVXf4%QFpP$Bl{iLzqJe_|4_NO3rhZu)KoNft6DrK+zeLywK;U!dKH@+ZbYp0-0(55Usucx zx6RomDIHSIn;Y(*^EZjrBlhLoa7FSPiA7?!78hV2hHu*q>?9eM! zc|oW7LZDQ)xS*fk4Co3C{ZgQmGULH;o37txKD2y~B+GS@-9ZwJn*BJ?Wg5CSNUP3$ zFx<2GFIj0?lI9tm=9Z6xYCZE{xbnE$B&HjL;G8s1>NHaXnlIHV^xFV^LPKX|(uV!E zK$mLhJb?;oRLl?e%vm5YY4nBj!3n2VI%mMk(qIyI0s5#; zyi%ZgRC4^ofiBe0R|Hxp6>REv1uFOKSpK^L1-k_Uy8-R_hz4&FaDZps0Y9vv+XNa6 z95)6kng0jewN`n`e9)zI4n zs!J*M`v9Gzp;H7}U~=dS^nMM!PoTQ`ef)kv?@Ma%LILaIP#SQ}V|2C#KPg}-PkQmi zTKMub97KCbz_WDn)#+R$`OCHNceCNI)WSCdPo`V>s=)8n`TSMVOVuTxwYBhlX?Q_f zf1QBu(aEQ6A$hU1G|v4;fM;s(d;#mua=7{gu+rc~0xpu&75*Ecc$E{Kq2X%;o}R~V zNqPf+BY>;eBjHv#?+2NxzK?|4<$Nr$7??O#Vqd3I&L@`qUSfJwj^v&fvb$XuvoFY? zxS)ssp@1iA@XcFEt&uI)|BHYpY4BYF7POL{_^1{>BOAWC7CtKjPx8UXwJ+wTUoe+H z5%6s~|Az#uk6?r0qg#-{VxSPPJ?g%G^nri$5*xR znQ3@IGym@buGYyP6tHebRX?u81kSa=9IzI!&OGXWEzlbU`kX-PO9kus-w5Ke-`K!0w3@6~N&goCUoPO= z1+0q`jV93F33PTQaRYyxKra*M!c5x!{!IcMEzo5G)y>HDZx-k%fz}9Ax7p$nAE1{C zbhAL~C#A-=e~W-G5%4$Z#DX2SX23UXOXtzi|Fb|Zmc%mzs(aq{xqHZoUL?>*1S&l* zSxn4;m#4u6<^F8~y^zF7|8)UNcS`C>X}JGk8eEXW9Z96VK(gK?P$^In-;n{|^w)Gf zEdNe{o-c`~2~M^juLOLKfN%SP#2UB$ z4{7fKA63!)59i+O-j=o@A%z68Sz1D`p_fbVV8KTO3${l=Q4~9xfDwWqf<`7nfB;bu zqDBoa2uRQfsE9!$grY_Wf(RHi0wU=9Ju~;->{6cR`Mv*7VCT-6cIM2Pc4p=ryi+3R zc!K5;)XL&jg5E^XRZiSUb1^~35%gVxa^9+EE+OcR1pUa#A=Jz#=)Va1*-xLidEaL9 z-$GaXnj-PhRyLPYr0XeCrIS=P3kZ50K@Ss@PYe;}3W8os(BpOvMB9}PxW)!!a#=;t zYbf#=C*w$5g-8wU9zKS^Gmau>PVqGJHHti%pbH7gDIR9NPS8;VeS@GFo7m*H-Muo4 z$;XKoYObX?!zj*21m)vEZ?lM?LkU_=&_rsh!RA{89YWBb2^vq(LFU^89Zb-yV|Ko7 z^BsZ?BItaAazZOG^af}qj!!Va5SSB2G2bQd0D`V0sLi+UIpFsL!S6fZvOsW&1OCmBg(KU=v(M8Fmh^6p1@FCr*sJh%BF zK}~|LAgD#>jRfsW(04Kk%qNOQ<|YF7A@F*Fax#UQn+e*Rpq~=dqS8kM?M2X?PR1T{ z3qgAlbU#5k5}H{`&>jRmV&_ZM+e+ZOIQ%yo9-){Y6SzCY{)eD^ph_}7A!s*(E;tS- z@35*_M$oPVT}4nnTxOV`60{3J{R9QZOn=usAae)#I6KPb=M<+C#i<}DuPcm560{>h zez$a`l)_-3Tv;##x>tw8(y9nBzpwIq-EI6Wx=2w7r4{t}{MFi#@wxL;0 zk=qh9pP-yxVdidvwjt;n1f@Axc@JHE4f*Urw}K+IqDbol!QVRIZGqrD4!FVwV_&$J zpe-qnLj#vy?>OR4*nIcV9Op-XrV;c2L3u+*m_HJnel-sG_IOV8g33Kf2mxtyS!*L3rDf+i4j4M8~;ab_(+;|cm9K{*rG zk1_usFbwue=9dIUrCH26ce-|veALYbtV1_DNj}ct9`gdFfO%ZW{GFh@n;Ec61SlT* zV3ht9P(CWT&A$nXXJr_r3CakUhaSk0>K3i zcvT>Hg#&&)5WLa>znK{bhtf-{`bB}@LI?a#Aow*0{9YjVbqBmI5d4M%ULOd~TOM{Kya}G{>BDl z;C+{%I8nuHMq(tA-jOmD$=u+0!Dlgcq|Y*G|IcFqTt<5jp7^&w@D2w&hj!X_?9UzW zhk@W-4)|-@aN4ntIN%e3;LM{Ayn^;^cJ$vJ@KIXh+u#?d&vFcx(%Q@hzu|yCq6yOm zf98Pa&=_HZPdnhlr#c3}|8n4eQx~P^5h3P$qA0e$Zc=Q3YjMzQ_VPyepoE=~Ts;;f zrBIgT@+mO6f$IYZJ-yLAL0ae^wF#2-__uLtOYh2khBwT!2@mpdkuMqVQVKV_`?;h_ z|Iw}Px8;VFyo3d@`P>Js~8l&(r%$>&eo4OxC{?y}=_ zi3wXs`Y6Q@%%7xw<9;~73Xw5eOcJ3rBDClmcQPJhfA)<#UzRSgyZ5-;vZqhF>#;ZX z;5NozY}X!l2lmrRyhw@J>pm)d<^O9h#N4sxUN}u9LA3~dTv&3NO1gfZ`}Ipg=H>H{ z$o)9MI?F!W?`{I2_^JKwL6^kK=kfeQ4!B!N3FrCU^Lwa|K=pAAeqlTTcfZ-(gYIkX zgrTD7B2o0|gYE(p@X~kg#&KtP@)|q_91OKBZf}aGs$O_F+j7p`lAZa^eT{NKONDA^ zHu8IS1D1K%9nK#9-tChvvPIwH)bAn%US!9=$I4+#02LjGo%Kk$%c z6!JGKMM%?sCvyN5nRMz4Lly_T|yT6*HBT<&l z)>OGO(r3WqZI-ISuB>jtqw!p7LU60Yl+7$zXkmkxoMl$HQ~fP|bmvRbvuyhzcL#MQ zWi(S?blBYjuPj;~c6atZOCbCRgh;zt5H2fO)?pm>KF8iZ>>kx5nIMdFaU5?4_UF9pdpE8PuiN zq$3ZN!zU^FIq;LaQk_Mp&C>lJ9(9k9?c$;Gh8XqW+r0P&$K6XV2|2+-{GT01 z?zZY|!fCe7vQOf$Jcs!K>ykq-{M3=KToQH33#boZasNv)s}3OUSZM=$?JNqP z!)nfgu5*cob9L729O(Ky8+Q({c?6rMvlq{~L=eWR47(h>jNQOXB2>^F>2ie#yh zeO_N_hf64b)mJLvRKzL0rDOh>cqLMD6{=7Uc2A-LWqS$A3kdxqL1~BEYwW{#r8m1P zQ7Olil+i%x9f4^r7FV?~V|g$u&1j&kmXLLeWF<*I#wWJWfi8AUZd8naVzRPWZX{;A zScqT9#;-)vVytF_U*Q@ z(an@iO=|f!-By753p2ZvLipVU+zf?2HwvB4+BR3#E4e(3ooufB8e8JTb76`u6d^Tf zN>`;!e78wg`YRRU`>}N8X{DNf`_HE)IbmC;JY#~iVRZIv68S^+=VRym;L3iz&e%2=gPe5bcp z?oTeE@A(F*1%15qAGgC{J5#RJ`G}oaNDWfWL;<=&RmUuvrZhD5Y2&Ua)x+rCLI%W{jUWQriq@X7gwT_Q7jAN5A z1nlFOSp;*r>wIPi{_2b_l4(U~O6<{9$&XRQlO6we!2OaJFU?U8IRbkEQ#veb}27_e6ooKr}a^>WW^rh$)) zeG6J~rtUB#|N_iaV z#!OePg|lip&*l7dr3-4<;c4Zb#4R#35vyXs0X)z=(8z?n^lA3~(_sBgE*MpBz#2WS zq_F1CD04L%J61D8Nnl?;qkQ9AP=^QVQ{ebW<4mzYX?vzJ8n370vy|47oAOjC%ZC2D zXDBgvR?VKAscg_nt(25?EF1kSx-6bYKdZbQ6q3MR$i|=`BD0cgWjr$WJg0aiX)kL( zOW74Gnn@5a)FxoeY~@@mo*U9!r$V^wyg+0rIZAS@cvi>*z)Ip-p?`D^a+QmY`=`!P za^%~sCwc{VsVCm-S&#V29f0-D?}7ucUIHF;UiDdT1#=xhH9hM~vYz$%$Irtofj5gk zzkr#hibc%_^HurB&Q}Jh$|gKB1edOOMd_#Pr0@CupI=c%$#L^|0P2eeD|o19=JP-{ zGEbR+p^GB2ck+~syNWm*!xG{JKNZA_Df_V?4O{rO9v0FRfY`w=5n%$%4(G9hUM8|( zuPUE3s`&I{dJgp(R2yM)dd?6f(QFc zl#igku5Z55UV730P`>h*B=3!2<_hH|{Z;S{R6BeA^p9Gm+)mB??sDZ`=`DX!0VWeH zrTeZ>#@EGSh}G9$y+VZPt9aChS1ET$U6#iZ8zgw+ZQVam0~D5;K$@?gt^;}#Ube&C z-h?n%fsA-f>2D=Y^d{JWJaM8(>;S!q0f`eS@$A=>>u_oEz-!7zrGRR?&7c3ef^EU8 z{%)%kTxmSOUVc-#Ti&3t6GiB!Z?S9FC}SFa6X7+|L#w3Jbb7yXObScI=5sE}F74xa zpKV#AG?U(EKdn((A$`nRkv@5qGSZ+|XVA?x!1EUSdaaU&;K@bG-B^%)U8Fn+s`Y02%byiUH$@|K6?t6-7Kg4n%QA9g0V#|AgI) z{{wb22)p&6pjE$bl)+psp>eqcgb7p~dia+@)vr(v6GgN4Dm$naBlm#}GyM1NQx?f2 zKhQAFQ#}2k!YF*jcRYOacgpKlxXi=vuTsWSKA#;zKCM8%|5?7kHdia1l^unc5kF(+ zs+GP_liTfxGF^iE!B5IP64afi98d~II-XNz!ujBw@+@%c{wHQXXsLemys|`*HnVz?+D1CWOi6tP zGj*w?J_cV`S#1R0zhw1;7<$d2nN{_K&Q=$Hvf~X^pOV-A!j^`rue(0h%uoD-UFw_& z>3Oz0S{>JWmyAYqn|Kqg-~}1p9)_Z^49_bBRv>r6nOX1-dr*gA+waZXPzat5~LvfkLTz|n*po)=K z#XXlGm5{0#tjrkuTuVhHtBP0K8Q7W!_&arWvPY$M;H9<?5; zmhoyc_CSK#0yp0mC#cQfY)w!}>F{CUGZIx&NZdA2ZPtRQ>^93|ZOyo$4oYIKsnfsg z&rDQ*i~4UXxEIq^6)(RSYoWHqrgeJ@wM+kfhRMZ;LR}=DfaZOK31wS|Sp!$h#u`>G zLJK~gjGAN=hOCsd!y=^9Bwrg(DKlHDX`PFG*{R(yfgN*M?|=!=9e@R)7N8V>9suA0 zym%>G?qkbZst16|ldaSjJ1#_Zp|qa#w5F0t9d2hPXBC8<>1GKWflZkJ)Nlan*;;*D zIRUouoMbhv)u*L0Hl>Z4i04K$CSo}l;^XQAWiTEY(;)lwE8D2)Fbhgz8q~w%gHn2| zsPNhY;!}Vz7T93f0kiXM)WO*EQiG5!QOX^$!YOeGk57r&#J1{i=_Grrt-7J{*O4&q zD_1M2y~x;Lm68hL23P{Yw(MIrr=8lO*@cQa`)x4&0pmIZA#8=|4>H|*p@Qver*?{g z=?`RfOhGdMX&>q{hk4tpo$z*GYyReZ9?*oiMF1 z7|q99$=lrXHM^#RdLuYuO$YUnsQijgLefG)GD1k#cy0x2(@~v*u%#WMcT#g3uc`0F^Ve7i?cjJ)-JUl&AUqW+-eIaCHPo}3z1tZ>%4$~C zSsf*vVQn+i#&E97P<>YFRw8wple&pW?d7SFtPR_ifo`*!oz75uWuA$;AbVA4DApjB z?(H=GlmTbVl z+MO@d)weOuA1C|bkv}}%cys*$_Ip>geT*GTo*XHMN*KyF9blfB4U@x39$b$kg{tRQ zc564aNk^!qPe58Mw7Q2RdlUN2%*x90Kry_TAgmlgZ)hAs3+B^v`&fKcK&NUqb=c@! z1DqTlOTq~l)DGd8nPj|=V@_JVYLGWh^sCd7xeZnubssohaH)TC(gg!#s;OdUyQ>X*75z^71m>v-H!|cLa_9ZZRSeK4@XvzV@6%U4&MJo9+H)jM zMf_U0{pS73uIZtsLA}6}J=6x5Sk$XvqqjDw->fFMAL6DtLF)P;EzfHz*!MluRQcFh zCiPT%1ocPB>W`y)s%yfR*GDB`IbeqbwW{u3>cdd0`b00)i-%T=d!fH>XJ7SF8(d$i zlhva!*b9<-y${j>*j0t;C{INs4N#F#_zMi0O6I!NAgbbNpW5G3rTfCX{hLW$hlF%N*CzR`xi!#_CCG+A|GRabD4;`6 z(4&A(n74kvTJTjCXdCOTZ*G2*9q6q#ZeEV);Im_Jcx#&_Z*#y-I07J2g0 zK90d~8EGcq>8s~eHnxx2TzZK;-A8RWkP5&dHENF)0o5Liw~^}XH4nlOj1=j`^f1Oe zLGTphgR?8M<~4Zcy@@D?qS)@!DRJzNK5D(Fd_XW|7m_1+4zResYV)|O5bV0X>X_zT z11O6MqX7-U#s&Fn^3zDo7nAA8sW6d z3;i%sRhGV3`T2eLVUBu{hfUOy*HpG$Jj)CgWV~J>?IA?iQ8woYNp<$>6 zbv8WB=Tr^lR29LrIO5(&lHZ8&kmK3etgz8URbVEFDT7rq^)d@ zb4|_)4W_ptGb%Gfh7#m7nQb4c{>$L_3XZKAp*DqHd{A-^$1(x0>4Durh-o)EV^$g4u|+ z+|&TEJXj)t*6hv^YUj}JTwzQatJY^(*Qtr@^AT$DWz?&lfi~VNs0TZMRwd3*L#s|D zhK^JlTGT7NoO+}(Skz*5F!dOcX_QrloG<-1L~;WVAth^~8H;I5t)u3>qZ~wXFGnQL zK_o925kHd2#jDw#QR?JND9|V(OfdT~VxxV6+2@TWW?ze<4Vwm>*~bvCtHkTLo{5uV zKK>B@iUuyb93@h9AZW8A2qkoN&hOY;a)vJ>UCiM4QgW%}j8J(KrNfnjA3hAi?L5N*#|*7_Q?su4OJ*OSgM zfer=-r=EO%X?d;skdi~gg+M00?_>*?(qLT5uXS*#z8sfh4lc)nacOiNa4B8OdR?bZ zzd9~WI4*myQ-{D{M_p_7xL(yKd=Z2PZ_TdMn&D#PI)QnGhOQI9Ks09U<>;Mr&^s55 zUcvQ1?~Ao;%k}DN2wj56e&=;V4-xu zK`C3lYy`=X?bgc?ftQS@IZ&cT18O#0Q^#7ATq-z2mM_%x$@tKASP(vTgW6Pd557QW zuiv0{R8|w(`>9`~L>{DJp{_TE|I2C2lFO;J-l5j|U~1ijWEy3aAr}c077bNh2{b1F zi$Lm$!M6Bc7;G!*x@WxCX7pc(f)O?anRKIrPwwTUBGrz9(er{x#YQ8U{L5N4gvS-GrjqX@QA∨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ᓸf!(LJ& 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

    (AaXy4MZcKqU6;mE-y< zvzI#+P3+mLLKAEDvQSzxd&^W4_bJcg%~+ZX7*KX=HVJxGv2EA|4cyl7cI*Zux3^=j zz>2@D9cvzc{%=TcCV;c5@Co7)P9XOJ6J&$-Y+XGT>)!SZELP9B4lG53+vxm`>`QHu z=h;pSHZRb^?80K>uAx}#+0=!N)b#9w^4abzmQu4Di>Gwq?%Fge``{Qe1m>Fq5Q9|N z2%|xMH5AKl$}QTw)8eokIFn=^C+|p?}101`Ia6vBrJc%5d>#2*CTP4$dVrHBY0sS-;*8J z^`&#=lf7ArUNBKkLfWNZ;=?$KBAy#b6ApcYSaYK?QzDj!%9hq-;n(T~^AZ9zwbpR= z;odA!FP$d;?#)DE&iRMH%W**5(nleee(<|8n<5MDl${Zh-c@0lKCLv_OM1?EDf+PS z(N?`;t<<(&--kT`!zq3}Tca(N9d2Nc%Wi$y^;S_o`AJ{)w6@fP@`>b^{^J|5Y8J>F z`mtwR)oZZ6cFGO?SZi1W{n(FnX`A-}is+~&%mG+OK}{ZF5ladM9TzZe!DbjGGONfP#B@mDtdReo{i3)5YX2Xw)r`N6Qm=?PM0$NB91jJ~dJyr#w zL9lz4>~cF>Wcvk{-`>vJ^<7D!3s%t2yOCQz-~({+P9%>EVXcVRQ9wS-OdBcSlfnHp zN?U2cU<$>94W3k$m+Udk4>XB||-@YbKsLRqo4F zbuW)&E48T}V?2x0wUx5t1U3}On-f@XZKbDV0&A~h*;P+uUHmYi9jAcOb6I489GuO% z6BXd^r2>d7u!w*)UJC8Fm8# zMKujHG#D@gT!$azi!(457i9EIHbT_WV^ELrA;(Cmj70G(oN~wvmMGWEWL@JwQRa=d zKYwVZ+M#52N_`gFfF3u^Vk3N1x#6p0B0eBJQ&lof6Z%NL{tr`40Eeyfb zl7^DgO>G>2#6%*2-TH-!QRWs7Gyq)|jh$Fxn|dId>@8;~x4jRL92N|z9XfdBh64&h z;1;AgOlJC#KdEQm;2jY@lf9t5E_=;o2sthD<}w2ysCX`00ga<4Bw#>|#?dOtx(@ok z)-3{i>DszQ?Q>ZP4(RG!)<|0@#ajSm%DqE2 zCdxs}frTxWFD_@@UDOYdzn$)|6hz(gBjJ|bQU(MJ za16$(cVhy$Q{ zm7?+90yY{+pH=KhYMYF<2|SsO$7F~xH?Cq;WW=}jeKwV<8=|@aZ%EY*QGHt>%hH>l z-viW}v`|nQWrYI?U#NgIkGVX29Xy^P=GdL5YP_5hi04wEA`zvBXe& zDmeV#9+JNmvBr&+T}ToTGqd1)r2~BxB5{8OxfkuCV7Hp(MCl%olITo?FV_3kvOD9g z6%x&_qc&|ev+EGoL#>(5*Rsx3i!~F?dr@tx&8!#IqC_UHW4%&reU@`jYq2!QZGD#9 zohXo=7uGSU1=vcfa>z&QqE|?6dm}AaM7$(9Vgr~~!#-kF^{Vrhc&lIV*4P0FvfopJ z_n$VfyLIAK-nfZ1O|f~ERW+b+cq{xlhWMIM(f38^8M{V z>7{h-V1x9sGC6Dqd!)lLEAASRCv>T1*F0|(3aAL#R}_Lq&%lY|fGcTvW(RB9q{>@G z_fkgL5Ygb{+ySE0FU${8!Gp5LCyZsBwi=@fZbQM;25p1WkT`^hZWHD-sh+a%i$+hd zJM%t11mv?$D!1Nlh?WLgZf|3HNwueyKteqquJP4Z*moH8;!_M)c$h2kE(<6 z(kCp|b#6NL);yWGlMV7tq`d;FRqT4tD?3@PMn^dyhJBRd3S<$Kcd+4FlmG?(L1vY( zd%IPUL#rLYebphVW6KbHcY#Eqt2)vuI<1K|09+wGaM^YS{C`#CB!ohFl_XO12PH04 z`pIt(q{M}Xa+Px{S8%$#bRZ>B-qk7~s|djdXW4 z;`=}ckn{j$d3+h-s`evnUnMh!gcK4UR@61fl76TvSus@DJB_m1`7n1-p*Sw* z*3@uZP!#+HyVv={?(=eV4=6-T`U1G&S90qYY_h)oD>>*((8yoOsb8{N_5EMTBVV$d zj8oBy?MN~V_fJ8%hFk0%I4Kt38T7z?HCf6Xqg+il=jzm5s*}s!>HCO9l&g8Qk0{s6`}hl#i}vQG>_V=C z$_>Q>w@|jFxAG^-rFnDHOOR7U&uCmN-bUuKlp8fo`q)=)9w;!_moswTtu0 zT#bA)hoiH~FaJnBxra4ro1beTZX`1Gvk|vKRR-_r*u|E>W6H{Hv_Rnm{0F&p4~uyM z8_Z&?raNfy32ctC&HpCZg4(TBTW}5Ts!ylIS9Gcj;H*wvP@5rHE@2(`QJW16Cva7I zl{1Rt^GrEVj<4jFaxBMn@^U#dfeV{^S^HZ`$ZT<`RX^| zTL6>&hCS{67E%t2OeE+BkuTrR_%IUk0%(IKvW2`#bdR3BUe`|hI+zA6w@ufb8QQCc ztM^$$a^6RU!Wt4PsXHX+_x;0=+^YR9-IXl*ia}{z>-|1tZ=xQSp}%ImQ_$7i z_~-4FJV3-Wp6xSDBt(I!ojpxfL~AI)qeK>0T5kLimdoxl?p$YUoMYqpfE5!I#$v{;~5*dyDIlIcIN zCAh2X2lgFxSa_2C5e(ISGye@xKA+0MQ|vnZ^bz^XDV9>_v*}x8>W^$hsP~(6D#`wl zr3Wo=)9LHPTW|Z3Wuf;0KY{o0h8+77d!*$8H!W%~X)HDSl@sYXK=H9z&6rT8+T^(`+DMPbXb!XTXseCu7dAru_i}TLUG6 zPdT&F`|#Xj+sH>?F%m`PVa&rYr`Raa(1vm!ct_Z%NYqAcU^&1EB9bgtKKq*P7Yqk< zBi*!0gXH@MSVCR6vGRbWXOs{7CwCn^DJr2&g7Z^pibGrFfZSbSEl6xtdqL{nxDw&t zLVYd!Af(B)YJ7?E^g*yYl`dbj?00`+R1!qgBxSex>9)N)sjh%OXdYrw&Qe&)+<6EL zxSev=A=V?~BDDF)+6m%q2Lz^9Rc(Wp=6bq(|HaB314!HgP)lk6AwQx4L}Cx1VrLNo zNk~gmIkc(@F) zUXbWPA5aWcJz}Zh+zXX!?o5qu>z!UrF(E?UgJhg|M2I8;7Lh_#>u8o8vo*^qe83_; z86b#C>drVq7yS1bf81xXZJkYCZ;GuztX^BFbKf#$3xyeO=cBpVK7*7tz0(Y(&1=te zeXY&^KGR_A)yy>Z8x16qD`#2`s9=fyN|Q~#e}>(VG5HtB6GDImg<4LRfSg#011qV* z77XX&y4J~tqedM7<#P@sw=!! zOT4w;MHLnQx(@cEqOFWYvf*r0ms3Y$a9LBj$Z(y%Afdy0S;i z+os-#?WZ>O0F>a`J7T%ZP=2*FRzwx{4NGx-l@Tjf_gW*i!){V<#LiI@d&Da3CiO?` zFKT0tn711>VmZrEQ9ELbYO-p?R#WzBBetF@>=E-mv)+j9R9)F4=514N#7RxNaPTNiDjaY6zda*|=3)j?3{SjM2ZR`>AcB4jY zJyq0>*shwa8nJzpz1oOXQiVNY-e=Yuv7c2}_K11g)Elv!#puQ!fJ|I_N2~%t8m_v= zs;I)g;UqZyMy#4D>=E-mv)+hJ2D3-05D^^nwy8H_1=PkKfIMOj{VIPv&%SL$ zng#x5Q)pC@dI3oRl=CisGmRX1;gC`q2B&{#on2@e@(Daky)C%x{|9S#^G;y#z`)_D zfGo}-3jZ>~w99bB5`2vBq8&nyqn{Jrm!2R35Hk z_tdMgf-1>Df3gSluNQb${|U)8wEqtL#oiNDMZWreRcm~6CsmzX{x`cmqZ(doECK;6 zG}G(oAsU)1H4(lD>9C1DT+$UJVK!R~8MhjC=*B9QZ2CgXNMS4__L_TNEUax2$}kr3 zj)-sIUK}f2n-DO^aT|K_?~^H?sAe2Cx|6Hf@Md0;lML7`$)sLmW1l~M2MJO3BLS~B z>LP2)Gf7vMT%$0|r5l>L;ZDur)IpM)g$t-kK6()>vpl)#BD)Q)Z?#M85zsr2U4l#s z5j8JEJc8V1)*(FS96H}idI`8~u3UK;BFi&#HQroay3B@a7v?<3?@FnaTIzMAmJ~l{ zXS96hGDe?nnhZR8ipKBq5lZTykm`9JoU8Mvz-)e6=W$_`6c{GbJWsk?U&@&}f1|++ z@`JSUkDm}e*epSU@@7u{HdK~&J9&Q{Lc(|#uTc@%LGBO4n0Pp7&o2fGB z+|!Q0kDvL4#eG58`X5}1C!pp<`JTspUOz=;SKRmavTZ<+p&cY=8veP5|O6wH&!^a9!W+Uq*K=10@2Dk~s@-FI(5mYeZ$&776HBOd7>{d6{0YtaQfO*}Zw>wx zVc}*jmZy9;jQ0#jR7zOpoBbVG#dx1dnfCMy?hpHs>lOx+E1U3O7wT_*Qze7%Y5D3iilc zS%~&@`)3ind)THrRSn#HC)VTvH>aiPjN}~})Loho^?2mF4SAw`IFcWQwpZpx-l*9` zz+tZoRkNx9LP;Wm59*LoY0p^O8Jfa%MEP=$3 zl9Qu(Y%41gsT*RAI?h|L0_`BB5VF~`L{3@0h~}+ZD332-V;>{GI?6)I&ur>TP}0`7 zbhczh10I*KA2JSNv_P!lFe{M3HsJ7@c~Blc-SCc#nriYoQmW)3&7AJe5I0Z_)5+n; z0ae^u3+;|fIc4ZXz#y21DC39j(TsQ$Iz0v_hQmFDP;^oRz`ItIgDn8ilC>A_tmM@y z!lQBZtX56r`*HkC#`?FhWJ%r0bl|VV zo5N~^(;9Ei%*K`30{aMV8LF4yI%@@q-^sm_?Xf{s`FLy~C3fngfc!U5|LdT<2yQ2x z7s=qh0Rmt}#LJ`v9#_}*L4fjo5J0{U@I&#@h9vN4NM9aJ;B6p%nVZ0SA}LSc*CB~c zmlU6>Q#0P~>4-f!7BTXEaquiBpa}wwJ$3^l;5>Ji)yy$nVONQk% zk_5VV6EyJ;)#TPBo}%xzTE$$ol^mVSPj;!%4KSC!g~14SpHMpcw1v%bvd>UTo}@!D zo2)PB^XY3Ha_Gx3(VqN<+)3J^1V5tcz>lm|X#J_INZ~0s$o@{@EuSdHO2?fLI%1#f zurdhr16D+dH>0i;4rjcHt7I|An@{Cb##?%?w=h1+ zd)oW+PXilgo580*tq&TRw~{K{;zpd5F{>MKrHtv^YAIv- zsP!pjOj7H+t}*{F%9x8Ab5h3qvoZfK%9y~}NZ^V%h4d|?jCpGl{$G?a-)(Z$N7`zA zp-s708FMk%hz(nE(!=c9l4l{=){-~Z-t?Sl$%jJ^b1Er7dTwjQpVy#>xwADVMa(~2 z^8?`M?rDSVx4`p6TRu#8O;CD~p6%`VNF7aDb>i`^^XhInuoLg1t@6CliT~(KIi|`; z4;oBPEF5CdxZ%Q}oXQcoPo9n@e!ci9*HY^he(S|cpxw9eI-meh(cEitQqdHBxKhz9xL(a= zKwqv@G&OrJeH3eziY6_FTl>}ytdBOOtyo*sSIuSU4ZLlGTE$wWqN%}c@+~*;IK;Pj z{D#`8^XbFZ$lP+nRi9u_9lw#2My7;D<{O^4{s4nmi+2v-q?S2<04KG~Zw7Eu%d8r} zNiB2tKt2P>gM;`?B)<*k>Bm*N(_97Uhr!ZVQEJcMT=`D_Rug{0F^ z{+g|u8K!hI$#5QQf8kHMnXL$`z>PQcVa7S>uDBFl#G7@2Cf#EzUwmjPwuQF(4!X`EW19%pTP!r=P6E_oBN*PO{2Cd*B}6-?o+`bW~jp6 zBA@1|DaUqgv(Q?KgGUIntGLvA(DeLV&;76Ph8kRDzw#>Y8(B07 z*v;#;W;yYAkSsU2@@Ao=jgn7Mo+2=kKw+uV$Q|IXqRNoheA-fx%OnuX!}|7xZP@125^VyB!og|}$|k9OXWF$4-N^n5Z-{1!lxfK-;!b@3FO{YT}gDSVhWk}8Qi zEPpVTHj^Bd4X`~?F3CpU6Fqygu}H9q&ran(CvHJ!I} z<wQhjua``zD zz1%HZ2N_8+XK#w13nIub04~-{wF~_GPylUgJ~-_|C5a{K#XZhNcuZ5$4*^m~>jhTq z8X5F#o59Jc|HT=cf)+KK$8uO|Ch|qCNaAj^KGe$aeF9{CzE>=JU=t_y)}9V`#C@=MBRr zECaOzGy7tsp#4wJ=f9%ra324o2|5EQ;!Jmh3Qz*Xj<6lI*1M;A0dGVR2tv%_x6#>K znwhVfTCPIlY3z1lQ%;nXX?s@W-g?w02E`=E}UnfX@BQ6{RpSKQ5auv5_A#WL0 zu^M-`G;`mlyJgivUg0&2I=F~W%_ziLG2_h=S~9eP%>8pwFjX`2rTu)&hZAcY!4OTa z?9%_1Fc|vOuvax`(Gc1W=^_Gj`Djy6L5L~=Wr4GsgxCJwyu$uN%4hGuN!J6*VUqkM zpGWCApG((b-Zw5YicTz?_(~X-?tsBG(ClyIh{fD!{$~`2*D<2fL)C78&SAPEjlSH4 z7y?*awe43g=B)iU5c%VKOz2&l_I{UHf-m)Qd0pKc2+WAHgZYEf$AcCjEIYp;!H!J; z7qJwZz+zTl7lK*w;1V2pKglUB17=50RH zokf$N{5-Fut9f$zQvUC-bBd2iEKclS>0Sn$6X8ad@mW~y#ml)FGf}4{3PJ~;j0FUt zslPi%^5wiMTHL#wKLW);)v)j?4X0ZT3+0d%ykCPW;Btsu{2w0UJw+32TD0yd3X0Wk z_TNv^>sNC4db3o8<&1Z*5r3BZ-{HNZYaNi1s?~Yr)l;^7m(K{E;Pq8{N#<_geLO+$ z0lOl;5oE=F;K%#WgTNNRTehkYZ`kG=fmD7><+9~!-lXa5%~&Ii@rpPLCeZap{8-$Ftk;o^GxL#P z8;x6q*DRC;t9Yx3dJJuOY858%|F+-4I{hwReU*N{U(Ne_0iV}(*;@+mneX$Mo^=iU zt5pp#3)G3>1T4iazKW@T-aOUCtoQkCkJatsblooepfM?XZr_{RV z@&~+iy?eTU$X~=!@ZpF20VEBI_%jw#ijCyn>6uc*{m4;D&RXDKyFBG%(koGJE^sC1ut}{1q6T`)z;}z~(jqE-l~4hd}|edLvM@b+X4M(AB@n*EVt9 z!B+tj@;CAXt9vN|ozS7HC~nC;n*dqAkp7$b2ML7h0wKYXYNICJ(6F*>GbZi>`SWJ5 z5H`pWTliQjiIu-^;Z?5OEQLw@R?IKC)?3#Xw(=jl*$W>EX|xI}meH0_>)8slj$@U$ z#C2Phs#Y>-JMU*<6mQBOx8v!1WZ({-7Ptoffe5ScblAaz|GSaTOP}yJG`#hto%}W= zx9-A1{U89NkEgZ_k$F4$2z_p#yu1_4pt*8f3CNJS@|P0c8|s4XKILN)eh=Hqu{Yi4 zk>Xf%UqA}0HuoR0_)}hhwg1#-yfvgr3qIqWGZrAe4{7&+vR24(?q&eRoxpsMT9!*> z5vdsNlXNWrGGi<%bc-aAD?cQ3=?Ct=ngS;+XqRFXQsV55xl~H^kO>0xKvV#+m@?Of zdKsM7WUv5|*Wd^v!j{zR&9b9J>>|U6M}sn$BVmss!9#2ZxO9OB;rJMZ;Og8-&!OI- zKV&^lz>BNp7zkZQQiypJ_z8d9gbTQnqC*o4r=#i^Sf&$*1F2AA^*PAN31mnVd!Ei$ z7_`z;fMkQWk5dOEZ1rcn+O^|CCD3utrO)}A8c&gd)|$z)Odz)J=CKr9v|&cWVF~tn zA=aGL=(K7S;gCHt8n%%C`jQuEAIqj?nARUWuat42!A@`CSNvx(!&~$fkC)58=I2=6 zhmaVOtj8>RQ^JaEUYaMO#yg9Rgu9WUA(LT`W^0|v%snnZC07NXv(-4cO=9lH(qdW#{ z=BK0l4(&79=X-wHRkHSXQW5WWj1NiqO-Wg4%z#aWM*3qL(<~tGFwd6Cwa0k#0oZq&W<`lmG6MmYXJjo4`3r{=AJClp%T_<_7a(j3fDzcHqe zlTj|kIexMF2>jK$LA&JH_Zx4ng`dMVszp|0`0t#-(Om1_xe-JT>C}RdYr=T6(+Pic z&;JgXuup#eJAWgr#vdTa-{bQBKfvi5FDL&2LT0Y~>JOf-=N_MXflmm{C%=D65(RgF zITv`pz`5j|2@U||;R~EaD!+j61_ItS7AO_Ft9Tlu7#*v?JosEbRmF$)`y7iE+qrxP zR(vaS0>vs;5r1Y=^x{_Lj+%o1-a`8S34pXqe)K2y+b;R*pF9`X*Ob5bh``L#K=Oj! zYvuXBc(aVjR*uZbu%|$p)H>~JV>?MFTIiiMwVjAud` zoD}2P&x-Nvq!`b1Ew8(X9gY~!4_pK-xKX}*kvE}OX6g)cn6PqKb&)p@-bQDz0(bJr zMScoI!{?Xy5*?iFr!~mle^c<%3(I)nBz{25KU5tTg z!wFsVLDI}AIFbQQk%r`1r!bJrc8d1##c;Ag+k?&oCkrsI``4;0)xlw>x>NXxlYMqY z3*an)rU!U>x&x3C!{C0OyzA6%SiQHo&Z;K<2q|a*A_CRV)_4Rs25uB$@3vGq{t1#5v0jT z-&reP3J_g0GRa3KX}$%!-;S+zqIIx)Nh}>G1*`Ci9nE7%)E>5gcv>eY+u3UyJhnP( zo2U!BuO;5T-Uj0;2vg*Q6-m93!59Vx+vAT=YO-6PNLEqY$ncAhHw9iF3lzf=mHv#v zd9ZTGVG{V7go0v5_Xdg%5tm6LRMi87p{w{HF@z!`j0_Tn{#~h@9wd~ymX;=(%Wr~2 z9G%VQf`qK`u1TGbq8}XKjgJTsq;xSeM3B-&S%`QVK=-;(@esEDl2EZ9$tz)^Q|mvK0gPHvU=%}n zLj=ymCc%}T2$<=HBPKd30%xHt04|thWf;0REw2w3gVDvzaO~6>@>As6?zjCSs7-Sq z44cL*w?f^y55&S*Q}6;2ytuCe|1u}EGO-8bO%b9RteqR&;v2B>8%BzQ+F!Dv)7?lm zY#>_WTFZ!ylZSP8xO~1|foE3(F_-)oJP<9mBcQ8@5tp5mCWzI}F8OngD8)x4)*%3& z_5qgClY@mqGMGa(gjt#;6d1Xwc}@jJmVpfgL#NFeih*&*;CIYyW6qxC?PMuABKuTk zHx$n$DO|0HsuU?fI}GlZRj)EWMKnSuy;8(|7@PSiq9c&XuTsSQb^L(jN%#S2EbnEa zwabcNBJ)zj%sOor)U;{kbr>?8iN)HFp0-^4M~lfWIgck})&>30l^;|Mw+QhBS)ytH zPyn!&Q3)oIyhuY)3yTPP2y}xY&&<3}R#3a(G=G&QxDjl}iP^IY0UwDsE;2K}BIS&KhpEr-Ko_-W%d9L_@MO_tDKviaGaZW6?ZFu{+Ez?p>aiO+;fqke9c$ z5MASUhLRVimLianG)0C3_jR9@OIwKJvHPn4ixg222x8+T<)9rTpKXOX-XnLn6655Q zbkRvr=X53r&3)K1;1#xfko4Pi@q!xAruG$uM1g`#V0Ft)Jscpo6+|`zblh9UCRK@CpGPbQ)4k6E` zw&E67@f5{0YS2!M2O%@39S-c~=az#eCv7Y9>eM-HuiT|*Gfh^u7cIb?4(%Ws#wgfK zpw9xpG|zy*y*h|kERJCvMEma6T$68R1suHtjS*Bw0u0PYx~C}k367jKx={SJLeg#T zAcjJvFs7pzB61|G4wO0=VRQL)a+xaUd9$OKOf#mt;_J%LHITiQ+ZfM&vxZlXzhcpPz~ zJ{t8R+S?);!I2xV*xGCZ1YNGFmEF6GPUNoy&qPbw2_c)If@HtNr0$|~n)Mdwn`(nL zTklKXz}*DGlZo5pIo4e~0?voLxuyva2^MVJ=1G!Hs-EwHzsA26G)kF5qNeN^u4 zB|6tuLP+deX*CkL$yB<7nIZ62Nc&ddX@2gHIB zy)LR3sxDUb6}#%yWLr*IP?I2kx?ZfVSABZ%wQj7oYI^k(7jXV|x=B0*6{e*(iN}zH z++6z{ujti!&WW4F*4P?;m*S#kE59#bcC8v9PRPyuaqjMtzxNmYk@Oy*axV@L?bT+V zIZzBvR8Rxkx;{I|43&ieQOP8diytI91I)XY8roAD0z znEnrkh_+S`wYp9x<%J=lb;ySi40=H$S*ZJr13c$eF&Vr-cAMCz>9gm{s@rkSe;`{86A!zr zaAi^s6TNXd>>nn&r7n&J2UjD2Ku`fYkd8qhS(Np|T(rGI#Au5={q7L(P?hx&jgr*v z644zd%>RV&Iy1{k-9HD=9)W{5oHK|9Ittt6iYD^ryTn6!)_l3@E-{i`F5zyGPWhYe z7AcWeHh=MM@swIH_{wU{`@Naq51M}DE&kHoA|9U{caL}!$=G{Dytj3h%iB5|+!A=A zKZ^mi^_jum-j3Wu1L*nt9s$>wi)H8gL=MoUlKVtkD8Kx5pBRlJ`SJV3T76@Y zC*=VVp^<-k3Ly!$13Az4dd+9`mp1HblxzG@qPT2l`B(9yuU(QqJt&fl-k^8keV*`J zphAA8gF9*{9=}^a@tm|APYkZ~p?JU*180ItXp`91&)z&~TbSoJ5yeBt{gx@kxB?o0 zYp-t)7%^lI7k$9;{BpQB>6Pwo8X?k~d>X>(U4Vvz^Gl2vryE6BgoKY1GW4+qSRN2S*pA{4lcBj2)Xeqs-b%RcexlR~B|R)(Ic@a9n%i*NXd(-r z6oJ}ddC!=%rchHm`lJBDBK=2--`j5f`56#e&$reVe)2Tf?aza)kvCa`t0x?G+H)Yi zH(x&M7^ye}|9(E*vARIE7$v%N%{}L_X;PT~viRWDTWD>g5^UJ?PGSN)oV?=Z8v94o8c68-jZ30gP;aTd9 zl#Ug8U<^*@3-XQ^#O2{Z`R%g+Zb2 z5nAp92>QMvj=(;z@vCAL*f8a%~-v%8gW*TO}`#$4dG{ws~D7c!k&z5KP?mx;PAV*7g&`RDAc= z2_i~EcJCYF_DH3F3CR}3ZEsingSg2lj(8Fp>Q?4H zDSBxU7Y>Z!SobbKN*uGr@||2UG7yC!0zRW<(p!K4#d5@3A}yfIWnK#4nk=3t;yp{> z0=BOedp@2gz9#*HX?Y@ckj*6uB{If`1B!iRUOWR_v-2azL4buiU?I*UfZ3yqg;5J; zL5H_T^k(6Ibz;0ttd7${$+GKG<#|iQil$X(j$!`5H~~EMHy7$=*3aGoVjrU5q-r5Y zy)9OEAeWvsy`8S{;CZbuzYv>!rPT?bQ^$3kZvdVD7dQZEmO?<0v`n-n{mNdg5`?}n zL_YgWihy~-yjPI$r0tOZ+g$em*5m09_c= zcuV9<%S9_3S1Xo_?ycSnhd0A0+I7k`k%qNhsQJX>ULn4P zJ+kNLl_FBpCail`4AX`#c87&Pdk^Sd>o{KC{manvT(C>rd?nb%)*tz zu|rt2xzSsWC8(R3>NdIjJ@L4!avI$sqY6ZN_&IeA3S2>Tgga`KB1ad9AebY&T@YCBAc%PuYr$AK^El*!p>dgV82{t#XoP4uT+y5?b!7n@c$coEE^znN#Xn_L9^et|4368C7o$&j_8XVV}2x@(Yr(pLG6v`jFY zX2m66+ixo;pIr-vzy`T(t>}=kA0jJt{zF1wZ8r_uO>-Xtwg)&!h#A=uxO&2sQGMFyN zkHk$W;2*{T-)Z3fQQeko-5sywL1=UFBk@I2BYdLpW6hcVGERXPSky?ta2kxlOUV-( zM8^ofK4-q0@!Q z17ekuV_&deK#IM--7|eVP^5Nf5eYRM2aQrPS$S!cxOuo5$!N+XTV?SmBf}6+&rd`f z9bfbGF44`YWqIEDOcd*|u^G8r+#EOy022x%LuBM~1WEjQH&D0>^73wx5&Z*X7N{Za z6or_E+vV4{%b8y&`v2Q6YH#gr-Fm~9qRn-RNnNo6YYtH~NFuKUqE{4H*1y=Wfo8t$ zGd!~BOF?Ybz%s#*G%FMHv@@O^WjNinGjib`F&O47A@B$MtNd*bUIZ-fa*^B!W0F8q zlE4ss9U%;vn+7l3%!h>_p*`Vd!J_RlqgJjCJwdGScK(sya6%h zSOa`F@T;@Y_>YRYw zyaQrD;Gz(_(U}9{!!uTc`q2u=L(d@Yvbdm*Kcec?IbAdbcnUE| zfC0H)l_QR@xM1DAG!`pQcQ2FLcS6wd57r6TI4!;t!vno_o>{2Vo>aUd9FM>z6%tS4 zHATs(F;^_@Ha?Xd>`Jd=cZb=-r?$6pB|^*8=CST~P_1Id%alqn1V`gDmB0yqkXwV& zh%fhTC1e^CWyTTlzE^X)j{OmcR~*D-fAq*vtjP6p{ZVmmdtWCZ=IK%ly5(Zz8t%b? zTpX2)-!o$13=jZ(#~y5p0P}N7$j=58=a$m-9#xJ*2Ey|qTwf8wNHY&o6PMMb@M|=I z>lm;fv^g^8 z*ozYbKm-nP@2ZyL?ilOqE&O~18Xc9do)F_>jt0QRj30&jb^y;J#$YhaX`+4*H@QmQ z1<^lEj`%@ru%sfOe60lwpo@lz>O8U}G1gm~=B*{siegTUI4NQ~+Pu*o-dZALz00+! zev`L&AjZ<#8WTuIuHdBT>1F&ju=NyI5GC%pcv5WB;Af=xN0H(O^zrb|;zcXz>zRL6 z!1su%OVXtFo0zB zZ}JTIOMI(mM?A)doW@Ic{(?PAOVHK^ zBS#Zn5tx|=^B|m(=gCTlPRTu*4j->$F{b`>B*6Ozv5-szC?+x|bNqxrxb-!eLO0s_ z84H4K<_zbBBx$Fj@ee z?Hyo@=xuxT0QysA$?lZf4nS@9(rDqM@)=GZvB{$cRUM2)rjqRb=gA`h#sDzU(gO_& zA@ORUk&fhDE7@-)t{{WN42X}LB$<}tihwM)$W4%wm=f-4d4G_R;E(+l^1keqYTPNm z4l-UMO06#Odob8&lkf)AIxXD-uA7zil@n#s2r;_rg`djdAx6xQQt;S`wt~AQN}}Br z_!_8@P$pD4GzYS;wmA6~0T2MY^5YmrA54r8Wpn^2WjVef+2SW5#vM^gMgk!*0b#Fj zq*bWV6}(G`LmEbE7?!#sk-4Ep`=m{NIDvtYn8iT$0n?L4Hpg*GT4L)x$3l&M8q9lI zh8qig%zKF6So90A4dr*?#<;p>kTn*P|Bf)Sv`Tp~!bs{pQ{xm5%ho=crHSCKP7;~~ zb#*#`kHGf}F^RWDk+@#RhDAxskzj9*dk?EF0Nmrd+(wTK0(!Uym047-6xaOBx9D2w zs^z0MGyv<-TtGjWmW|dkTg{ZN`@U;DupxNfo&>>fkLjw*_f;Idu zrIMp#^%5dSbF)~sj5O}-{6|DJ7<@#X2U*Hgl%;)X<3k(08^-)B2XEBT%p`NXFXe_v zk(XkP4n`#iOsfaF$D}Oow}P2a#yoah4vRC| zw?1Yy#3?``1}lR(j?}E1&<6|y5F0N9M+6=wJ#ogJp}q%$OpG^b_+jj9eT8M>35HlF z3wnA^_D(Qv$85|=Fvv!FbAln1$TKJ2NP*Df{dj<(8a~;P1S2Vy7`K=o%n?Wm1nVQV z1oqR3fkrdgJ<&*^2n%FStUR^QmA$WadY(%(8t9EC!bk%%3&4wW<2k?A2zGQ)a2Prs zHOeh3lMRwxoJcmB)stN$H#BbWRmF|O@%gpe3Qi0R(On%(tf&O}?03vh=8sDA2hU@D zL!%((D49#-EF|_XX-XDOSB&8Qq+GMATdMH|l%|51@d$A1rx`{_O394$_&mbIEZ#4< z!=)=gMl9tDW^`$0)#9_D;7PS4tU^XXMnYVn_YDurfRJ)%Zfdkl4oC}%RzVv=LN;UZ z*;?-IIr3?4%t-YynI_|D%jTLsprizwaK20tMnmBIJ%sTDHqkO+JZp_kQ`yTfVlyhq z5f-ea%*h{N>m~qVb3m#_cxUjr-CUVd0cb-*QYoZ_)&thi-5BZK8iRX$q|_jt35XL& z2M~u9msh`2F=gu5Zbd4~Myj_q#kxbDV}|jd*8JESU|!$Cto#FodEtA?t12BP?3+%L z8H>6t`%hebDxYd(i~uqGWg{ala3T1D3RnNRkr5lU5LmLmnQ1AZ1Gy!tF0L_74$@^Z z2nU*l93pXqgcNET#9F@uh9T>}lN0xL*f*Y(fj@SLJ*5w=e`{ z|4uh<(=P?eA+3y#jgJ$xgqgG;IzoP4HOQlUua(+~7g`z3uJc;{IZS1*=d9;Bl9ncs z4z$#;?#LKWt1$-DU<~+RYa`t~jwaP2a**OI&TDNXK@$=0Z^EmM@qI|9+Em2NxFCOM zW4xp7lr!2IA7L*HXlIZ%NM<_&p$BYeJovpx?auY;0^#L2&dG+7y}_u z8hjg3_MHV<47D~ z@SzK+!((*~>1-TH+Kx3syIXmga@e~Y`*}28YhV{6A<62|+Xe@jw+$L(b}{aST)x%8 z6mL5PDIf=@>98OLS;r%+OCt@4`5N_vmHDVw_Tiz9myYJOrID}vg%{-}fVAWd2 zYBem4E55{IbjPfP?N=V908S(wd*fnRv>eyNh#vk~D6J0sL3#ukAIZ*9L7;YI$%A8}TK8l_G`N?6@Wpa} zFJlu>^SRd<--2@dcWvbY?l7B~=a5+3yezArNELV%n8s4=LGPNgg7Cn< zi#q%0^apiLaG%h-|6vv8UvErzl}%RXQOCZ<{n~2JtiG5Xx3vQ;M~P}1R^DKYyTYRT z*&7XL4%jLkm*koojrUx6pDB%(hx!>?afG!w5S}Ejy9r(XCCA=m%*DEjzWExP^~sx! zj{P^pklaGS?3uvC+&hRp1I>97z=F2Y@v-hXWGI76KuO=?8bcd&;KMoz?r*#XeUnN3 zjXPlCR@vXUr;#PivVr$CEo&bfU^EPz@zpj$r#H)|M;i^3bCDwmt()0YPOKpidP{K) zao{rp4Wof?`3|eRVzdzt*c*cC|Eas8I^As=2&_P1KfSJr_(&i=Ox`yLUd*%UD0(jeo8;4+FH7X-VcEppIcqgnqgp;&$a zY9T_q+^(VvLY}vThD@WA4=mg^wg?j^Iws~br)Yu55BKB5eWms)XxVw~}--?!nb7aHYj8`HGmX4>ZYFGS^go&yG~WOtD7_(MCyBeRDY&Gh_T(lg8$;i~%XS7J}4 z++p19$~uheopS6Q#sVmoQ9Kulox;>NdB4PoMT;|<9+Y*LF`J(4Q`GEkBSR+MX)J-4 zludUU%balMDp%YM{gn&y#NEcj89CL!kX+_mO0ei@O=Z&c*}thA1zkp(JLvj%`k|!{ zhGaReDc(D61vCL~mR2T60(F!5BPEb{m_>9^(^H9~epi7b;rC7TCBMAKxS>tvw(l*j zH@XEEVXHSsaJI50M+eq9m6Lt%HSTNj%avdoMfJhdpMF=2*?nJJTK!AE6 zf$I1mB+x?p2#Wh4dU%?Z!^Q^M=M9>DCEBOZH>$RSpdJ zV^;XN?Lava%xqvQ(0!3ZM;kH!)6r61d$a^DAngM1U*zIZM&FE$f8v~j7a`z>W&y5A z1OnOz5XXWx`>CE79e`OSc0DjN$X2K#lDNAzfKm}dP*F`4HViv$-9b4eA75>s7*<#q zly=yEkvOYeaCsgvZ#(I2a8;tpn&N6BpkybV;cC4qLj+TEyj80nYj4&I^R2rHtV3{D2XUJck2HHPEx}Gsc$U{#Xo#hkDjZpdSGsfhr z@A(QfCGx&!jSGx$v06hAosbYOQf=9S{OdWRQ?yr^)%OJURfhqlqCIbLf4K4vSxHRt zSlRP=qjR%!WyBf=d?$JyKYr%isl_<=Fz&W5bE+}7ostWl2UG08Dd~CMIO{6^4Tr>T zgbp|MXh26Z#~2R<98+a6a@-5X3|IMw1m9)m3&u=W&e>zQ_Qbyk{<~J-8S;{GmOSeB ze%YvW*D9r)CjE;~Wt&%U)K2ic`ik*@))jg!fn@Io`ss$SphPT>03W>;jIEFI3Yag1 z$JzQTQDcqPUY(Jg*Nq$G)UigW%Ti*Ii^hWgzD4dBYYeRik~gXlsj}H?n9S{R_-lsH z-j{gcbWh=H#&Y|uqvfbfqX=~P>88m|lm)MT)@LCjgN97WrM zUKu}lWhEh0yj(qG39ZotdDG4HQkbIPY>J9yW)7vLiDrpuN=5qppSAYh91!ci z-}ip-`<=b!z4jWO^~_64>rf31)W&PnxR|i<8qGT?3MONXE;uQcPsRkxo479V182f_ z5Sd3>G@kLs&8DDXy>@v^FkF{!StAN2I^vVT5vlnKz=-6;dL4x%XUz@?&ouGa>)OQr zjcwf}->85bySNJp#Nfkrkzl?F+H2}ecFopCbR$Jd;0h2+2BIFQ1PUY&jkZL!i?hLA z+9;yl(BL;o-1vq@X6IeQGz}c+58u#Oi(Qcb5oj`q=y!aV$-6uD1_ zvDY@^=LhnhtY*>awE)-XrIPHCYSPZdAS@wSEd3CaXew-?A1F?^afzyt$v24+?yPu989v>!lTXT7N{w=9984Y{to zKcwuITn#1)2g}}^q3w6#`Q$9EEC_7o_OrERh#s7+O$fS(5$5ynYCRki&y|3prbkpWInK*rhm+JJUT%!90fU4o>hc=r-<#{%tUO>#YG-9b<- z*WY#4N*sZ4jwd$K&IQ<#V5zA`+xN8BT+3dS41z`PX)n9-UX$VOMcT_DMKYWqu74le z3)_mcUc)|-+R@}&W;_X+rSAbqs~cVMA-VYSxW_Iwax(vfXiO^c;z+}4#xB0@eeL>@ zh5u{qfsytmbZUgXQi~IRzOVH*SCWb2D|3n=3$=S=mldD3jZ3p*goXef+4ZVov0|Z? z=9rf)jxW@nbeyabcYUC72RyG#{6HHAA$9EsT54xYIYeoOoX)6(JR6G`v|1j*z?>{R zXSx`)OzSB6mS{b2DvvACSp2$A0L4hzNZ&~zf@c?&iaGwNS*o5wpCyQ|7Hb2^0MP~0 zqvQ{@k)eN*hI}{RZ*xktCq+_WD)dY*s;4V1E7oq2sEdtkSg|y~T$BtL#SgSA)a9dM zXxsc_k#;NO@=F$haD^`QBCV~PyvYCq0oa%=S}oREMK<;zqc7G58y;lDYl{Kh=9XxW z%Pu7abm`De0Ib#63ts|YRg29ZX|2Z=Y*UhOqHIm+cr~3a!1a)Xn?|Gu86_|$tPg{H zZi+;RV9_t7#aKtUs8z}-J=1^y5-2CxKX4Q*^Q)F<<6*hCaf$X;_%gMQZSE=J;1~GN zjHTM3Cd=daA_-{ve4+Wp?@P5?nq=?$upxVLDQ5GuIfuFp|A*wopli@ zA8T7c{{Q%~)(r?}@+VjbbH(sav~R`d%e1cX)*6uY)!Go_mzm3@jo7hFON%$w0M-n> zMXilSrMi^i^npmCOe1}aiFimE+kKycId@k0murK;LV9?)*4hu=4nQpo+ER(#ES&{_ z#P5hPGh#R{!1r0W9G`Gh>{+hOiL3@E8$`%)VrUC7X@&Mrmucg$LK_f!+|s-y4kc>8 zBZil2X>m$Rmyl(kh4r$Km{zX6fDT5k)UGf<9S(&6Px_N z`*O*hc>>%SD9ziM%>zCI37hilKBu~u#R*0OU?VoDV-PmBWHl&$qv9?|Jpp12>y{4$ z@+;_sb`B+wU%^qh2t}zW0Lm$S0$EEHkP5(*FTLR46^0&Nj>5~g|@yg%V{ zOs0ssRoW}A-Ads;qe9!-bbYDicXy@s?*o#H@Fv;&6h)5g z8=9+KRV??KS-V|xW%3m*{g-8m8<%O?iz+L->5$i3IqigF(*@nnRx@(3?&m9#(d=5& z*Opt^H_YhF!bJ6FT1u1g$yP^|Y@y?3;59471Dk97~f4wSlujLf%5L6%ALGkeAOAZMSGwX#a(vT4Zj~cpqRa?*Ct((MVi8 zGge%_6?^rZP%n%S1HJ~E@RlU zUF+0EGUg+p*GbGEn`Z{Jui@94PFjFJ(wK(dNlCfc^}4S`k7~g14dR(i|AoA) zdKY*Mz$L#5AUo^~PP5ZR# zO?%8x&xW#;{o1oGloH=-gKw7P?de|+NUSgS%mHA1m^l4yO4$Hx2~Q~!^|rL8xk+ht z)tO-mQVUoEMS-q|fSODfcOBA_fKfenNb5@mrym{C5-y)NnasCKq1&ZveHk8W*ms@>nNZ^Wad+1-D%-G4DhyI^g68l8{_=JVTi>`MDPz_l0h@< z2klWj7XE;JR3Z-kAZaJp5p5!!o=3D1NZfx!mQ1M8dLwpi4dm!0Vxo$zsDWs1qxh~y zYX|;S_)%=C+6+Q-rD7w%Czu8L`UPtUXFX!zu!NoyZi{8UQQUJ>dtU562K|Z2;-zX*=)2>dS2FzO~fVfkG8Fr!P^91q2kJ^JM_Wh3<-R>88TpLB62i&jOn=|?j zuy=}z6YqVfFzkl02=C}Q6jI%nE(+mIROtuh) z45b97dky(4RsmRQu&9OF`70pkD2vZ%Pk25-pNI^?XAvvx zu;_9YEWK0WjkC~nJ10IktF^vr;%t&9kp8hW>j!^;u5p9v3xp)7>mYKN$Wc!|{IoaX z90usI33nY7KzEAvb=p7})IU@Ql6r%9uTFc=YpKY;47al&WvXqGmciLWN-faW@A@0| z+kWwn-?TRAbm1rT8EujjA?~G8bcWd0M!e2vfB@=M=RsBncudbTT_T!TASEa)Fc%^6 zm`+d02`~s8C3KbLB!NTubmqs>9gmICC=vQQ6rUS=1kax)_iE0cU=(-zQ|l)p|IlKa zl*yhU%k=yMYEg}AS1D?t_`;lW=}AYR-KKSlm3RO-14YK~nsuqLal+PH*|I~|HF;yP zf)mZOo2^c6+3uW;T9_DlPHWS&wkq?=xeIz#Wz|k1x+hXxP9p*WDNMZZhvpVHoWEdL z$`vkv7dBK$5Feh`IyQN0y&?*zXP_IJesGm^sWjsO<`+SBc0HI7Nl~m#7-nx%8L^SX zb9SaH)u4E+?#9 zO)fk9;>ZhYp++wHIoSZHnLh7i+^Ll6CkeVpw1QuGgEX))yqKB-S;5%gs*2C*S_I5S)u%x)&|auD}cHeg`R zKn&~#7wg^ZrIigx+NUly6vy8FUc99k?$g{s!DIs2A%tE2k9l%z0sRf0EI2O4_evF4 zf~(fifN?h%3fblf@w60YNEkrHLL|?XKKJ`Dv0_i#QIg?P7{YoaWyTpTJ=KKeAx?y_ z#Pr%s3`ini78LmpP&jKM=7U_AJS!Dg3GR|b5n#+zsfGy8)~wJ()w;l| zIo?O|6c3tD4rHl_+)#$Q!^F-|)(elYFg6O)aaS0-2O61%y2)$HriHVe3Ae zYjLHJXI9ZtNaU?E>y`+1d5bJk4>XiR>$V_ViUgR*EsS7X^o?TQ5ck%DHQzd#Nmp{w z2#<(n-3OB^x%Lq2MS1BOR{Du}Gd0%o5O;}#ArtoB0V!Ujc)V5_u{Ii`He0mxFzI#< zE-c_xF6PbDIIE5?3QbrQ;(g@a!pap-dsq*@>R&wUo}sr$oW}AAI8x+@hp&R=ztR8A z9wwd8q5ttdIC}({waObwxhe%3m&ys9;4y4UC~aG4ZFQSzXWnqqpE5wv50Q+R;s8)> z#r6nx=}?JlAYWcMK$SpSlJ+zXX;L^PbVTY&X5yj-rGW~J9}o^rI#~9xTRgiR>Dlq@tK@G;3St1BrhXmE(i7O|VEgj2-L2SFb`W4~liAhrxjV4X zXW$322;0z9S8F|Yhj>1jF~{m1VoowkXt5nURapwyR%>6d3eVUfwj{IOkbm3TuvU(l zJ4Bl{j5}uS5LdQgK2W6(v|+>S+r>x7wSOXjXN!mB#+x|eY_OErP@;JD0A z7Tsus#2?soAQ_QNQkb1y*~b(gDt;FuaEZ;6qQ8$l99xQmm8AX=F;3F`$AAidDjVx6 zfHMitR*{{`uIU5LJ7iv1en!AG9fukD>y2h{@!@oPS&(MWIop3}>5wCTUUFg`E&37rK6Y*tdb}MxO&Lv2A9t?N^}3e;8^Yr=m+Mj&w8vUyA;YO@AU-yst|j6vc8GS zOlCY52Pg`ClVZlV>&30ZQ0x}if)r3e1f)5%3!+#V9u_eBkT)~q~d9% zfx1wF#ayyD2Qz?VUYXM3Tw0~u^|vS^48>7c9PH2f1QW=#>>hBb$BoX2M)85-p5810 z%nidd0!6%H>ZPok3(m3N#U>#AMrX@LajRT)({e$L%H8PnHi{edJbH$`QTmO}_(pMA zu6LuZ-3WqGU7lw>$$`2gr!~4P~8^3ApETD-mEb#l*dxvpW)KYqR1^Dtth zJs(1QJ{X#6#-9J?dTY;@d2V1w2{!#cT(z>)bk zTChp%yAi`uAv)d!BBVlGdlO5xmx_mOV!cx9J~EsX(M>=Hh;;XMB>ai&3k*2B>LxbW z{Cra`Fi;BG45k~5dd0*W#~85zG8G&X!){^8SC5YeFM@PNrFG8kZCJ@sAR zTDX=F{1DPBWV$4l_TGvid0#}0W-U9MlUh;{WV;%mtRvOQq=@ZNAZx%Tr-X{TH}f{Q z4Jg37X(Z`z8U?2QU4dC4f78NS%@qJKd-gVVIn2D#J+dXFJ!~LqhUJ-LTLxV23%K;U z9hd~}#+6JSX)UX-QxUMTh$o;TJ~*aANP;B?$bVul#U$8}Y*YmzO7*<9TM6}~MCrZ) zqbZwN_r^h+{w45MJf+YZ8O!7K4EPz)+vquX$kcHbTI0F^tqH%KJqpvAp*?uVzKw}Z z!pNn|oLXwGyR?)W4R)7enLDosa@2tDv=wJ=XLq{PwNGO1(^@>4O+Il48)?rMtM6b* zkc`tx1e6rt-@!)t55>q6m$(>U??tIqDkHNS=KfyM{XP~ap1hNFO8(XGQ{P+#;k%3V z>V06+S{M>3fLw=g6JIi9xv`0g0Q;jYQX`89?_I2SqpH<^Uv-(PI{7XpHNLPwiE@{` zW!k8_$-lBxoUEbHXTmXtjRt{z`xwT()lgl4+)j61O}a7)+a&jcq&Ne`>jR=-3}bO- zDF;}HgsSM(2gFxnnEyJ;(4CNxV)%4rL`w$4rh#qNWMj^;x8?j1Nw^7h&_8fJS`hET zp(ldo(W?%KYsRuidMqH@T&ScFvXg^w!>}p+^8(5zWC&xX^2HBh+2x?YaQSsdal_p# zBt;Tp)+%cjd2TQA?`Bsufs@(NCydy554*WZPO-}Ab}zd`oWF;~c->fVn89Ym!}&@@ zv6)87RSp`t7sH=>FY7D8PuhL#^45Q`0EPcPHl+1mEI9G%eXMWv(nNv;RD}XBjuQ)M| zwGbbUW1ky7DFYk<1lL8n-FHcFp?h|~Z3Y%JmaOSX`hfJgOpps_39yJjm&;P<2ZiB) znLPIh>l&4BCng?{GQe8c;`e)$T|aV;-RB}bC*U`J;u-{d-j0@f+t7h3Xp~3WrBU`= zBO4?Zs!CtCL1=dOqihh^tVxeyIn=HMt~W)De+*>MA~EYR#$)r8|I4c+?4+yF^F;My ztiS!3hgk2(00{4L~2fdfrAn z$QW)D#b;BDPCeMA?52c8ln{o5U?brZRbE1l8kF#SYeAiR#p*j*f@nQ}$BwSc0?;)O zGb4*h~pz)>lY@?et@i zf~5${Zxjhnfe=-GPJ*QjR^0V8YbkP`VnIW(JOIrx$9RhxUS)3w>e5(ZN}J8F^q2RC z>6wec4sN68;-R$Ha5?Y3r&yPPXiZt~fk7lCV6>m%w1J2P3-pj4J)J3Z6($>)@)r`F2QqVBMEu&C~2Le0R5hf*pkWi~q^)Qe&KY&Dsg;F^PH=J!p8zmPp@f z#;}BB!Zu2vvBl`igfA$;7~XxT0=MH^o&kkSQ+7?(WMl^E+4L}`Z1-yjzhNd|%4EV6 zN}y(ID0@1^j5h0#pxSJI&GD&JNtYOaa`znC#}Q6`)+6KFM3__Rupt9&2AXs-Xi zAXBltp-=c1;O-(Z|6gpFW7%YJ@?WfluM~#6HsHU1!%rIYZ#LLflqtDJ1D<6g+!C!? zW+nJV*t1|+Z&K>vC~>o^NL?O0>shStHR8;(>}g#7{_u0)!n`JyJjZSXVHW&6BOcR& z=Nrhve*a-3F9m}~BHQsqL@9QOqJ5wfjqQikIutN19C!?_pmLD=BOoNUah_vh-GAVG zutJ>w4_Fx$BH;x#+JxxFT_gEQiZ=cQM%F^hUcec5N*s8BjdZOmBg#wkeUZK3uYE^p zUIcrCm3FbZm(rt8nOaGFO!Bb=gNofRg57^g1ib_v&ZlD1ON`7sq&{U!h;)8!RLkxy4nn1oprg`+ziD-;#ES+V4ZJ&7uuEWZMHr(*jelOh;)#SGj&@;s9P^b9Pa8}xU^P<;B0uG z3I#!(PlaHpRj)#XrLB@Rzy!@-B^G9|>Cx4z9#clu5IMj{wzzIQ=+;T%t?}%s>qR&Q z0u(CtckQ7WOI=Vqmr2KE(s#S}Hzd`mQa|iyNXmremt8-zX2%C1R8fq61qnEMKg zyR34?F-$S}j{qd0d6wonI6#8*Ty^X6JdZt^7@#n{CQ5-As2E}RAQ2~CVXw4N{7PCZ zaQsRHrOWfXyj%m9j=mwLOkiW-gW$vj;JQ=9&`imiIGo995ZgA9O>kAeE4_2wI*E0$ zj~5dsvDH|>SHH?ecRnB)p{ijlIA|caJar`P5AmD~@!_;Y{5Oh+c}|Kgud>0>$PIyY zx&2x%t`9_Fau&Ni8b!iXqAEEl3s~CTP-;LA$7zh}dqq`N108SNB(qN$1fS1hY38yv za*nHb@p!|#aa*X>=X&B@6Q$S#k7j;wE7L4(_scb@%4=Y#QzdeWj!^|;Qt=TH)vz&%c zHr0M)PFch>7HP-Fwt5rXwnajJ6Z2gm-g*;K;|lTho1hFTM2qS2%Z5&8JYIb)O)jAp z)r#jmemd)pa!aY)Jkk3-)?P&9vKs@I7|FJA^tnB_N(N^iR=^wLg3odEG12oKY>4;h<3@_Z?*NF67j0)lV3T{rghDovY$E5g zo}L<+gHO|UQ|L2s?PAtiJUAaCQz71&FDGvMeA&fo7qBPLw-pN@7OJ#HgosH)w&8h? zF*OFS;`3L&$NcWSxHS$63Yghr;^+4;MAqj&TO>a};eFZmwD--=Z!Mx0;=r9PI^&kS zP7Ujcj3`raNf2Gb4IQ04d#a5zbjazFF*wgJ3>=($KQISpjLR69%Zkl`(L}-`uueY| z4=-vMomq>pulI`Oi!e)jVSmGXsZB?R%;ztTP-p?f!`Eux}F! zm#_(rKO;rjQkImw1mc?ZjvTuW=C17>xpp6|)GWaN5L|A1-%`k)7K=km0Vv)U;iY&{ zu}CYGb9Z+s<6V>u>T$b|_~99-6-38@XHz8BGxjMpnE}KTH#ym1LA$h+rG*kGgL;Bf zOkD=l{(LE$j3qVsV~puG@#n|vUx|N4QiT_FJFY-Qd$kyoVDu&H6E?D^%6rHsi#!UD zj7=l+)n|-_&vewi&qpwi29qjAl(Coa5o6cjb#uy?2VXM2f+dO#Wh@Gxv!FtL&h854 z>!d!XMtu%cv(QSV`8js=IV0!=qwq0Px3Dy<Df=h3SMTL)j4P`|?!UK|vqB8Oz!hu?P0jWd05AK+4=dQq zn9Tdj*|qUSvLJzq5O1`er>dVzb zOwBJ|#S)X$ka>LZ^hHn_z{g5rbHCWXN{-m=6>{1!STyYc+ z$$2M}X>TPi`l%4tRLY@xzLE__th|yvj!*5hnoa7i=5wf;&+FuT4tfMjPxWn(T9jA9 z!DbK%f@w*9x7wUdT9R@i^Vi6U#JWrsTi38?ykq|w$;3F1Id35sqcMBksC&I&866QM zzWp5KuV2f;QGWDVc3FJEyg*=VfS&i(%H379mc5!fKE&&Wxgn4)QW1ccWO7FUr?H`4 zH;Je{o}b0P)=5ybYn|M1&h_j&eCiMD)rNayL*Rysp}t_l0rXfKu3!T;+$!PSh>qEpX+BKtmx^O#K+#kMB0IThOmdKWl!wg<_o7 zea1$%GPfJH6HJh;?G_3Zt+dUoC06Lr&6ti=V#8+HVVaI;lFsgypx`+6PO*sD!n(V0 z3Ic~ESE->QW*KYof3wvpwp?_pjW>4LxrNNyWq(%5UG~)0hF!L6D_e%|p1Mu#vXt#| zm-X2WdS|bw-HshQcRNez*4$Q;m`9q;2E7})F*2T}&YMJD8-Wd?>c7xJbv3}si`A@6 zz$OHb$J~UAs#&Tk)jmm$Y)iGklO^~Bj^@I^#_Ry!X20mLLk{eFJIsM)LB&vB5UZ1T zib(pBMF$M2OAhJ(<*r=+x!jeZU&vkg>=$ejL5TBTSP)_mF1*C_U+D{P*o9qp%H9?4 zG<%mQYIa_*3p;%IKih>+w9>@JFIfxoXgl;JtHcd|^LMeV_+02Q=$&vVXHp=CaH;Qv z{#66Z*T650Dn%UlYiN+q0kD7F7;DS^#UV;2YMZPbDcPkVy%69H!%Z$WNq- z0!cE8DW9lcuqpBp22%~WLjQ(!=#zaGc?JWd{*26%@)hDoQQbPENsd42)~I}|?y^@K z_+3Q^H)YlpQa&MgsGFZp=&?~;uB64Bv1cHCYs62F%M|NFjY#Mucx)Sx}I;tAeH3=8EkVn`j+z)%1O z`PiWbvd&oIRmvmP=)>R#EE01Mv-U1S?!Emm+v4~mLR|hh?AI-WwT`o$5~GYh}&y^1_fRrKKdCXQLQ2_ zcm0eDk|fi$&LG_d@gY^HYi2haFV^@DkvmuqwL^G7G!s2%%_O>@fY@ZdWj5EZS%6djXEBG)O6W{y_e0HAbeiW4a4QJRj@Z*tp zhTVY4J#t1)Zr0g`$;F!OO3SqavY)JL&^j>|PPsBq4*QMWlZvaa05v@8!IdO%AgwM4 zm=V=&EZ{eO1F|Jw-AU~K4ceg-1^b;1cTI`H84@hQp9l4t`#a0P#!2|2p%e4}kSpxB zKUk)@!d!~CQ?K4XtsdNlLXq?*`;S?`;C5CivxxWpWDD&diyP0e6{(rO5OYh-HZ&Zq z7!B=d<(59}JnQPl_dr^49U#Jb@z8npDBbOa>)|}7#J= z^OY*lhfY2TpWY{kXPBRE%uT(r71gz*`m!VZOT z;*X3C=XZm=Srg7Lg$6@?IB&umh))IeK`a934Vk;` z{XP`UpG1dm_VC{32<3Qqnz@8ALP6q$2Nfrat}$|$#>dEEN{ww8rcYw!Foncnn0^x1 z#mQm%C=SC^BfgH~#Xy!P#q$X`I>HlptK^R)vjA=yJTxTvkVT@8Il%QpaWBoF1U}Ke zT5L$*PUP|6_s>W~_bUXb6`%Ma z5sl@GM_XV#w~23C@PD#Dj}tGY+b1Bm&kdtk5;!bD#@nttve@-b8l z^6^wRqQ`+_Lc6g-fIRVqLkHMo;s&g0#m8fn3~J3E##b$D&58bRn~YUr(Ek&X z`AmFht2TT}lKRkIL=-?GNwhHuQRo|1x8aw$4AS4xmS5#kl!~~%E&sW@s^(SLI+G*? z&ZbC@8k3eFT+m!HQ5?h$jzn{hrtlXrBu7)^kVJdUA!#A5_3~DjiU+*h*U=gmT9Dun zV8%i3%VCjcH;`*;IRfv_Vw(d13>O z2bJ-s;0}CvJAmVLWskMvxTX?UceTd|OcKrxJROe-9r&FAj&wcn%k)zR?(JOLSXiT+ zeY+ok-2%F4x%Lf36gA%L$Ok)%maP$Ab;O6)R{`aJOGJ0#i7`7Mdr(vq-A4gTQ?%&_ z7y<7*9l7#Ub6F?e$+0CujJ!Tog90*ch3&*!44XqnVsjxVyhJ+9hnpHuC|;*!(*iq5 z7&SXs8WGk@D5s?xZY zb{;0QB$mRzk_{HnwLF9yR~4>7B25kqw3T_5`(~wIke$X8F3X&3kR%`+ie9%!7Q6n_ zYm}k1wXt(xu6Z*AvNZKJ%rDxj6kWRU){dP~^6R7Etp!SpcaV;o&66!&=*GK|rpd@L zsVzihL91X=?)ge=?Z$OiWjl5D=ji~ZSFDRsV7+C zu$Bg;S91hJYOwy3d|=v_QQ#N)p`D7U-bCR)>Bk^-cOwjU&1g@~^%ya-gtxw;ss<&( z^e^Q@)y${xH}pf$jfh4HA65yH{wznhp(#<*hxd!D{S}3J>qSH`Zx?BO`6YvDi5L#i z&nlXj{AlTw6#m0ZKsT!?e4c*HYwi#>+7>030G8}@iLN`iSA<;32SgTsfs(!TOnB_V zTW-9RUk&NGU&F^O7tEJ?LkP)-U2wO5Ac8eMeH-h@_>?2pDze#BvyEi<5) zKX(9cEylhCEG&O8k7-mPTX9;5012q@%K$z~93IHq`%lLJeqmAow&+|v8soELg-Hz| zV_Cw&afBHEJdYQZ1JL-g-y3>Ug|O9|-NeJ88_sVSfTcltRK7qsmj#$-l{MiUmF>}} z*s_ZiYo^JXiP*E-gq`qpjPF_<@8%qjL6Cq7S1o57dig+a zpn-)wgT-&%bO5&)^(cY6(LL`(3(u9j3lChz_026~X`=MR6`XWuimu?DFIz_|3b%O5 zg+;3p3_J>NHxsZS3Mu>r{TRTi6k$(>m^*}bZM3lRnmcDj?GT>Us90Td3*f?Hn=V?c zbKr8yYp$5NoNVrYi0(B!@v^GV0YO9bFX%y6{NOwDDg2e0fbT4(@Lu{czOx)*^EGOs z#M8t0RgUu!F!bU3!COnZj?YHHW!G`hwSoOWib1wZ0%J-B#G7@-+=@`sZ>5&Cti2*)epvt(Z*Q44waU-8*zQM>T zSNx6KoA@w$f%x_&phNEn|0q5NE`}$K;@!Lz4(Ne7frA8&0%A_R#Na-T0%aK1-0hUwZ{c@4(9*J%DIshv1TSH@&kiXFXNuz~m} zAxLZ-&8KyEjg(R(ibKaxv!7R`K5zVO{1zWsb>c2;A9UGh23%2&AUInjEK|=>9j?2L zXQ5l;acc`+j<19HuB0@BJyXhFza5BUlfljY>%lEoZ@1scpEC#7U>@yzi|nb&-nf&O zT3&>Wtm86E+&qTgY_BN$U<~hXl`v)|Up4#3SP-2H#elncihYl`>uzq@tQqC%RZC~@ z=GS8euet{el6S=9d-$DQcR`LxH=&XOO2cx7g_>RA)iy0g* zmeFI1aNj34Q1N{Y{Fp)a%ZbdppMQyoeEb1^57zaL2e{W&sYdzi1ALEKJi)YhU=t_J zd+D9P@y&Ohe2~8)XISa!OHOB_1#AwNy4NB9A%1gm4XF4uyNCD&X?D8j7PANb0jcLr z6Oj+|Z=1|QxvV>L93O~r95hbup$Er-on%h4@s46yj`onVxh_XWmz?q>zqEbUAIAxN=Fr1b6#gA# zm5Ru^X#FM;(*{bP`P+2IXKLu&Yn5TF|lk9pcdtj5C>}k@N@WX#MU$Iza%XU5Y zvZn2ptL$;l@TTonN&nkIRy_Cg1sy6E^0jUMc|pZu#l?OKIVp|pYm48dIyCb{E2r6F z#p-|YUQNEIxKK8Hu?|}EQgJ-?x1?K2m2IA3L>TRp|pe_xPYuCg(VO$VZQfoxY+ zY-VPTt8DbQyXg0C9@XS?s+7Ue9T&D+rFarAO9SNJk;U7Md_yW_XUyd;#hM(a-UyMl?fFb2I~rc4<_q z{34|cGUv{wt?v6DmOuX2GuN(c`iI;oTdd$IF^6R`3F6{o@TbNKpZhRF}S(9wg$1v^o9_qJ$Y zkAXBMtm9Q6!65g$59Nt!?U#_jhKX<^Y_0uSG6QI=cnRn3Mse&VK02nvGN8o!V)fm7 z#Z52s9&u(eSxh?ZV8)dk5P2_i9uLBcv=%XT>pGJKjZNQ&d*WZ_38Rq{3`0oZI@0l} zUVG|*;$YxB&il0$6%N)*{#-BBCV~f(!+rA42l;J*OlMJBYpF2lQB`pJeS zp&zSbScnb}y9zFK!*q1?oRyFG*==Pj$MY_BI6wRG74E}&i=2RE_mvns0k|fFT@(0C z;nLg;J0Ue#+&!diBHhj&N`>*;v+NB41{Qj`xp~cw5*XgulvnAhz#S;Kp-B zR2F{%E{tBz;+;nc4p9~^#kBE7lU)pjHB;lm)-HIGuJBjjSkPdnFhT#{EKeYuu$lH8a7lO^wXx&YqG7Q{)(kZpz?#F<;q`|ijU4aJQ%Wrd*Tx9ow zQ|5Cz8e2tU^};TC)!UHZlCkt)8wM7_paI(;A1M~W`JC+RiIo^4nXc}k!4P2~%YQDc`F=fr><6G>WM+Sss zh+x-4AY#|^WPpBj}dGAE?JUPxA!P6z zwH&AC7jl-eZn^SQ7w3(p_ZmK5=wMX$e1BWWwZ4NaUab_C0BU2qEP(@WAU~-H0q^426fP4IkN_@`Lo#aI!>|$2 z)IRal5Uq{loKre6am;aw8@=8G(Y#WmzsEmue807&zbmSI6 z9@m0f`&^b2wf*n&i@0R#v5qL%Zv8@Z z1YY2dFNCD!ogL!mMR0f4`2*gzUN=)_Hn8g*j!BGrlYi>yv`g)h1`i+P83%l2b~#ps{WgATwv3RKfiEJhiqX%WIO zzR6j^Q@Sa;>!PE`jnGT!5r!(j4zeh`l70-vZyv&`R{M5&L~6P8Uk~9y_+l2t>nKKI zXuRS8!e-$j5-vyR6=b|lh`AK}t)rO1QOF}1iR_Ozjl4FKp?xXkyBjLyQarFyArj0t zoEFH8(I@8NL5%F36n~9kWGs*F972LwxE@eN_vl~I130|Cj{>_@Ocp+luvz%{Ok~FB zf6{{xf+`A3q~_^nO0>KW31;D<8I-P<(Sxv-ozoFmNim}vc?g?@OI6G3=|Qt}n##?l zm@z61-DnttcEoN_%JCp{^GbkQdI0p%4;VNn=M&C~l z`GHh;0mY0-uM8-hP3ayzpB^-S^%p5TZYA%h&x?QeN;)rWw#otblB0)Dy!KB#Z^gKW z%R|S;fhs~>q786`1B%@8!9}tWav~%SujG1CuH8y(Tu=;L#d{>p_^a$B@$M>UG%OWm ztN5^vrz;QQtMuRLLDK_}vXjE+=*IvlhY-flbgJM3TszvbswCoIP?kpt{cWjL+fc zcDs0K4Nng6${9&#gj+)2MQeDsMDqv$=i6}C?9>7APpwhFw$)nRPJ72VCU8I+E<

    +Q?(6ulXBQ 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

    2L^0HHN}j4gW6{UtRLP?EGabjy0@R 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�E5;(I#(_etrVfOTiM!NWsK1fP^;ZM@mS5WFbJI}_yPKd-_qu`smCN@5&B>n#?d?g(O^J}=BLe^vdD=SANz ze`kIB3*yBv|H*bcR?37B_G4mJH?-Ohqx(nlVydE2EGzs7=QO_-)%sMawX@i9)T7* z#eTDNqv#OpPc3(RBYdG0sgtg36nU}!M*5~rVl__c=!Z6m4`cnc^b4EBFR}jr=$~#W z+!p7bT)o_mpR7pC=Hs74n|l7F3TMcRE;)u`onr>me-^{*`D?XU{R{Z95P?-Z2x})f zay)!XED`n@@@Ls)FQjHWbV9sa&p)ujj*~LcvurfVdCjk)sTU&I|Esvq>u;jZ{7pRJ z_51WQzllPxf0$l;N~9S6Cgpbjj(FiH4B~u-nbPogZ2iI?;JIQo^FKr)Xcb0i<~Jn5 zVqqkj^G*YGTZ3Kp3l<4AS#w(8?V^_YkEcZqpWm#x_6)*tj2>fcF{_;g?ZT|K_MCXg z2W7D4PjSxYZ>C@VQ~1*SeT}cqGXQe9KtzXjjbE_jl?2 z!ru&mNpm|9z2poEPAn`1FQ}N`NM9f)iNtWph0Qk@k>*wDQxS9QMfz~J0YA1>;YIO^ z-(NH796c)7lS&16RxJ4)At0&+mqfAOpVIi9ON`n9FQ~PIH){4h8Lk1EIqNcz1j`Dz zjS0j0HaA`tP3rrHCA|F)Ji}rsvn&6Im%;MRz6zG5cp#~Lq{^|eOZKbo2!(c4+|aVA$q73lsG7z3 z`|&M~N~kM-D24Tll(@MqU)-7QuN!}gfn@K?pC+cI`@M-9rhyX$U4bS=(-ch;+tdB2 zb$%i{Lb5TyNk|!cm>uq< zX1-ql+*M99&k{igL7S9FUK7oHd=5Rmkd8z!%wHlBl^}OE5b|}&Z;51x#BlMzHG4zN z{7N#Ux&x~c`o~;`VC#FhhDMN;l4Hf;+Q}>;BeewM@PNN+0{RtyG?Guv6%RJ@H>k4i zaRi75hkR;UbnJQa#EM4#yhaP3fFF!Mv>k^h#P||996FmM0?r;H(*F4J3DKdke^`}0 z^JR)MFQlfGd<`+r7e$Tz8T#r4;!I4hAJYi!(25#dqzBZlfO}wMbAnO{NA##UC{W?iuZ5w_Z__MIrzo1Ycn^r z8ancO<`U6_JT-m;jiVHxl;OJFg-;qIH96U zxZ{7@N^yS+e+|8OrFgi7-<$O8i!wCS%RXY!(zc7Cw7!2)yxGFvT3@wFTxj8MTK>o? z_(yPofxy465-nT$->>k-OEQu@zrh52NhGxLPmKR^jZ7AWIJHJR*~(ujF!yC}VAr97 zsN5P-Ix;igCKe`h{D>$=!yzjyx;*nbMatG0S5=TF)`D4)>Pz--bOYE3SW7^0t)rJ^ zA+d#Izkk0XnUvd(I-`e&ktee;SCnf`m@-d@L$PzQU~iA$OO@tHec z1w&^^z9a?(kJam0oIJ}Gi~`oGYB`%SdJ6IR%gKg6Ir{V(WmofhG}{BzpwJtJc*Locu}NaFlC$=eN? z{0S@`Gl~5}&CT_HDc`++i~sb3(Ru2UINi(dt@7PJ(g!KbzalY&UVf#p{$2hx<@Ke@ z#ODM3WAq)%MW;dj@%s1Ai?;{)@7If75Y-0zQ!7qeaR9TyM0uEh0PFPh3ejz_zfS92 z(qph)f?AkJ$Z${La0#)5#@b*+Y^;sKU#e!VS}9%~>^GA3uY@No6!^(s$u)Lk zc8_0Iia!SXt92`QQHHQpH@8a=f~zKTHGQNq_<&eUjMXdHYG0F4j)|g~Ps<39*yR_+ zUD^Hz<4>%T5g~!gtHe9mu!Fa*7N@iQwW`d0N%<_ZF!Zl=FNr!s{JpDeS_8kZW_U_r zzWFjxS4)h7m&Fr9{56_gAcH$P)*cTPCGuOkR;C7dz48hpKt7*Hq%+3GI`PdA*s<5v ziB3Z?eQkSHOc;u?eYsx7f!uyrFTNk@&-U!w08TusZHox?`;|Ot+Wfsi3?Amssc?KF zIMLSXfUA(J8^!m-{MF;1-6TVzG;24B@ErdFeaB|8ILH4$#)>U6U8R!$rqI75FA{RB zL?hq~DcOe8w~E@su|8P6RkR!K_g4OHtMoxhej~=3<-aB-5BHCZzwny$;_7bO{~_&N z;H;e5`0qWtbkJ!^G9`3U*)z>fkib^4bP>G7rlMoMfNC!%Zx{D+UsgV#f(nAPg z5JHF!I*f!O3_85u-@4Z|`?lZbc|Y&}eRrR$^}BxSTIc&->)Q8Pd+y+us}dJ9xbH=n z?C{1cA7_%Tcrm#A>cp+}R=i}Htq&2MzU`&px2qG$`23=v`N%}i>XV9{4b6C{IJjkG z;@0YOUv>$#cUMe&Ws`6~<{-SLrPH4Fdf0ISWSl`!QGUW%n1>aIv#x3u{yF>W%S_MhO}YZ52M zOa2oaer=*_{DD`43$IO_7T@q{F#p;_-T1Gs1}|O9O+W9o;LB?h4dbg`3;wv4uetZE z3OZkhOEKy7;FaqVJ>y@!9vm_XZ+5~PE@#%c2i_<;aa3Y!wfKgwf+KI`+sEv$gR^f< zG)sN30TWGS-#LKC_uMxy%xQ3M3A%YspqjR7!UyK`(T{1fKb_erv4V-;Vs^xl&P>m& zk0q0ePEOGGgl`mmcvbfVS~kpx9Z@tfQrLdQ{;wd-R}Tb=guueg-%I0c~EZQcaD?mq$S=x zbbM2s8Xjx~$or?3Wb}9?K#ZIes`j?9n=saei2HAskiN&$U0P%h{1ogNmzWcuyE~Y2N1{Xg z+ugx?cQ855+f%gfjzr~J@h#JWL+(#>iI+?ddfcDLYq)RvT20=rw_Tw;?dIRlIA!+- z)9z24Rek;pEZhuII3xJ}erAx*W&~AdFwplr5VV{@U#)l`IDbas=)|}Ooh8%yE}@EU zNhm0p2|mcc8$CZO*ft|^X?$)Foc2JXb)C-xEW@X|Jow?6$n0R^ z1AGZtJsUpi_~Ea$J$$1-tl8Cv~NaPfmY*tzFn#}K#l;oz|c6D{j*7o%&$eN;b|+;1LC^sDy6Tw-(; z-up<e4cEsmk=}*=OE`!A55Ix#I=Ita zDtj5=J+l3LUa)*-;;i`6M}vy968+=5<_8ze;>pT{1;Kr@81DIx2PLx-=f{f|2F(J7 zefPrP{D3De>z)Y4p(iIjN&c>#?>q&Pvom9nQ4602Y0G1Mc)b-r9c&B|BjYO<2YqH! z$S;e7J7y=k$JZ?h-kF`~RBu+HOZh7|O^E1{-G#xCa}xR0i=VMv?^zn0Hz(1i+B-{O z)JY4U4Q9;2LEZao@ZKEm$XAyIyD^A={#?-PA%4g@<#|_)aC3kx=SvSCO7MOEm*<0P zA4;6k^Z6HC0pIhTODOdDeiGD!B<`vi%fV^4`A~gHd{!Lum%bCbIqvo4!S07}lAe4q z=<{%*O`A=?;T1qP5x454qURn?^r#j8`}5$h#fiT0-`5Ad zmf*6@`!cw0N#dON-mii;mn43T@7oX*7jo+?_$F9im^i0V**C6sTMm8`UYyEo`nOj2 ztKSA^KEsXm>$kz!XAX#-)kGv70v&QJLp}JS65D|6ZwX zMI67yA=P$E@Yd2qUf0zj!^6$BLX{4_Mb(JkER8JoaWSF(Dz!FMXLbJU;1}T&MvYVO zUC{Q~M6Y`HY_*)%hlugozP0GVXA@1c;?s8oFE67zckc*lJeO#aKd%&H9$opd;A{CB zF?GuCPCby(t+eQp=Mw9()`y6v zNt31p1#c!2)mKh)arDZLX~9izCeAC^FdZg+vAXYg`_f!R4Vrp!_5Cg--8OLs=ny^l zVNIGhBdD=DF-Q~gb*mE%>%IMeGh@bC7b2#ypB@MvTb)S6KYuXz0GoohXJV7?Fh8y7 z!r85uN(aB-QhJl8NVCvt&5r4xJ{R)G>SM)*Q=lI_n)Oq8;sZiVfG%-*QQG1LllCl}?TR7ICZV&q4L@YQ`XK@pj_E`0o!F{rGm`&kFJ73xm@4 z65~!-{REcUaM;8BQ?O22_)SFDR44_z%*H+0pYUWb_kB%{PX=4w$5njjsi1iYu3+g? z!L=oc4sDh!A`kaXxhCI~rKm>l2=J_ldH72~He|zp1!Zh7lq5PG_S)0XRW6;~5zD@G-m$ z3g(EY&ETl0@ahN~wEUT1(1(deM{STnC3en!=Xq8@vk^FCt9dudsa`Us!9beXf^mYMQg@a#v4daai{ z2cs6eE#SOQU2fC{qp!trjcjojEX3aDg5N&k`}B{`7hSzJ@nq%rTW*_%P(`osyXzFVzq8OaTA0pglRmc9+rdc?x@YKV7ezS;hEP~%HhgL~cyT78*l zkeahbNtwh~2~@Z37izQ`|M6Xy9T)%V5W&6rp5s)+`xd*k9Nk%B+_&Xu!3U0GA}9+H z`R)GDalbdFhd&A--}r=7my(deZs$iuJHAY09U6aVYq0aX#QCX*x5gjXo^^P=4a_?nO&hi?_g_m@rZ+SbHz zedq6hsu`|ty7ajPsOy_zHMsYb0$e7mgzTX{Bf2$=VI1~`2yXWG!AaW~%n!d0uHMGg zGj3-vb6a9ywI6mWI|lXcAA)_`5-oZ>|AS@wtx&W{y0Q!Qi&gm(cL~s%*#fR2PdJL! zytXU2YJ1|;dJ}#ms`|kVu1ok&4OsMJ@XGeY@H&fsBCbjpr%4FU{=^-LiSc`P2e0m6 zD|^-MV9yTb{KxkM=awc;t@hm>mB+Z<{Y%lD(!?k6x>NrY!*uhd1Jr5nUqR}J#N_yw z`+`GvCC-jNvp=|GS7PyL_Z`GCuFHR}P$hkTKKMNsm$aPnn>GCYt$He+du?r)j#`%< z{5a_SBhM=4jr&Ad5xI0+@UI_nh1QQNn)zemxtj5*j|M&WB~HqoHy;xo^6~WmvzO)v z)Aq40EqN?>WnZFCe8qy`(EZ$RYaS2A?N8hq|8!w+V1MH5#!o##D)r|p0&EJnUBLGp z%lK@4A{ccbF{|IX<6%Hnb#eaFKXyn1fVU^>* zz7*V1DfgK8j-p^erQFkU))!-6lanhS9DGp&Lr$MCRTc*aE9Lf!uYWl>yK-($;?5OV zgcuH&CUMg6_;L^p!3(PoOx7LcUM03@)sadu;rZmBF|wxh-4W{U72r$kPQH;M3yQVyJ3< zNJZU>iHAj&EB+IFRwehW_`Rv;8MdPdHj;<6RR}z%f$~`*1vLwjK&TSMg zD+x}?&dran_#n6{J2#Q@`v+uQzS@O7z%YFn6w2zS4}%Tax&8XA{s=4A;!Q$z!;3@@ zVa%ab>*uYtICpm{vvBTb#QvkWtV+04YlE9>=QgZ9{$rOV&7Spf@EA<|^N)kqYUeg- zFyj+qvcq<|d08#ydVcYf;P2YGBjT$*ExNK!Zb60k?_UK^AD(+v_OcBmsN|MRrrtL; z1WoJZ4yyk9H?9B{(HY+c)9U56tMlQvFl-VW{F{GVjMFy;U)0OZi?7;PbjT68kJO0& z{#UTDS#Dmc=x+@DlMlBfeIP>j`IiDrf$jm#9x2rX!w>Nj_d!+AcaDOYf-LbhLgPZI z`o85IiZ`S;aL)wPI+uqZ+o12Uxs@B0%D~MeK1(}TiRxsB@GcRwr-7j(a2|2xmU3!c0`*wQMuNxdy$oEux$dhXf3?k{SPpZjIS z_}32yzqQHDP0g4~2F_7f6e7MYtPrYnaK^zW54LAxFXWm2cL?duKM+!%;ut*Ql5@W- z7OHq~yK48Ua3y^;Iyeb$F|=W3-&Kyr&vWV4*QU{eZiAIvmd-0$);9OwHRAtyIcQao z+akX6<>35++(dlAiivj?UidFkFe$r*(kCyfG0e1w~>jQWb+Q;+q{}q4owxIi?`S~^e z@dsCfJrnt3qca}O&#C$phitFrO}aM*FFcxmLFYxNPe-Ki)EqZb4cmmjUeFISyK^o6 zLFv2oYpK{`te1Fc7?fPzwqaegGlsfk&V7*!vn;rIe*Se?4U4{?pI?v_|LX6eyaoC7 zDq5rr&pUFqT?gHvj@}9kk5nLZ)kZwWk#re5esi5QH{JvQg zf~kx1lktO1f>#&kcdfl2165OU0DLg0y(B-$&vFWu6z1pFZo=<0E`y!w>(V6HT$ta2*s9Ou_iCK^_o%a| z`CzOi936|D7TkMbi^jpMXYy-ST`@ZRfzQ0){-yc#gTf129NomYm&jdHt?Bx!L7^UO zxUj`p@fD+kyo*}2Q8?(L7VXN@^{*WM@Ym9lf&1C?4>hssdD1U zkSN!up=-b`!Ssyy2hj5(@egL`r)Suoh2A=1-`DA>bOF`_R^YQ48OV2wX7Eymu4yFN z1Mi`qgjjyBf%VIgs9vL4gjfZRwphAG@U-AxFXSIlvvpslt~K+xhxgZf;orfm7xEKv z!Z%*XAAQCP|IUtGg{gjEp)yR@&+ntUp19~~pT#c#w~FXFyTcUFo| zSynW$B)@h%^>+W9*ayfuyht}?~%R8ex$G|fZU9vkZDMO%tM|;mLSWKmyuP-d&sB#QnVVd87W0} zBflYkBQ?*?iPc4NkVZ%XX^nJ1x*@%g^N>Nv705_r3^ECshRj0dA&(4+CSnaG96 z<;XDPHsmg31`;6ik>`+C`Uj=2$& zis3D4`fuo(6-?jnZ(-99pr4{*{-56>cWJqSy+$tc&!`U`5O#y+s0N7Xr-C&N=|f$d zxL2u0^x8$;-pFrSA>aPaOCREnK}_!f)|#o0^O_JyC7W)9DZ%|#wZ79-CgE0EWb zHOQyPMr1qkGx9qUACVKQf@C9yBS#|5kzC|>5p85T#8(Qj6_BwV@EJ6 z-36G4Jc!Ii9z~u)o=1w2H<1$L6J#AyP_~g->E4RXhUUbMMvg#QA#ITENKfP}lMWQvswcJ9}_y4)#CS- z#|LkGlHWXKynYQJSIY7oLB4W6E&fE>tf|y=^?^>Zo8AgtQ?KcTc%o-S^n83@{qWl2 zk4H}`3nNa1&?HKOZ}~L8NgdwoRW?B01MktQ8Otu?l0r#}_YKBBinuNl$xx276L^a<$EF_?rNt*3tc zu4TmX*L&h+x@Xy{ow?CBfUDv z`Yu12GWrollc#8RP9ob+-pU%M2D8?6>1^HB1p~PYzxeN66T!_R`W57>)!V+Uo(R_U zWV){AXuJwIL_hJjFM4M|D3R&=niNA#P)x*jcl0a(Le z`qR=Wznpjh9BtuxbY0+KME?<7E23x919Gs;Ub=d~^kQ^f=%%kiPer?M6NDT+ zGuVn=FQV(YP49@Vp^UchPV|0}_S7}v!XVn>$1)NuK#$r# zo?)+BG1`OXVp{&yxzII4=0aX_NU&sE{*%X>M;If%8S$pe#ng4vaZ31Ip9|7WAsvhE z+@9a!oZ40W-=)&Zqgk8|uhcPIs*RAwNE4(fGP7UkWL|=qns^qk!IblS9z|+~ zH(ie#T10eB267-Reii!B5&cbc^{K`4SA0{hE=Fv?Ky{lkDlAZcK}i)beG>6{eqr|i z|0~2bol&FzTA|sugq_-rPF4j?m)otY&UAIIt}@d#)hU0|<+eA8=PIVB8T)7j^<5yU>+hXM^+PlC z{p1&^U@CUtpG53nhR&QFP2m2tUbI46uvdla<%C~OHe#y&IU_=kGo zw5M}Pjfpb-hYJ7H|0G*I^|onp9axL2>0`heVAHR`UTfjZ;MGpaBWu?`Kb%WyL#w&< zgPom{xhbRXhjO$x)8(R-(Bf}F*U}u-RhsE?=`;yP^^OsJBJolC@#uJPsqzFO)Evt| zdNhGtz-R@ecaPZLpJ9I=x?DudPkJ=H^xAT6%;2Ez#xtSccN0_WG(AE$|O?-8p7omZ9qkj>i8g=O#+*%`ng;8jY8;5!Lr)==uXo zO(GSL9<6X?^k{=B$Las5L6r;x>CyPA8G1H)lSqNmqxO1Pe6&F6(fB&441+ou28U(D zOIHT3ot4c&NPJD-1E%*xmv3bH^BM6=GvZUaZ`GxiK(}2qgOf7i+h)W+k`a%?`j7O3 zGvY7t@!pq8#eSegy3ftIk{)%oHW1f160hl5Jz(|=(X~F9uH_&ae@908dFUFF(4O(1 zjuLe*CuHdV_6a^{6fU&W&^3{mJ`t=hw^o7wBkxf6|Nqy8_{Qr)cU4>HjmRge%Ud*E z+ow^z0zIUeQSnDz0I(&&5_|{e41(sTC6g(mUnHGYcGK1K`c7>6+6?`BbS+pGe=tKo znvrfCiErn1yA-z*$CtGW%dgEQWT2tehc-FUn+K0Ynjq2wc-_d{xymqFdq5CdbmV4f#9zO(0*^Z5~*Yf5v1*$*d=uk{6_ zd^u&Z6LS5-$@6OEe4pdo2$3g!pA&SLoBUUc+aWbnBIiU5iapka4GwniNS@uI?#`U> z6)=!Tg0*@nVRNv)B{xA%+?f;fek3{d$Oj?Sd>Isa42-SDAp85|mz@iKNY`hO$1+HH ztlZ{B`2T>ds_TEq393Ju99*mKE}r#bs8Vj+l@r|hXmWDrHzAMlq+!(9mI$BqV`!sN z49lj`k2%4hACfa$-0cmN^Fohh@Ma|8mLGFwJw`8f|0!LItHH`tpQ&Jtu<`&0vmZ-d z(e_J78{yp^%eeB-A+N-S#UK51PSAQm^6s`bV_?mi9l=E&Ype__;pZeQU68!FMc(f8 zP;~cL1_M1-iDP!>1d|@;GI+$6GoxANx_tp@PmBSq#tAX<){A&^Z+db5O zcXD&bD}PDne7DESWTD5Z#+wm-OT<1acctU0JHnc; z@(8e1yX7A_LCeL-OKRN+DPKxOKLEMxx8#VnU-(pN$WLI+3HoG}rElaaVBN_299BjH zmn65f9SLbYa2Hs&7!zY`F<2LeKJS7x{pz!mggpzBt!hpGGt5~f27l%R^Z(!`eH&7b z$W^yJklAI)JKE;|mF~kn9uprM3D#sH<9~x~l9}*VPO#{iXBK7K3#>uWXEfM`ewxRsbD_twc^7PJ%@+q4g6ETG)arjQT}tX8ML(aMdF)b1 z4Y6|gz+>gG3vAPG<>3Cyl6{*Grj<66jRji)6TtezsmkKMSP;CBY*%wr#CB6G`1*z9 zDK(GI3e%`Td09c{gUN5tyb{hP^r>JqOrOQxR=C*PDF2Nfi?0-~U%uoV9S?fFn7pdy zbU4eeC_Zyz>$8rmRv~TH)MF*r8l3l1vQN!_LE02Jy+SbYrR3-~@4#tgS6$ae5|w&h z8SDY8PLjha2DwGa+Z$X}F`d_Bk7c{4Vz4pZ`uqkJ4@vjokzl*KPB|p#TcPziHAi_) z!c(y|8Q)+$zXBY3zLxQi1lV{aDs#5*(;b~tfIO%2j{3bYS$4!-jf4`i(^UT-a z?9Tq&r%`?OfbCY>7r~V)*LQc4Z0dn43-CFra?oQ%^8A|jz*!kjS1wwzB6($nRNX4| zWA9?5N`K?A562IJ6G zp$UY#O)BEds0G~yU+r4h61@)kDTJy(8KLaE`S%WX!F7>-I-z=KHlZrmZSd$31hHl4 z>X~A~YLp*qjho^!7=FcQ8I1M%?Sv|zBA$mT*zLMet_My)7k?_D3Ycs0qei><5_A=? zj!-RaiPNF5+s&g#62yk0A4+&FVM-U$CU4OFiW>*3iyGorsDP$~DqsqsD$xDNtadR}wwD1z7;bmhM`1yBoHvO=qXUV}%vE*$Fh5roRP3+u9rAAXH1 z;8t|4pp*R~KA%tp^csH6^#rkE&%frwY*t?vf9=r0if@Uod{em|%qLU{0B2|{-b`1n2}Z+5p~Hp`?kKGH9jy9s62=Zc|M62!W*2r7La zLh<7Xm4Bb>!g*k><9Ry%I6xV-WZBbL^|?9h;sIVCNT{Bf>-Dp)4tuC17F<@d_0g%% z;5C-t5UL?%ges`-$jh%Lh-I_v>0)j`sQkMSy7omGN=moy?)pg z1hH{mpGc_qO@uD}_19=9Q?W7#-DZ_p(^NnQLRI9P>xK^_hz&wl1+F4g#tR9R!MTHP zaM#jG&%a5i^vziNRH1W+559>YHV9qmh7l^?bv`_I#I-bl8LSLKHOpeLlR_2?)2*uxuu5>9v#jhb$0p|}M;o>);s{z{x zx%*vS1YE1v4AEn~*!KTn(0BCBgBAY+@p{0VnZda?kGxLte-N*hWybd(F=UM54-l^* zuE7c|yAIXD2K5|#ZBbcv>#6aSt;-{cDJWdBm0#o9QalBUV+pGrY3pwxWSoNwj~*9qw7VerIoy;&Xluv)%iiG*!@}6 z53BE<{#CD0-%XWSm$$iGQ%@WCV+qA;@qoYna>5JX^<7Q8`a0{n(L*)W4TslECq7&J zsLKZ{pKz-9V+G?K%$&r;^ zj{C_UR_f?MMM)t z_`$i12+es)pn_~{M!;)X65mX`ADg@2U0=iN;;^v^;B_&`zPaPWv3U~ST`ce|;8Qj@ zMMSu(f{2z%z~E?X%q3Aq$I;_2sK?C@$%9PLzOJ?=*VsECtYM0kr0h1W2OKgIFkFkTO@VHAIA$h%u? zJiJCxyf)Bn6wNCV-;oY~N!=({D5V)4Lpsayc@ZfAV1YA=#8-m1N#aZ^+Y_s2y3t&8~%d zB;@JL(~dO%WeQ{uW(`N0(^3f8lUXCo(|al`3!IdPDZ1OG8WVa*TvLiyjU0^lru!$iDRG3Kj2ti&U%-wjW*U zSo0`KAkU^t9cgaFF35A}Qb(HWP`yrlUT9RX*OBHgG3h(z~e^XIUYAop*)PD+BE@N_cjI3_eRaY3p_p+ywKz3;3qw90bb;B zOYmZkj{_HaoB%KNI2XJO?EKI?l;?d!EAVoU^T989oCFtp+#0;XiR^XW^?R~^7 zu-#5se5JgD(y3Q?c48HviWF+thm) zc#zMuF4z{2!@)KU*8|%Ea|F19@1Y#9&64%OHr+M=-|F)`61>Rf|Ar_w#U2G7;*E|5 z+q!TJ_!iGM0^35;7;Lv$6Yz3x(-ds8XEU%($;X0iy=b07u?3?A*cO|XV4DGt1KZ+} z0NeeY3x3rXmIt=$rWM%kpnPyAdWBU#mIN>J_N~DM@NWJ;9;KV_x;Eg`JZ=l_;S-+# zKGX9jf_r=14s3VFNno4X+k;0t}kC19J~E(IU(i3fq#dOR5X zxyM7m14*mt{Bm%K=ZAtX^?6}R)F9dP-C1}>k_z&EN~&HwdKY!=G_zu^n04=$g%z)L-U6xb%HMqrzW zT7Yf-$OGGa&;e|VQzx+9P2IpLyFE@r32zDR2(T?G{lM?}g3kuqMJjB|=pbQ7G@f&3@*hg%-WU1Aw*LXRf+E<^tey;a2iV{|<&v-F>!H3)=U9NR&Skj999l$Jp3*A$nKT0QttD%-s{brx79m_8NU2(ce|{I4HHWXH74v>4G-QPV#Kca$FdFpCE~dBOCBZH~!m6)aoW z=E$u4py0aJHyxX#-hBp-yz;uy*IY5Q``|Hyv9Ce7R}uaqED5U3=2s!DG1W(by( z=%c+lRiRDASWst1n^uj@Xfot!i0PBSdX8H?XgVj^BK6--`+oHs&&T2%_AMwCy6e)` zq)_&4`R@cozZ-9doP_9O@$J!1MovLaMLHn*G(rSB@?R&UGtvdgD&Q}PCvR$g&Z*%B z4_9dHIea3=yWj8^lpBfB%D0xc#-{I5^vaQ@9eEbcJ9UiQBDEVloGoZ95r078& zEBYp|t{L(7dS2Nsh}gW`AqXB$wyD{)WBQCyyNHc1w?I2ajcYuE$DiSo~Gpg5-kagf?#xZ{xN#f-7_n zd5w`_rEAhX*s&maOY^bB+ZasuSn-QIR(Y$s2NNIX+BodAbQx_<3x0S!xwF$W&&h77 z$4dV}#CBJNuXcJmkHqOga(8k|r{^Qw#~v$>-5x9NT0O#a;`2R@gZuOd`tC_S)cmao zw>4s0p=X#*c{J%6)cPfPQO!wkHXhIR3@&~uc}u75aCYDP1=g*nPqj1Bw_!uD?m6XU zST=3X2$H`hUv2X^@frigzu~dsH-nESRDJ858E#ODZ*yjF%--aEZ2~wOyO;h8*4TaK zd8ONXX0UN@^2`RkdxiDs3U@i!>O7`b&|-1&(wc9;X+K%zZtN92`&;s=Hcfk{+i(i_ zFyi#-1Gf9NKUnvzK9}_ll1q}?+Z0FAeFRqjF|);f2dnP-9Ns6~WEF1LC+Ja_%&)lt zPWh@&zUvb_`Um%9owKw-8P==gSwXF`q5Ny-DVdZhc*+J!H$#ZH>Bi^oqrDq4j zmL;#OSq5i))%2X;>t)HEXWnp5IESdd0a#<9&#Pb?@2@>peak$S&Ee;!my)LE2DP3~ zo>_A~oR#tZxxujKncx0`v+=0kKRw3n!L}Bh+dt@hAbD2vXW(otS`F5mrq3r}t=Ou= zSN+2?!g=SVZS&3x&j?=yXY=6L^TIR2&%>!*YU7t+wM(Dhy{&M?^FtfupX;&sbHFyI zU3GqVMtFmdSAG>P&>7+S7hI4wyUt@JKRighl2p={eT(p%-L$FHG z=LF9ScLb|OlFJ5!Cxl;uv%Eg@ShjlygeQdiU6}5bVPM(nGvUJUgz!qwsobwG3{MC* zy(oQkw!0`iAv^-kMrZs*;R)e4;jHX8FACfV;c6GBXMrYQrO~G&*zT~N5qvIKccDH* zz`CRL8F6uVLilqyE919|{R!bg1JfsjkGmwqO7si(Jwi<%D=$r-5bl0zxCwR!p**8v zLfI7f6T+qFmC%18)Mb@}OM}lIvlGG{(8YHrbk7ym0+hf`2QOr( zJOEw0aF=+yg+AU+2(R?~n?7uY&!T+ngm8a!rN5BSoe-XD2L6O_3A!ru3860demE~` zft?T@gRTQT69`q&?l>>9w-drsygq|aJ+zfj4d~`i2=7Bz4^+e3I>v`~LUl_g!}#!oaCdZNcqX9=9#5#AuoJ?I z&~SS@G`HzNT`gbutK|de?oW-x>nB5{K~zH zP!+Kg!WCJCm2Wj44kUE({)F%tbmg16$%9>lD!@(%w`7Tx-pYsL3DsgdAzXs4iFGTX z3L40A>f?u?#ulRM3+AhY>cK87kv@Ku;wPcgGpX1VfNHXqP&Kp@!lmfC`1TOGYk>7p z@peLZAi6RhMyP9`18bq|?1b=ibfur;`L%>Dy+0vb>UfU-{0vY=V^|0^R(3*oq1P7? zswYalZYP9u&I?ZnH)gTZSRO~HhIA)XL3ToTAi6H*D+!hV6pQyKgcqVK|ECF6;jt`& zYKWZ>F7f)Ogo@8&$#ePJ3E}P#y3NidQ~{F+RS`QOT!^j;yhNyscM~cDJ0V-f2xYK}P&M0X2L6QbK6JG#cCjC7LKS2ugx8=e z-TOYi?!d4Dc0xE0T@5&%kOwJYUWDZ*gxmZNgTDTR@JYn$gm7jCc0#xt@v2Z}yqyp} zlXwmBh2Cz`WnqKTCxmTXE+(d+?9*)=G_s|5JrpN|r>_%f>u(t(CxjO{($?IZLF$Ks zRgScUwml>#gx5RLR@VWLoDkmaNS4&_gz#8cP6$u>OeNVmIvbJ`!t)(zi)S$;CxnX~ zX)ES>NKOc^b)+qmWssZ@E_I|WjX8rgkOkvDS3z12-3j6Lu$&N{Ep~s_fMMaG&H=ER zD((2T5f`xEx+Ho%n3=qb_$RgiGL+ulRqQ5dM+~dBTc_ zo)G>WULLr3Eq`_jwZ`z!PP~>)J0W}wymaw;Vrhp^+rq0N;pAfc%UlE)Tp1)qb zW2^mGNKObBJJMG8*^pc>YaMCJdNCv?gi9T1Yx#OeP6&_tQhC|}UIxht;n`nq(+Oc) zwR483{RPXMsV&v*AvqzuM&$miZZx5(ngA=8)W+sY_$Hoj6!O;{DOm zS@7zb0VyJs&<1A-5t{RqKn2;^(ih>iEQxO>-k%Wu2;TKIyeh2(^AsUywzm<`DZ;c**e zX%0s*BqxMtJJP(2^+UM-I3c{uvF2NpVaf^NHI6iYBIj~6 z%i%SQ;@gS$x7b*CjiUII#QPJ%cfxBF#kUu4Cxqt`pB6T-_js&nKDxf8<0u$&NH<5+Wm)mb zLU^|$bG(aD49N-MNt;!Y`7i4sIUzjXk><3NL2^R4*pcR`<{4a449f}ONn2HtxdZDVIUzjXk>&%GL2^R4*pasI=dd<#LU^qsZMAO? z$qC`otwkrTO`cQtoaaV_TR-|Mu6h_?n>iI{}Ir%z9?RzBUzgC@Eij|OkU>5hpbUx0Nx>$7cQ@XABUd7bJ{N2obqrOU$!!3^!CZ``>oI5$(@^}c&9;Cb&iC^b&g8bJ_ z4!1m7-xGRRD*q&~JbvM>V9gAw%h~sYTOP~c)NaME@faA}bWgbDaml^uy4>oq;ve={ zd98@>UxKZkd+!amJg%OSesnftO1R~*5zZda7*@UqBK%QPLmQQ0ST-G|hFczw`FPcR zmB)(T7)f_vYPjVw=)QCvCW4ixK2L)+uFB)-`@$`c-{9==VfAStmTgmymDaHMQ>KMm z9y5uzHZ70fPd(Nc8CJSqriEJ`J*TI~V35a(AM3Hon?60<^7szU%81_|Zh2gGf4ZM; z_gLv4@mOtJ8R5T<@PFO!wmdrCIU}9#V;(DyRUWGj8zcOIi2YH)S$mQXH=p@HI*a8V zD~nG(Ru;cJ5bk&kcrZO2sR!MTN5_}o?6!FitXof?uRT^Xc7ZiC%F8e?Rw2m$HTl}{ z*UwD%#0-xW{T%psLS?Z9+=x)0cyRRIq~SeveJ zZNXOK?m@WUF$2z?e?J?9`yIdfG-|=2v%{NTc^(DU*CJK18CW;1KFQhPe#ay@Yx$#K zd1CsW_cmA;gg)EAavAlhG$-8eD4Y}KtMXo$Ci;9ngyB(K3lD-OV zezWpPpWu&JTAxZiBPw>OrvOzW}T!1sc<&a6+RwrcYFt@6j{sEj;&r8Zg(_#BHe{2f$fGm|A}zBV~Xcg?vf|M?T%mJ?5eHwWVqdN@{{S|>HlQ7 z-SGgNmHohz;daN@aCS5Q;?t-;HJ(b}VL1`p6s)^YpElsiA&i~;RJh&oB%GD;)>D4F zqxGWncE_I{E8QDUr?)$%EKYBC)SVbU`KeE+PM$=l8OF9diqYj$zDlU;s+6#T;{6{) zsg5fnzAhn8kW#Vv041>Pj#6~h{AWUCoQ?0I9<}X`mgsu))QV7cV|=`AcTDm813uhK zsC;a@qZ$5-(jQ0YwmU`woPpo&n2W9oEg;lg(gC_z^btRr;W9lh~HTn2u-<3X=KM5qE9;c2LXw%ySWUHk=vDxlcn z{dPwwx(e7$sFrrevruT;9pljTCGs9Zx81SN8`yS7d)x;V(3wyLEFx3|Y`bGUx(e7t zsGezttDqLzc1H(vtS)Y|*140$tpHMwv+Z|)k^(5*(LRGxP+u3%Enl>#>SxQiNt+`>cMQ*NFVREI|iWB zGpX1>fNC`GhXL z-|i@OJkK&-11O{Jta=(N+wK_S^|6HNnPRWoc1P*_aJyp`LrMrtz`R?{%`F2NL7CF`IC_)(w z@}d1fl=0}waI)u12vq^w?%0a%?gl~?RF}mrj1RXv^3Zkdv>{adD2uo4j!6*8U^<~H zu-pv%c1H=iTDF$Z4fRuDMz-BC8(rxhCRF@ZLRG-FJNBWg0a=Uw$96}Yi2r6_+Z~mO z*LFu{2DaUC81ZUZX1r~8G$39>e4Mu%uq13ydb`8c+L~Jm$#%!|T}o~XZIwcmRj|mBwz{^2WV>UPBUw_z?T((XY!I83sPc^4 z?kE+zKda>n;S=hXu$n6ES?pQx$D)hZ;sNg-sb3GT?`GoF*Y+%SJiKN)@zL#$x$w$Y z{6Drko+d)Bup*+{9dE`2b8WTn3CVWHq}?KIg&z*d^)la)wyaNrWV@r-k+zmEf@Hg6ts`v#UjxZ@ zN9peE+U~GbyA+n~j&XZbk}cI$mb&eZ*&_F69YzzHs%fxtNo{P}!8h@IqmXxFGXP#= zCHv@h$CdEvnwBXdl+YfO|C_Pe6@LHC{Hxuu-J0691eGRXR!^Y+XcwG## zZ|?Z;Ir$s#?qY#&0bjn|@hK7Rsvx4J68P;7b4ipD+a2?NRp*!=(i4*Hj$%ie?=c*b z?T)pMG>2mnB-fGVaW^8v0JEa@!q~VA<}N?O1bw7D2MzvCNU)>xr#_?7}?lNb__` zA^C$Sn;dEGO_gPCyQ9pJ=F_xT}nTmIgOp40(gk`&9kz>t!84k&I$0|pb;-YAw zo&?Eu$9hMa|FQ^@?T+1!G^b?^B-S z?T)pMG)JT-B-UwBhAfN1j%;CGDmvfA{AQ$%XY^a z$C^h`3dtWt+2lxbBdR>_wmZrkX|6*{NVYqs|E=Q9U+4+RcE=(|nv*ablI@OFjx^68 zH3^pOj`fZ;cVH1D+a0?dX+FRjNVYpB?Ndp%@Rvfe-7(*hw%S)=0bskMSmcgT&)=LK zlk0poqMM>Vuc;YLxDMeDlBJ1_VBJjvn`XP8v2+D%(rn6GsQyK$1iA}{9h)7yjxaBd z>lV@VsxaLY7XJeAT4zjOj;?jg^hsd%whbd@LbQ$;%mvH)vG^y!$3*n%6sWt+;`ORi zok}*n5xN#g(@#K;R(PIXdljwWqZu{sN@DehWzYkx$=URC!HI}I0vxTtJLIRO-|W|* zM|xZlk3g-!S?JLM1N6p`c)c`7L-2*ahEf5Fo~IgdI#^zl>C?d~(DaMID$w*{ zU@g3+zX(j+B>*ZHCa8S%UAu(F*KESBotEcW|`81L)BLE7C%F2xc!` z@%9p-pFTAd&=*3tk!5%x z`f(Y$E<%eROuUvO(=SKYHDdaoba}Kvw-Ohv(Cz5H0V($`>p2j*DlLQOGxU$q<*ix# zN^rDCMxm>Kp1rbT*ANayQ~`U-^aoT(eu({aaviv>)A{YBdSDEq7Mel%o2r<46@M)m zsKslUnl*PnO^Y=Zo4zPRXJ&IlV(|}W=-bK9rBC?`^{zEJI+oxYbd9O$UuEcJ#A%2u z{!g&(BGXUBzHvmq6kQjs#g9bSkf)3o4bdcGFb!Q-t;N5Ft|5x*^1Muci+H(qroWBe zIHLasR+n3RQ+kA{|MGG|?}pM^UjEQo6=&j9})G0>A!$A^he(s4z-3x<(RIKX&TYLLwD)D{f!y+H)Pmr#>T!*d4`u! zG2Jak2X{AZ-8p6D6hOC%lv9~$rEx@Gg03sV>{n*QKaPH4Bz{JQerkq(ennS}RC$J7 zA#x)b%tMb>;BWNfiT|5dK{O_6|n9{ecoLY9st=7rwdT=b>9uK;&Z{8Y{Yl=yz)IiVsp*A;Q^2@;Pl8q zwmaVq4}j#nm)_1f>Amm($n9{tSYoUk=#iu-0Uh^XSf52AH^(El}kiH+JcmHnu zAUpu_CY&Z{m1`In+Y;fkJ`8PChGE$>`Y=2Ia<`9H9t%BI{F{+ zo7Y}nr0agwf595N$(~obMPGzxKw|5|`gDam0&I0|xjsAtawDAPK$ZKz`tS_M7e0;J z@Do_~zdl)CrtjM-VBNR+9QI{+24p0hwf!!z7Bi;Y*kZ7*34PuLYfjc@=a=Cbkm+BA z`Kr9&tME6kZ^LOWSH1Rp75?Tm|Lb($_3@bK*hsME4cY!1Z1c&4ufszi+avM6d8~95 zH-yhxl&&_|=AK3#%O<%Y{LSm5a5fJ<@3G?Fity{fP2toI!?O8%L-?E5+rCMk0(ls$ zZxQ+wfwiuv9&5oi&l{Fa={MmikZ#|muY)1qhNnOZ;jFK|_%=KRa@@vn4pE-Hz#0pE zMuTm8(XR)^xF7`Ic zf1}6ZD}9%q(~kbmzZVD2@+AWU;EZarf{A+Qxr@Qb-u-#IpZ1=Cl@tn$?w%xxL2hJ|r zo!k9uadxDKr~MB9S{yiQ$4xujDUdT>gR|TDbDu`_*#ov)Y+nRdE)8!X+0+A97T|MK zaQ|toFQ|DBloj!Gsl65F`}84@Z#-6-CwHa~fsFbgeF&uNop8HrKcV_~BrcAo7&`@%AXK~^0vYA`n|!#QQ2E#)kSh2vN?(I8d@ByX8Q5EK&{d%cgt|#8;;^U%b_k>; zx*kcLLZ}Lq5z5{Ufpoz^k$yU%dT2JGDrkp5mZ7U>iV4HF;^2+A4D78qUca4C1ysbt zPzCJ}$O-7;PbE|Vb1mN9ii55K))A_uE%7cC+98mk=!c?TOBlWt#~auokcPMrDxfK$ z3YbEu3fLi#mFOy9HKBT@3hshhXoo-=p=+5wflyO=3|Bx6v_l|0(REK;{HZ?#(unon z8(b~>;pnR2XhPS;gsOlY0%^$_FZ+B#4bfOaRlp8`Oh;FXXZkS4dhYVKx8k6y0sTHt zg$3+`Py#yy(w>D}23-hsr>!Im6R>zn9kLREYjq3rAs z$QpFzzcvL>3tO@(s{lI$GSurM2$gXcR$~`$Z^c2^H>Jsb<(^Nd0_+e-vFBg&VK%F- zi}!~>TB0l8RIUf}2~~g{0@>j8%|2|;(yA8QA&_zCS|(=`ssY(7n?8OBYHR?yzFUtb zR1fY=$Lmo`tOH9XJ(G%c0jMV92~|To1Tq(07vB?v?i%p%b_gV!HBuQL>DS8LgtD_k zAl+F5mA((5`0<3wzx=H@j_<%*aRADwC5xWM$_{}H@cKYP^~_wa+aZvW&Hk-8@EXf+ z2-T1>LKS3(K(bl%bTKy|RQ_EEUA(;&2VMDJLZ}Mou?VWdb_isg*C!GxeiNZfZ-+q2 zAat8mW=T^49SBtsI|MQaT@|>BP#G^IR0ehkWTofdBvkrlEPbkw9ReAIu5`l)mG3$q zmLCEsgHX+~SnFia-G_DvWH`DqyvFm(2vq?)1hNL*-3^2)sH{BRzZHiyPS;jFLdEwd zbm{F7$VdofFosYSm~963RvdJ-Y&oGD>QbMPy%h&t=~9G>Uqh$@>=4K%bTwcbAx{fj zUIgjiyl(#=26hPK2jaC2l9_=W0{Me@wJbB<4uKpXUPD~t`_Qh#uCPJrLm;*;k0hp` z@W&l&FxgT(1&Tu;s~x#}P1se1kQ@Tp;7D6@OCUJ}ve%Ke(C&id5Xh9Dl&7t()qhlv z6fATkOR9e>4lIX2RyuZ^Uq^dGatLIdBW>{<3CSUlU5>OBa|$GfK*s;9l5CkQgyay& zTt|-eOJfNnhd`D)QtM&(Rvg&sylp`&Z^hXg?%P-YDV!?ppg|k>W9durT0G$0L4yn6 z_2o>w`r3A=hr?^86R&6BcFtfNyry9B|2PCPl?ZvlijbpV2Mrd(%bgRi<YC~)BAO_||61}gA~feI zfeNzY10&$IEQxO>-X8+F3*PlLyeN!d(?a zv{V9nE6!)(6rzkc1Ty|N^>|P3hvY$W2xP7!&G+aH$sv&Cjx6&I$4E#Hfs{DXyo@P7 zhlfD6I@WxPLQFXXvd@v`Pn1A%2;{-vRs5|u2de8XNPhEL=t%P%s_%A(Kvp}_+=V4{{fN*oD&k}p;oph_uVEB_isQp!ydGY|C|=Ki?G_skubvT)i%>p_ z<`s$O5Xjatb)#IN@U1wo90J+rSaX1;KJC7Jiq82ZS*fY6^9cw;G^0OSIkQ@Tp7v|~Rj46;D0(o$siub++Z^eP-5J;h8&7&xR z)LU^Jna%1iH)0ngzj@u@NOK*k{~Ep($C1mtzmNy1x8m$qNw=~F%Sq@Bskh=d(maFI zNLalU$Fb%POo8MO$T~-w4^RlnA&^~;w1vL}l0zWl52z$t?RP9Cgh&&9m&Y0d4T^FHEQ{ z#u4HQGSE(<8C;p6*F}$JP!C;?;>^BThJEV{`{U6YN9=oK@jXJbvt_Ux1KsAPznGC= z3VO7KQ@wsGX&yq>A?x{Tu-6fuhv-?F{SDk6VD;8N|C%iQZ@(zMkVs8VvyTopdezwOuI1bGuIQTlOg|f4y<@t1Q@vxldb3eP{}=l48h_grdjYIj(-O?Yplw8-jovt- zUxl6*(Vs%s(@;zQAULY4tk#H?w*;Ju7Py5BqAk*Xr}S<^!ryG)91k-v{cqxqM{I{K z0d5u1bt);UKZ>gy)%Wo?;r~Hgh_fFxxSabos-KjR!C|;x(fGz0dV>u8t_*#09r`4i z!Nv@Oe)xOQ_}4S^`pnAF_)GW}64jS7K}YpJ=mkTW7U>a0=f_7o1KnX$j)?~)rsociHqucGW5Y2 z>E#zi?a$88PnS;lA|Z*TOSjjet)oWXzQsdJ2Iq| zLRW$5XQJmv^tQeuEdDj(>PF)K0;}E@ePTZ133Xnj)G7taXKuS_P5 zmORt70OUsW85#OCbX~14o)KJmW$SjSsg1K^ccBmTSG?){yl#K?UFUVwEvBE3?&{kl z{4??W(N(U+>)1;)eqcuYh%i3R-;qCRSnDIRy1TzR|9_SC9#B$LUDRlG(-R27kTVoH z!%Q~{5`><>%rJpr0%43ulq^9pd^RE|V!}izqM`zlGY%>sDk}1!qM~Br19L=7pkR1= zSD#aJsrUZ%-db;1m)d)ubMviRw?bFNH2PX3#pc!GWCFAI)c!6odsO)$-~kBguf452 zDBYnv*~h2F`ITM*e6EKXcJCM8qnWtLIgrT%vd7nxDM)#-k5^ajl^${;1sN9tT+3_+lM

    )W+XoJW29QsLX{#|Lds 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 G1OFG(4&j*q diff --git a/crates/brain-brew-cli/assets/workbench/index.html b/crates/brain-brew-cli/assets/workbench/index.html index 42a9e07..eac27bb 100644 --- a/crates/brain-brew-cli/assets/workbench/index.html +++ b/crates/brain-brew-cli/assets/workbench/index.html @@ -5,13 +5,16 @@ Brain Brew Deck Workbench - - + + +

    + Brain Brew Deck Workbench loading workspace metadata +

    n5}6E1V6x7l3ZH8`}tY6bmqbP@2!gIt)8QVAO$mmDm5{> zl}0+SA+fs~F}?C@MV(crsbQyXV^w$htKwI0`U?wI)t~;-rL1i^xmmPb3U3QpejLHU z$J5~un-LiBDeNwR~005T+3ElD3|NGtT&(Ay@<+!aZx zQVe1lLe!F4bZ?Z0G_jh`N48`)z{L#ZbcWPYFR`5)WdyqpKtZD?GWE(<-Q=oceX3Sr zdg6g%(`_daM@`3r)5s=S-DhGv&bqUg8i+%CJXL~Fc2;(cGZk>v z1%5xpP@$4%Ce*z_p~L+-_4Ctz{6KUKOt*)?LRq@tLK_k?i2cYh$OCpMF#>zGM zMr)Dxa?+&?Rl9>EPeLEySr-Af-H?l{7YYefnAxa1)6K$}iQN5k0DBjW0BxWHV-}!z zbFtEINlegPiH=N?RJvnSx*0Of$$?%!LuUs0h-w*L zqp+V*(5K3%tNmu`W(YvsS1@|t zNS!w6Paksdjx-4S=VY#ZsG{WnNv$NXh~N64$MVn*A(qQ_0>0xBgASi%Y=8S1pV4u6 z9q}kXvv_rT^Q!jdGOA*4z^SP|3kY-(oVJ)AyuW#nztZL;qf@{mGj(5Ix!rBy$Owho{HEeigVCJakhyk;MXN%WQQW0 zRCl1R_DStBY6IcN+{{r*;NF!oe0CwkiFpr%&a{2UVB z=g-Nn*^|!q;8j5kRf^r}yvcKq@Ug49GqHh|IG8d{8`)Wfa2V#q)}4lxTI8>hCq%CKyId`=w%yAMu3 zjD1^HK}m&7WQ!6s;fmF5lrNxeqp7RfWMNE1+K!aA(Vd)?ZEWFhTWJcu+)lxFUtP!? z%r_$B-+%ltRxWQ8m+BuJH~@9P065g@LSTaFx`($o5+#2cqqX)EegPgWC}GKrTH-Vu zk!*8qUTfZPaN$O;0a0iwYQql#&9LK2b{D|)I9#d*L&8vh{-z4@+Zt}O#3eve*1M&O z7T`SbUTP5OEEQt<{PS>*hAEKoh+1>7UWDhzmbQsK{yM$=MH|opDY|qg_WgczYxq}r zQSQrihns)RJ!tB(9mQxL)Ty)LG$%qb9(p(Tijrje@qA0gqqJjW zqsMeDWRwexxwuy~QDV)9|9cMiV{-K;9PV3-q}GIh=83d`^+54KJVRO2q7=U#0^`DN zt<%clo8SFA-~9DYeCb_Z%U&0RfAPSBw|?`6KUjLx{_9`2pF`OAXd6TU`T)I0hS$lI zD;}e`BY^cXZTvpSZ#HlIIb~0579`%5LGM9V+K$jxO6Xfi_yM_^>s*nkUdLGhwVU4R zD83fO-}#$D-^Wt0PLvSA0@29*;lnT>4&(k@4fq}J!N*e#czw_GjxVU}mT!Kk5w?>* z4d%_)HPP?@hg49~-9&q#vQu3ad|geFBUlDi{jyi4If|d72^?LD>1RAO@l`O*o+|H! zwX#Aj1&r9Gb{Xi2lqu1{dA5LXz?Yj8OaHA2tj=tdzk~<&H3$!IrS9+-29yM{2(F1Jt^1#|&G| z3gB#0CY>uB!OL{_O4#+NZU~EsrbLm>KrhW#Av4H2gLwUr z8P*Wh_={`uwena7Ey_?KMmdEZRy*z_4rFe2C3v(#gpDT|kCAjZuVl^+WRsKRCgv(G zTiGpRZ=I7zAc8xc$W`p(Ib1F4G@GnmR^Kb4jN@3YbQk+Ng(?PHmDoR`iSIjQ?iR5iwJ z8+#${R`CJYD^2X-#mlM<#DTb^nJXe4FSN~M4AW~B1bDV&faW-l7V^|&teYAeC?Af@SVnh#Pkb*B&@L`_1+ zS-FK-C0R9^-}x?pL6!i4(TCu%P-S}Y{bcx2)OJC0fkEi7^G1Z'}z`&6#fWdiv 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%0Dj41 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