Your career: spoken languages chosen by code, with every language's name built in for every document language #689

Closed
opened 2026-10-03 01:59:02 +00:00 by tiagoagueda · 1 comment
Owner

A spoken language on a CV is free text the person types, and a different text for every language they want the CV in. The maintainer's list asks for the names to be built in: "English is displayed as “Inglês” in Portuguese, “Anglais” in french, etc. French is “Français” in french, “Francês” in Portuguese, etc. So a matrix should be built." That matrix exists as data (CLDR); what Postulo lacks is a language code on the entry to look a name up by.

What exists today

  • LanguageSkill.name is free text (CharField(100), src/postulo/resume/models.py:250-270) with a CEFR proficiency. The only way to print “Inglês” on a Portuguese CV is the by-hand translation of name (languageskill is in TRANSLATABLE, resume/translatable.py:33), typed once per document language.
  • There is no code on the entry. The form takes name, proficiency, order (resume/forms.py:204-216); the archive writes ("id", "name", "proficiency", "order") (core/export.py:391); the candidate file reads fields=("name", "proficiency"), identified by names=("name",) (resume/candidate.py:269-275); a plugin importer's Record["languages"] has name and proficiency (Writing-a-plugin.md:1333).
  • The matrix is half built twice. NATIVE_NAMES has 69 tags, each in its own language (core/languages.py:139-212, native_name :420; #64, #119, #337). The Europass reader names a language from its code in the CV's language with Django's get_language_info(code)["name_translated"], thirteen autonyms for languages Django lacks, and the code kept for a sign language (plugins/europass/candidate.py:100-115, :310-336; tests/test_europass_candidate.py:452-483), which is why the wiki says a Portuguese CV gets Inglês (Your-career-record.md:441-444). The older reader keeps whatever name the file wrote (plugins/europass/reader.py:445-451, :719-730); #641 (skill headings in the interface language, beside a language named in the file's) is about the headings; the “Inglês” in its example is this naming working as designed.
  • A precedent for this shape exists for skills. A skill's name on a CV is, in order, the person's own translation, the name the ESCO classification gives it in the CV's language, the name as written (resume/translating.py:188-205, #266), through a wrapper so that a theme, a plugin's too, reads entry.item.name unaware (Translated, :143-170).
  • Where a name is printed: both base themes (base_cv.html:98-106, base_portfolio.html:112-120), the plain text and Word file (documents/rendering.py:348-357), the preview, the overview row, the candidate review. Not searched; no API career endpoint.
  • Dependencies. uv.lock has 89 packages, none of Babel, pycountry, langcodes, language-data or marisa-trie.
  • Measured on 2026-10-03 (Django 6.1.1 as locked; Babel 2.18.0, CLDR 47; the 69 tags of NATIVE_NAMES as both the language named and the language it is named in):
    • Django's name_translated knows 101 names and has a catalogue for 44 of the 69 tags. It already gives the two examples (pt-PT: Inglês, Francês, Português; fr-FR: Anglais, Français, Portugais) and leaves from 2 (Polish, Latvian, Slovak, Estonian) to 54 (Igbo) of its 101 names equal to the English, some rightly.
    • CLDR names 4,090 of the 4,761 cells (85.9%). 51 of the 69 display languages are complete; the 671 gaps are all in 18 African display languages (Southern Ndebele, Nyanja, Swati and Venda name only themselves, Tsonga 21 of 69).
    • CLDR has 636 primary language names in English, 631 in French, 553 in European Portuguese, 421 in Yoruba, 25 in Tsonga, and names for languages Postulo has no catalogue for (Japanese, Hindi, Mirandese, Cantonese). It has none for British Sign Language (bfi).
  • CLDR writes a name as it falls in a sentence: inglês, anglais, francês. The capital is a separate rule that CLDR carries per locale as contextTransforms for languages (CLDR 47, common/main). Of the 69, Brazilian Portuguese, Spanish, Italian, Czech, Slovak, Croatian, Swedish and Ukrainian ask for titlecase-firstword standing alone, and those plus French, Danish and Finnish in a menu; European Portuguese sets the menu rule to no-change and says nothing of standing alone (it inherits pt's titlecase-firstword there, so the generator must resolve parent locales before it compares). Babel's data does not include these rules. A plain capitalize() is wrong in ways found in the data: it lowercases the rest (Dutch “IJslands” becomes “Ijslands”; Turkish “İngiliz İngilizcesi” becomes “İngiliz i̇ngilizcesi”, with a combining dot), and raising only the first letter with upper() turns Georgian's first letter into Mtavruli, the all-capitals form. CLDR's own Turkish names are already capitalised, so Turkish needs no rule.

Checked against main at f09b6010b.

Proposal

  1. One module for names, core/language_names.py (the only place a name is made, as core/languages.py is for codes): name(code, in_language), listing(in_language), match(text) (a typed name to a code). A gap falls back, never blank: CLDR's name in the document's language (through its own locale parents, pt-AO to pt-PT); the language's own name; CLDR's English; the code. Capitalisation follows CLDR's rule, in a small table of the locales that ask for it, with a test that fails when it disagrees with the tag list.
  2. LanguageSkill.code, a LanguageField (BCP 47 through postulo.core.languages; blank for a language not on the list), beside name. A coded entry's name is filled by the form from the table in the person's record language, so anything that wants a plain name (an older reader, a plugin theme) has one; an uncoded entry is as today.
  3. The form: Basecoat's select with a flag where there is one, named in the interface language, searchable, with “Other…” revealing the free name; the native select without scripts, as #301 decided. #690 words the section.
  4. A CV prints the built-in name in the document's language for a coded entry: the person's own translation if there is one (as for a skill), else the table, else the stored name; through Translated, so both base themes, the plain text, the Word file and the preview follow, and a plugin theme reads entry.item.name as before. The career page shows the name in the interface language.
  5. Existing rows. A migration sets code where a typed name matches exactly one language (casefolded, accent-insensitive, in any display language, in English, as an autonym or as an ISO 639 code); ambiguous or unknown names stay text. It never rewrites name; a management command reports what matched.
  6. Everything that carries a language entry gains code: archive (format one more than whatever it is when the commit lands; an older archive restores with names matched), candidate file (identity compares codes where both entries have one, so “English” and “Inglês” are one entry; candidate format one more), seed data.
  7. Importers fill the code. The Europass candidate reader keeps its _language_code; the older reader runs match; Record["languages"] gains an optional code (a surface change: Writing-a-plugin.md:1333, tests/test_wiki_surface.py, tests/test_plugin_surface.py).
  8. Whichever source is chosen below, #698, #679 and #680 can reuse the module.

Decisions

  • Where the matrix comes from. (a) Babel at run time (CLDR; also territory, script and “Language (Region)” names); (b) a table generated from CLDR and committed, as src/postulo/jobs/data/nace-2.1.json is, with its licence beside it; (c) Django's names only; (d) pycountry; (e) langcodes with language-data; (f) strings in Postulo's catalogues, translated in Weblate. Recommended: (a). It is the library-first answer; it covers any document language Postulo can be told about, not only the 69; and the same module answers #698's “Language (Region)” and #679/#680's country names in the document's language (Babel's French has 294 territory names; Portuguese gives “Alemanha”). Its cost is size and one more dependency. (b) is the fallback if the image's size decides: 558 KB of JSON for 69 display languages by 636 names (176 KB gzipped, my measure), limited to the 69, with a generator to keep. (c) misses 25 of the 69; (d) covers fewer cells and is LGPL; (e) answers an unknown name with English; (f) asks volunteers to redo what CLDR has reviewed, 636 names by 69 languages.
  • Capitals. (a) CLDR's contextTransforms; (b) capitalise wherever the script has capitals, with exceptions for Georgian (Mtavruli is not a sentence capital) and for the dotted and dotless i of Turkish and Azerbaijani, and for digraphs such as Dutch “IJ”; (c) leave CLDR's case: “inglês”. Recommended: (a), taking a CV line as a list item or a standing-alone label and capitalising where either context asks (giving “Inglês” and “Anglais”, and leaving Catalan and Dutch as CLDR writes them). That reading of “either” is mine; CLDR does not say which context a CV line is. NATIVE_NAMES writes pickers in sentence case (“català”), so a picker and a CV line differ on purpose.
  • Which languages are offered. (a) CLDR's primary languages (636) plus the variants it names (Brazilian and European Portuguese, Simplified and Traditional Chinese); (b) only the 69. Recommended: (a), with “Other…” for what CLDR lacks (British Sign Language, Europass's BFI); a well-formed unknown code is kept and shown as the code.
  • Gaps. 671 of 4,761 cells have no name. Recommended: the fallback above, a recorded list of the gaps in the tests, and no machine-made names.
  • The stored name of a coded entry. Recommended: keep it filled from the record language (a one-line save rule like Skill.match()); nothing that reads name breaks.
  • Existing rows. Recommended: match conservatively; an ambiguous name (“Norwegian” is no, nb, nn; “Chinese”; “Portuguese” beside pt-BR and pt-PT) stays text and is listed for the person. No migration changes what a printed CV says.
  • #641. It is about the skill headings, which this issue does not touch; the language name in its example (“Inglês” beside English headings) is what the Candidate reader does on purpose today, and with a code it stays a lookup in the CV's language. Recommended: nothing to change on #641 when this lands.

Prior art

  • Babel 2.18.0: BSD-3-Clause; released 2026-02-01 (after 2.17.0 on 2025-02-01 and 2.16.0 on 2024-08-08); pure Python, Python >= 3.8; CLDR 47, 1,082 locales; wheel 10,196,845 bytes, 30.9 MB unpacked (29.9 MB of it locale data, 1,082 locales) read per locale on demand; OSV lists one advisory, a 2021 directory traversal fixed in 2.9.1; not a dependency. CLDR data is under the Unicode License v3 (unicode.org/license.txt), permissive and notice-keeping; compatibility with AGPL-3.0 not checked against a legal opinion. THIRD-PARTY.md by its own rule does not list packages that "arrive through the package index", so (a) needs no row; (b) would, with the notice beside the file (#579 already calls the register incomplete). Adopt.
  • pycountry 26.2.16: LGPL-2.1-only; 2026-02-17; no advisory; 21 MB unpacked (an 8.0 MB wheel); ISO 639 names with iso-codes translations, already capitalised; no translation directory for 23 of the 69 tags and about 2,080 of 4,761 cells differing from English by my count. Reject: less coverage, copyleft.
  • langcodes 3.5.1 and language-data 1.4.0: MIT; 2025-12-02 and 2025-11-28; no advisory; language-data is 13 MB unpacked (a 5.6 MB wheel) and needs marisa-trie 1.4.1 (a compiled extension). It answers an unknown name with English (ts shown as “English”). Reject.
  • Standards: BCP 47 (RFC 5646) through postulo.core.languages; ISO 639 inside it; CLDR's LDML contextTransforms.
  • applications/ical.py and documents/docx.py are hand-written file formats; a 4,761-cell table is what the library-first rule exists to avoid typing.

Builds on

#64, #119 (each language named in itself), #118 and #269 (which languages), #131 (entries in other languages), #266 (the ESCO name for a skill, the same shape), #337 (BCP 47), #641 (the Europass import), #301 (Basecoat's select with a flag).

Needed by

#698 and #679/#680 can use the module; none waits for it. #698 leaves “Language (Region)” for a tag with no region to this module, if the maintainer wants every name to read that way; this issue does not add region names to the interface menus unless asked.

Done when

  • A new tests/test_language_names.py: the two examples (English is “Inglês” in pt-PT and “Anglais” in fr-FR; French is “Français” and “Francês”); a gap falls back in the stated order and is never blank; the capitalisation table agrees with CLDR for every tag in NATIVE_NAMES; Turkish, Dutch “IJslands” and Georgian are not mangled; pt-AO reads through pt-PT; a well-formed unknown code is kept; the list of gaps is the recorded one.
  • tests/test_career_languages.py and tests/test_documents.py: a coded entry prints in the document's language in both themes, the plain text and the Word file, a person's translation wins, an uncoded entry is unchanged; tests/test_europass_candidate.py (:452-483) still passes and keeps the code; tests/test_europass.py covers the older reader's matching.
  • tests/test_candidate_file.py holds the new fingerprint and reads an older file; tests/test_export.py carries code and restores an older archive with names matched; a migration test places French, Francês, français and leaves Norwegian and an unknown word alone. tests/test_wiki_surface.py and tests/test_plugin_surface.py carry the optional key; tests/security/test_candidate_file.py a malformed or over-long code.
  • pyproject.toml and uv.lock carry Babel, bounded to the version the CLDR rules were checked against (the capitalisation test fails when an upgrade changes the data beneath it).
  • New strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349); Your career record and Writing a plugin updated in the same commit; CHANGELOG ends (#N).

From the list of improvements of 2026-10-02.

A spoken language on a CV is free text the person types, and a different text for every language they want the CV in. The maintainer's list asks for the names to be built in: "English is displayed as “Inglês” in Portuguese, “Anglais” in french, etc. French is “Français” in french, “Francês” in Portuguese, etc. So a matrix should be built." That matrix exists as data (CLDR); what Postulo lacks is a language *code* on the entry to look a name up by. ## What exists today - **`LanguageSkill.name` is free text** (`CharField(100)`, `src/postulo/resume/models.py:250-270`) with a CEFR `proficiency`. The only way to print “Inglês” on a Portuguese CV is the by-hand translation of `name` (`languageskill` is in `TRANSLATABLE`, `resume/translatable.py:33`), typed once per document language. - **There is no code on the entry.** The form takes `name`, `proficiency`, `order` (`resume/forms.py:204-216`); the archive writes `("id", "name", "proficiency", "order")` (`core/export.py:391`); the candidate file reads `fields=("name", "proficiency")`, identified by `names=("name",)` (`resume/candidate.py:269-275`); a plugin importer's `Record["languages"]` has `name` and `proficiency` (`Writing-a-plugin.md:1333`). - **The matrix is half built twice.** `NATIVE_NAMES` has 69 tags, each in its own language (`core/languages.py:139-212`, `native_name` `:420`; #64, #119, #337). The Europass reader names a language from its code in the CV's language with Django's `get_language_info(code)["name_translated"]`, thirteen autonyms for languages Django lacks, and the code kept for a sign language (`plugins/europass/candidate.py:100-115`, `:310-336`; `tests/test_europass_candidate.py:452-483`), which is why the wiki says a Portuguese CV gets *Inglês* (`Your-career-record.md:441-444`). The older reader keeps whatever name the file wrote (`plugins/europass/reader.py:445-451`, `:719-730`); #641 (skill headings in the interface language, beside a language named in the file's) is about the headings; the “Inglês” in its example is this naming working as designed. - **A precedent for this shape exists for skills.** A skill's name on a CV is, in order, the person's own translation, the name the ESCO classification gives it in the CV's language, the name as written (`resume/translating.py:188-205`, #266), through a wrapper so that a theme, a plugin's too, reads `entry.item.name` unaware (`Translated`, `:143-170`). - **Where a name is printed**: both base themes (`base_cv.html:98-106`, `base_portfolio.html:112-120`), the plain text and Word file (`documents/rendering.py:348-357`), the preview, the overview row, the candidate review. Not searched; no API career endpoint. - **Dependencies.** `uv.lock` has 89 packages, none of Babel, pycountry, langcodes, language-data or marisa-trie. - **Measured on 2026-10-03** (Django 6.1.1 as locked; Babel 2.18.0, CLDR 47; the 69 tags of `NATIVE_NAMES` as both the language named and the language it is named in): - Django's `name_translated` knows 101 names and has a catalogue for 44 of the 69 tags. It already gives the two examples (pt-PT: Inglês, Francês, Português; fr-FR: Anglais, Français, Portugais) and leaves from 2 (Polish, Latvian, Slovak, Estonian) to 54 (Igbo) of its 101 names equal to the English, some rightly. - CLDR names **4,090 of the 4,761 cells** (85.9%). 51 of the 69 display languages are complete; the 671 gaps are all in 18 African display languages (Southern Ndebele, Nyanja, Swati and Venda name only themselves, Tsonga 21 of 69). - CLDR has 636 primary language names in English, 631 in French, 553 in European Portuguese, 421 in Yoruba, 25 in Tsonga, and names for languages Postulo has no catalogue for (Japanese, Hindi, Mirandese, Cantonese). It has none for British Sign Language (`bfi`). - **CLDR writes a name as it falls in a sentence**: `inglês`, `anglais`, `francês`. The capital is a separate rule that CLDR carries per locale as `contextTransforms` for languages (CLDR 47, `common/main`). Of the 69, Brazilian Portuguese, Spanish, Italian, Czech, Slovak, Croatian, Swedish and Ukrainian ask for `titlecase-firstword` standing alone, and those plus French, Danish and Finnish in a menu; **European Portuguese sets the menu rule to `no-change`** and says nothing of standing alone (it inherits `pt`'s `titlecase-firstword` there, so the generator must resolve parent locales before it compares). Babel's data does not include these rules. A plain `capitalize()` is wrong in ways found in the data: it lowercases the rest (Dutch “IJslands” becomes “Ijslands”; Turkish “İngiliz İngilizcesi” becomes “İngiliz i̇ngilizcesi”, with a combining dot), and raising only the first letter with `upper()` turns Georgian's first letter into Mtavruli, the all-capitals form. CLDR's own Turkish names are already capitalised, so Turkish needs no rule. Checked against `main` at `f09b6010b`. ## Proposal 1. **One module for names**, `core/language_names.py` (the only place a name is made, as `core/languages.py` is for codes): `name(code, in_language)`, `listing(in_language)`, `match(text)` (a typed name to a code). A gap falls back, never blank: CLDR's name in the document's language (through its own locale parents, `pt-AO` to `pt-PT`); the language's own name; CLDR's English; the code. Capitalisation follows CLDR's rule, in a small table of the locales that ask for it, with a test that fails when it disagrees with the tag list. 2. **`LanguageSkill.code`**, a `LanguageField` (BCP 47 through `postulo.core.languages`; blank for a language not on the list), beside `name`. A coded entry's `name` is filled by the form from the table in the person's record language, so anything that wants a plain name (an older reader, a plugin theme) has one; an uncoded entry is as today. 3. **The form**: Basecoat's select with a flag where there is one, named in the interface language, searchable, with “Other…” revealing the free name; the native select without scripts, as #301 decided. #690 words the section. 4. **A CV prints the built-in name in the document's language** for a coded entry: the person's own translation if there is one (as for a skill), else the table, else the stored name; through `Translated`, so both base themes, the plain text, the Word file and the preview follow, and a plugin theme reads `entry.item.name` as before. The career page shows the name in the interface language. 5. **Existing rows.** A migration sets `code` where a typed name matches exactly one language (casefolded, accent-insensitive, in any display language, in English, as an autonym or as an ISO 639 code); ambiguous or unknown names stay text. It never rewrites `name`; a management command reports what matched. 6. **Everything that carries a language entry** gains `code`: archive (format one more than whatever it is when the commit lands; an older archive restores with names matched), candidate file (identity compares codes where both entries have one, so “English” and “Inglês” are one entry; candidate format one more), seed data. 7. **Importers fill the code.** The Europass candidate reader keeps its `_language_code`; the older reader runs `match`; `Record["languages"]` gains an optional `code` (a surface change: `Writing-a-plugin.md:1333`, `tests/test_wiki_surface.py`, `tests/test_plugin_surface.py`). 8. Whichever source is chosen below, #698, #679 and #680 can reuse the module. ## Decisions - **Where the matrix comes from.** (a) Babel at run time (CLDR; also territory, script and “Language (Region)” names); (b) a table generated from CLDR and committed, as `src/postulo/jobs/data/nace-2.1.json` is, with its licence beside it; (c) Django's names only; (d) pycountry; (e) langcodes with language-data; (f) strings in Postulo's catalogues, translated in Weblate. **Recommended: (a).** It is the library-first answer; it covers any document language Postulo can be told about, not only the 69; and the same module answers #698's “Language (Region)” and #679/#680's country names in the document's language (Babel's French has 294 territory names; Portuguese gives “Alemanha”). Its cost is size and one more dependency. (b) is the fallback if the image's size decides: 558 KB of JSON for 69 display languages by 636 names (176 KB gzipped, my measure), limited to the 69, with a generator to keep. (c) misses 25 of the 69; (d) covers fewer cells and is LGPL; (e) answers an unknown name with English; (f) asks volunteers to redo what CLDR has reviewed, 636 names by 69 languages. - **Capitals.** (a) CLDR's `contextTransforms`; (b) capitalise wherever the script has capitals, with exceptions for Georgian (Mtavruli is not a sentence capital) and for the dotted and dotless i of Turkish and Azerbaijani, and for digraphs such as Dutch “IJ”; (c) leave CLDR's case: “inglês”. **Recommended:** (a), taking a CV line as a list item or a standing-alone label and capitalising where **either** context asks (giving “Inglês” and “Anglais”, and leaving Catalan and Dutch as CLDR writes them). That reading of “either” is mine; CLDR does not say which context a CV line is. `NATIVE_NAMES` writes pickers in sentence case (“català”), so a picker and a CV line differ on purpose. - **Which languages are offered.** (a) CLDR's primary languages (636) plus the variants it names (Brazilian and European Portuguese, Simplified and Traditional Chinese); (b) only the 69. **Recommended:** (a), with “Other…” for what CLDR lacks (British Sign Language, Europass's `BFI`); a well-formed unknown code is kept and shown as the code. - **Gaps.** 671 of 4,761 cells have no name. **Recommended:** the fallback above, a recorded list of the gaps in the tests, and no machine-made names. - **The stored `name` of a coded entry.** **Recommended:** keep it filled from the record language (a one-line `save` rule like `Skill.match()`); nothing that reads `name` breaks. - **Existing rows.** **Recommended:** match conservatively; an ambiguous name (“Norwegian” is `no`, `nb`, `nn`; “Chinese”; “Portuguese” beside `pt-BR` and `pt-PT`) stays text and is listed for the person. No migration changes what a printed CV says. - **#641.** It is about the skill headings, which this issue does not touch; the language name in its example (“Inglês” beside English headings) is what the Candidate reader does on purpose today, and with a code it stays a lookup in the CV's language. **Recommended:** nothing to change on #641 when this lands. ## Prior art - **Babel 2.18.0**: BSD-3-Clause; released 2026-02-01 (after 2.17.0 on 2025-02-01 and 2.16.0 on 2024-08-08); pure Python, Python >= 3.8; CLDR 47, 1,082 locales; wheel 10,196,845 bytes, 30.9 MB unpacked (29.9 MB of it locale data, 1,082 locales) read per locale on demand; OSV lists one advisory, a 2021 directory traversal fixed in 2.9.1; not a dependency. CLDR data is under the Unicode License v3 (unicode.org/license.txt), permissive and notice-keeping; compatibility with AGPL-3.0 not checked against a legal opinion. `THIRD-PARTY.md` by its own rule does not list packages that "arrive through the package index", so (a) needs no row; (b) would, with the notice beside the file (#579 already calls the register incomplete). **Adopt.** - **pycountry 26.2.16**: LGPL-2.1-only; 2026-02-17; no advisory; 21 MB unpacked (an 8.0 MB wheel); ISO 639 names with iso-codes translations, already capitalised; no translation directory for 23 of the 69 tags and about 2,080 of 4,761 cells differing from English by my count. **Reject**: less coverage, copyleft. - **langcodes 3.5.1 and language-data 1.4.0**: MIT; 2025-12-02 and 2025-11-28; no advisory; language-data is 13 MB unpacked (a 5.6 MB wheel) and needs `marisa-trie` 1.4.1 (a compiled extension). It answers an unknown name with English (`ts` shown as “English”). **Reject.** - **Standards**: BCP 47 (RFC 5646) through `postulo.core.languages`; ISO 639 inside it; CLDR's LDML `contextTransforms`. - `applications/ical.py` and `documents/docx.py` are hand-written file formats; a 4,761-cell table is what the library-first rule exists to avoid typing. ## Builds on #64, #119 (each language named in itself), #118 and #269 (which languages), #131 (entries in other languages), #266 (the ESCO name for a skill, the same shape), #337 (BCP 47), #641 (the Europass import), #301 (Basecoat's select with a flag). ## Needed by #698 and #679/#680 can use the module; none waits for it. #698 leaves “Language (Region)” for a tag with no region to this module, if the maintainer wants every name to read that way; this issue does not add region names to the interface menus unless asked. ## Done when - A new `tests/test_language_names.py`: the two examples (English is “Inglês” in pt-PT and “Anglais” in fr-FR; French is “Français” and “Francês”); a gap falls back in the stated order and is never blank; the capitalisation table agrees with CLDR for every tag in `NATIVE_NAMES`; Turkish, Dutch “IJslands” and Georgian are not mangled; `pt-AO` reads through `pt-PT`; a well-formed unknown code is kept; the list of gaps is the recorded one. - `tests/test_career_languages.py` and `tests/test_documents.py`: a coded entry prints in the document's language in both themes, the plain text and the Word file, a person's translation wins, an uncoded entry is unchanged; `tests/test_europass_candidate.py` (`:452-483`) still passes and keeps the code; `tests/test_europass.py` covers the older reader's matching. - `tests/test_candidate_file.py` holds the new fingerprint and reads an older file; `tests/test_export.py` carries `code` and restores an older archive with names matched; a migration test places `French`, `Francês`, `français` and leaves `Norwegian` and an unknown word alone. `tests/test_wiki_surface.py` and `tests/test_plugin_surface.py` carry the optional key; `tests/security/test_candidate_file.py` a malformed or over-long code. - `pyproject.toml` and `uv.lock` carry Babel, bounded to the version the CLDR rules were checked against (the capitalisation test fails when an upgrade changes the data beneath it). - New strings in the working catalogues (en-GB, fr-FR, pt-PT as drafts) and Weblate (#349); *Your career record* and *Writing a plugin* updated in the same commit; CHANGELOG ends `(#N)`. From the list of improvements of 2026-10-02.
tiagoagueda added this to the 0.6.0 milestone 2026-10-03 01:59:02 +00:00
Author
Owner

Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#689).

Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#689).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#689
No description provided.