Spreadsheet import: status keys in the preview, English skip reasons, counts without plural forms, a tab title fixed at first load #395

Closed
opened 2026-10-01 10:46:23 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the spreadsheet import view, its templates and csv_import.perform, and checking the templates for counts in sentences.

Checked again on 2026-10-01 against main at 0a5cad587: still present. core/csv_import.py shifted by a few lines when the currency list moved to core/currencies.py (#248). The code is now at core/csv_import.py:760 (map_status) and :789 (ParsedRow) for section 1; :944, :948 and :964-967 (the skip reasons in perform) and :889-891 (the translated problems) for section 2; and :441 (Sheet.row_count) for section 3. The templates, core/views_import.py and tests/test_csv_import.py:231 have not moved.

The spreadsheet import's mapping and result pages carry four kinds of text that do not follow the reader's language. The preview's Status column prints Postulo's internal key rather than the status's name; the "Why rows were skipped" list is built from English f-strings; two sentences put numbers before fixed plural nouns ("1 rows"); and the section title in the browser tab is translated once, when the module is first imported. Every user-facing string is meant to be translatable, and the preview is the step where a person checks that their sheet was read correctly.

1. The preview's Status column shows the internal key

  • Where: templates/core/import_csv_map.html:92 prints {{ row.status }}, the value map_status (core/csv_import.py:765) returns: "interviewing", "ghosted", "draft", not Status(row.status).label.
  • What happens: a person reading Postulo in French types "Entretien" in their sheet, and the preview's Status column answers "interviewing". The same key is shown to an English reader, in lower case and unlike the status's name everywhere else.
  • Fix: give ParsedRow (core/csv_import.py:794) a status_label property returning Status(self.status).label, or pass the labels to the template, and print that at import_csv_map.html:92.

2. The skip reasons are English

  • Where: core/csv_import.py:949, :953 and :969-972 in perform append f"row {row.number}: ...", "already recorded (same address)" and "already recorded ({row.role} at {company.name}, {row.applied_at})" to report.skipped, which templates/core/import_csv_done.html:25 lists as they are, under the translated heading "Why rows were skipped". The problems joined into the first one (no company, no role, core/csv_import.py:894-896) are translated; the words around them are not.
  • What happens: after an import in French, the list reads "row 7: already recorded (Dev at Acme, 2026-09-01)", with the date in ISO form rather than the locale's format, and "row 3: pas d'entreprise" with half of the line in each language.
  • Fix: build the reasons with gettext and named placeholders, for example _("Row %(number)s: already recorded (same address)"), _("Row %(number)s: already recorded (%(role)s at %(company)s, %(date)s)") and _("Row %(number)s: %(problems)s"), formatting the date with date_format(row.applied_at, "DATE_FORMAT").

3. Row counts are stated without plural forms

  • Where: templates/core/import_csv_map.html:10-12 (<strong>{{ name }}</strong>: {{ rows }} rows. The guesses below …) and templates/core/import_csv_map.html:109-111 (Across the whole file: {{ applications }} applications, {{ listings }} listings, {{ skipped }} rows skipped for want of a company or a role. …). Both are blocktranslate blocks with no count and no {% plural %} branch. The numbers come from Sheet.row_count (core/csv_import.py:437) and core/views_import.py:80-82.
  • What happens: a file with one data row is introduced as "data.csv: 1 rows.", and a small file's summary reads "1 applications, 1 listings, 1 rows skipped". A Polish or Ukrainian translator has one noun form to give for 1, 2, 5 and 22 alike, and the summary holds three independent counts in one sentence, which no single plural selector can serve. tests/test_csv_import.py:231 pins the sentence as it is today ("3 applications, 1 listings, 1 rows skipped").
  • Fix: for the first, {% blocktranslate trimmed count rows=sheet.row_count with name=sheet.filename %}…{{ rows }} row.…{% plural %}…{{ rows }} rows.…{% endblocktranslate %}. For the summary, show the three counts as label and number pairs, as templates/core/import_csv_done.html:14-20 already does ("Applications", "Listings", "Rows skipped"), and keep the explanatory sentence separate and free of counts. Update the assertion at tests/test_csv_import.py:231 with it.

4. The tab title is translated when the module is imported

  • Where: core/views_import.py:9 imports the eager gettext as _, and core/views_import.py:14 evaluates SECTION = {"section_title": _("Your data")} at module level. templates/settings/base.html:4 puts it in the page <title> ("Settings > Your data").
  • What happens: the three import pages show "Your data", or whichever language was active when views_import was first loaded, in the tab title for everyone served by that process, although locale/fr_FR/LC_MESSAGES/django.po translates it as "Vos données".
  • Fix: use gettext_lazy for SECTION, as accounts/settings_views.py:15 and core/server_views.py:28 do for section_title, keeping the eager gettext for the messages inside the view; or build the dictionary inside the view.

Done when

  • A test in tests/test_csv_import.py renders the mapping page under translation.override("fr-fr") (or with a stubbed catalogue) and finds the translated status label, not the key, in the preview row.
  • A test in tests/test_csv_import.py checks that a duplicate row's skip reason passes through the catalogue: perform under an overridden language does not return the English "already recorded", and the result page under fr-fr shows no English skip reason.
  • A test in tests/test_csv_import.py uploads a file with one data row and finds "1 row." (not "1 rows") on the mapping page, and finds the summary's three numbers each beside its own label.
  • A test in tests/test_csv_import.py checks that views_import.SECTION["section_title"] is a lazy translation (a Promise), not a str.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the spreadsheet import view, its templates and `csv_import.perform`, and checking the templates for counts in sentences. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. `core/csv_import.py` shifted by a few lines when the currency list moved to `core/currencies.py` (#248). The code is now at `core/csv_import.py:760` (`map_status`) and `:789` (`ParsedRow`) for section 1; `:944`, `:948` and `:964-967` (the skip reasons in `perform`) and `:889-891` (the translated problems) for section 2; and `:441` (`Sheet.row_count`) for section 3. The templates, `core/views_import.py` and `tests/test_csv_import.py:231` have not moved. The spreadsheet import's mapping and result pages carry four kinds of text that do not follow the reader's language. The preview's Status column prints Postulo's internal key rather than the status's name; the "Why rows were skipped" list is built from English f-strings; two sentences put numbers before fixed plural nouns ("1 rows"); and the section title in the browser tab is translated once, when the module is first imported. Every user-facing string is meant to be translatable, and the preview is the step where a person checks that their sheet was read correctly. ## 1. The preview's Status column shows the internal key - **Where:** `templates/core/import_csv_map.html:92` prints `{{ row.status }}`, the value `map_status` (`core/csv_import.py:765`) returns: `"interviewing"`, `"ghosted"`, `"draft"`, not `Status(row.status).label`. - **What happens:** a person reading Postulo in French types "Entretien" in their sheet, and the preview's Status column answers "interviewing". The same key is shown to an English reader, in lower case and unlike the status's name everywhere else. - **Fix:** give `ParsedRow` (`core/csv_import.py:794`) a `status_label` property returning `Status(self.status).label`, or pass the labels to the template, and print that at `import_csv_map.html:92`. ## 2. The skip reasons are English - **Where:** `core/csv_import.py:949`, `:953` and `:969-972` in `perform` append `f"row {row.number}: ..."`, `"already recorded (same address)"` and `"already recorded ({row.role} at {company.name}, {row.applied_at})"` to `report.skipped`, which `templates/core/import_csv_done.html:25` lists as they are, under the translated heading "Why rows were skipped". The problems joined into the first one (`no company`, `no role`, `core/csv_import.py:894-896`) are translated; the words around them are not. - **What happens:** after an import in French, the list reads "row 7: already recorded (Dev at Acme, 2026-09-01)", with the date in ISO form rather than the locale's format, and "row 3: pas d'entreprise" with half of the line in each language. - **Fix:** build the reasons with `gettext` and named placeholders, for example `_("Row %(number)s: already recorded (same address)")`, `_("Row %(number)s: already recorded (%(role)s at %(company)s, %(date)s)")` and `_("Row %(number)s: %(problems)s")`, formatting the date with `date_format(row.applied_at, "DATE_FORMAT")`. ## 3. Row counts are stated without plural forms - **Where:** `templates/core/import_csv_map.html:10-12` (`<strong>{{ name }}</strong>: {{ rows }} rows. The guesses below …`) and `templates/core/import_csv_map.html:109-111` (`Across the whole file: {{ applications }} applications, {{ listings }} listings, {{ skipped }} rows skipped for want of a company or a role. …`). Both are `blocktranslate` blocks with no `count` and no `{% plural %}` branch. The numbers come from `Sheet.row_count` (`core/csv_import.py:437`) and `core/views_import.py:80-82`. - **What happens:** a file with one data row is introduced as "data.csv: 1 rows.", and a small file's summary reads "1 applications, 1 listings, 1 rows skipped". A Polish or Ukrainian translator has one noun form to give for 1, 2, 5 and 22 alike, and the summary holds three independent counts in one sentence, which no single plural selector can serve. `tests/test_csv_import.py:231` pins the sentence as it is today (`"3 applications, 1 listings, 1 rows skipped"`). - **Fix:** for the first, `{% blocktranslate trimmed count rows=sheet.row_count with name=sheet.filename %}…{{ rows }} row.…{% plural %}…{{ rows }} rows.…{% endblocktranslate %}`. For the summary, show the three counts as label and number pairs, as `templates/core/import_csv_done.html:14-20` already does ("Applications", "Listings", "Rows skipped"), and keep the explanatory sentence separate and free of counts. Update the assertion at `tests/test_csv_import.py:231` with it. ## 4. The tab title is translated when the module is imported - **Where:** `core/views_import.py:9` imports the eager `gettext as _`, and `core/views_import.py:14` evaluates `SECTION = {"section_title": _("Your data")}` at module level. `templates/settings/base.html:4` puts it in the page `<title>` ("Settings > Your data"). - **What happens:** the three import pages show "Your data", or whichever language was active when `views_import` was first loaded, in the tab title for everyone served by that process, although `locale/fr_FR/LC_MESSAGES/django.po` translates it as "Vos données". - **Fix:** use `gettext_lazy` for `SECTION`, as `accounts/settings_views.py:15` and `core/server_views.py:28` do for `section_title`, keeping the eager `gettext` for the messages inside the view; or build the dictionary inside the view. ## Done when - A test in `tests/test_csv_import.py` renders the mapping page under `translation.override("fr-fr")` (or with a stubbed catalogue) and finds the translated status label, not the key, in the preview row. - A test in `tests/test_csv_import.py` checks that a duplicate row's skip reason passes through the catalogue: `perform` under an overridden language does not return the English "already recorded", and the result page under `fr-fr` shows no English skip reason. - A test in `tests/test_csv_import.py` uploads a file with one data row and finds "1 row." (not "1 rows") on the mapping page, and finds the summary's three numbers each beside its own label. - A test in `tests/test_csv_import.py` checks that `views_import.SECTION["section_title"]` is a lazy translation (a `Promise`), not a `str`.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:46:23 +00:00
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#395
No description provided.