Your career: spoken languages chosen by code, with every language's name built in for every document language #689
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#689
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.nameis free text (CharField(100),src/postulo/resume/models.py:250-270) with a CEFRproficiency. The only way to print “Inglês” on a Portuguese CV is the by-hand translation ofname(languageskillis inTRANSLATABLE,resume/translatable.py:33), typed once per document language.name,proficiency,order(resume/forms.py:204-216); the archive writes("id", "name", "proficiency", "order")(core/export.py:391); the candidate file readsfields=("name", "proficiency"), identified bynames=("name",)(resume/candidate.py:269-275); a plugin importer'sRecord["languages"]hasnameandproficiency(Writing-a-plugin.md:1333).NATIVE_NAMEShas 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'sget_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.resume/translating.py:188-205, #266), through a wrapper so that a theme, a plugin's too, readsentry.item.nameunaware (Translated,:143-170).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.uv.lockhas 89 packages, none of Babel, pycountry, langcodes, language-data or marisa-trie.NATIVE_NAMESas both the language named and the language it is named in):name_translatedknows 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.bfi).inglês,anglais,francês. The capital is a separate rule that CLDR carries per locale ascontextTransformsfor languages (CLDR 47,common/main). Of the 69, Brazilian Portuguese, Spanish, Italian, Czech, Slovak, Croatian, Swedish and Ukrainian ask fortitlecase-firstwordstanding alone, and those plus French, Danish and Finnish in a menu; European Portuguese sets the menu rule tono-changeand says nothing of standing alone (it inheritspt'stitlecase-firstwordthere, so the generator must resolve parent locales before it compares). Babel's data does not include these rules. A plaincapitalize()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 withupper()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
mainatf09b6010b.Proposal
core/language_names.py(the only place a name is made, ascore/languages.pyis 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-AOtopt-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.LanguageSkill.code, aLanguageField(BCP 47 throughpostulo.core.languages; blank for a language not on the list), besidename. A coded entry'snameis 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.Translated, so both base themes, the plain text, the Word file and the preview follow, and a plugin theme readsentry.item.nameas before. The career page shows the name in the interface language.codewhere 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 rewritesname; a management command reports what matched.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._language_code; the older reader runsmatch;Record["languages"]gains an optionalcode(a surface change:Writing-a-plugin.md:1333,tests/test_wiki_surface.py,tests/test_plugin_surface.py).Decisions
src/postulo/jobs/data/nace-2.1.jsonis, 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.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_NAMESwrites pickers in sentence case (“català”), so a picker and a CV line differ on purpose.BFI); a well-formed unknown code is kept and shown as the code.nameof a coded entry. Recommended: keep it filled from the record language (a one-linesaverule likeSkill.match()); nothing that readsnamebreaks.no,nb,nn; “Chinese”; “Portuguese” besidept-BRandpt-PT) stays text and is listed for the person. No migration changes what a printed CV says.Prior art
THIRD-PARTY.mdby 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.marisa-trie1.4.1 (a compiled extension). It answers an unknown name with English (tsshown as “English”). Reject.postulo.core.languages; ISO 639 inside it; CLDR's LDMLcontextTransforms.applications/ical.pyanddocuments/docx.pyare 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
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 inNATIVE_NAMES; Turkish, Dutch “IJslands” and Georgian are not mangled;pt-AOreads throughpt-PT; a well-formed unknown code is kept; the list of gaps is the recorded one.tests/test_career_languages.pyandtests/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.pycovers the older reader's matching.tests/test_candidate_file.pyholds the new fingerprint and reads an older file;tests/test_export.pycarriescodeand restores an older archive with names matched; a migration test placesFrench,Francês,françaisand leavesNorwegianand an unknown word alone.tests/test_wiki_surface.pyandtests/test_plugin_surface.pycarry the optional key;tests/security/test_candidate_file.pya malformed or over-long code.pyproject.tomlanduv.lockcarry Babel, bounded to the version the CLDR rules were checked against (the capitalisation test fails when an upgrade changes the data beneath it).(#N).From the list of improvements of 2026-10-02.
Landed on main with a green Forgejo CI run. Its entry in the CHANGELOG's Unreleased section ends in (#689).