feat(onboarding): Transactions card suggests Add as keyword, Budget card names leaves and the Annual column

Both Transactions sentences name the context menu's "Add as keyword" and
carry the advice on the word and the boxes (D18); both Budget sentences say
only last-level rows take an amount and what the Annual column does. The
labels go through StepCardText.labels, quoted in the locale files (D11).
The keyword dialog is unchanged; a test pins that it marks the transactions
as categorized by hand, so the step still checks off through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-10-13 21:12:25 -04:00
parent e177a7561f
commit 63d8ba7589
8 changed files with 138 additions and 34 deletions

View file

@ -34,7 +34,7 @@ Les six étapes ci-dessous sont celles du parcours de démarrage. La carte d'acc
1. Le bandeau de démarrage s'affiche en haut de chaque page tant que le parcours n'est pas masqué. Il indique votre progression, par exemple « Démarrage 3/6 », et l'étape suivante, avec un lien vers la page où la faire. Il n'apparaît pas sur le tableau de bord d'un profil sans transaction, dont la carte d'accueil contient déjà la liste 1. Le bandeau de démarrage s'affiche en haut de chaque page tant que le parcours n'est pas masqué. Il indique votre progression, par exemple « Démarrage 3/6 », et l'étape suivante, avec un lien vers la page où la faire. Il n'apparaît pas sur le tableau de bord d'un profil sans transaction, dont la carte d'accueil contient déjà la liste
2. « Voir la liste » déplie les six étapes sous le bandeau et « Fermer la liste » les replie ; la liste se replie aussi quand vous changez de page. Chaque étape à faire mène à la page où la faire 2. « Voir la liste » déplie les six étapes sous le bandeau et « Fermer la liste » les replie ; la liste se replie aussi quand vous changez de page. Chaque étape à faire mène à la page où la faire
3. Sur les pages Import, Transactions et Budget, quand l'étape suivante s'y fait, une carte de démarrage remplace le bandeau. Elle indique « Étape n sur N », dit quoi faire et entoure d'un contour le contrôle à utiliser. Le contour pulse trois fois, et pas du tout si votre système demande de réduire les animations. Si vous arrivez par un lien « Étape suivante », le focus du clavier se place sur le titre de la carte 3. Sur les pages Import, Transactions et Budget, quand l'étape suivante s'y fait, une carte de démarrage remplace le bandeau. Elle indique « Étape n sur N », dit quoi faire et entoure d'un contour le contrôle à utiliser. Le contour pulse trois fois, et pas du tout si votre système demande de réduire les animations. Si vous arrivez par un lien « Étape suivante », le focus du clavier se place sur le titre de la carte
4. La carte suit l'étape sans que vous changiez de page. Sur la page Import, elle accompagne l'assistant écran par écran. Le lien de l'étape « Classer une transaction à la main » ouvre la page Transactions sur les transactions non catégorisées, toutes dates confondues, ou sur toutes les transactions s'il n'en reste aucune à classer. Sur la page Budget, la carte désigne d'abord « Tout déplier », puis la cellule du mois courant de la première catégorie à remplir ; elle ne déplie jamais la grille à votre place 4. La carte suit l'étape sans que vous changiez de page. Sur la page Import, elle accompagne l'assistant écran par écran. Le lien de l'étape « Classer une transaction à la main » ouvre la page Transactions sur les transactions non catégorisées, toutes dates confondues, ou sur toutes les transactions s'il n'en reste aucune à classer. Sur la page Budget, la carte désigne d'abord « Tout déplier », puis la cellule du mois courant de la première catégorie à remplir ; elle ne déplie jamais la grille à votre place. Sur la page Transactions, la carte propose le clic droit et « Ajouter comme mot-clé » ; sur la page Budget, elle rappelle que seules les catégories du dernier niveau acceptent un montant et ce que fait la colonne Annuel
5. Dès que l'étape est faite, la carte reste affichée, annonce « Fait. » et propose l'étape suivante, avec un lien vers sa page. Sur la page Import, elle passe d'une étape à la suivante sans quitter la page et annonce « Fait. » une fois le fichier importé ; jamais pour un import qui n'a ajouté aucune transaction : le rapport dit pourquoi, et l'étape reste à faire 5. Dès que l'étape est faite, la carte reste affichée, annonce « Fait. » et propose l'étape suivante, avec un lien vers sa page. Sur la page Import, elle passe d'une étape à la suivante sans quitter la page et annonce « Fait. » une fois le fichier importé ; jamais pour un import qui n'a ajouté aucune transaction : le rapport dit pourquoi, et l'étape reste à faire
6. « Passer cette étape », sur la carte, marque l'étape comme passée : elle compte dans la progression, et la liste l'affiche avec le badge « Passée » et un lien « Reprendre ». « Reprendre » remet l'étape à faire et ouvre sa page 6. « Passer cette étape », sur la carte, marque l'étape comme passée : elle compte dans la progression, et la liste l'affiche avec le badge « Passée » et un lien « Reprendre ». « Reprendre » remet l'étape à faire et ouvre sa page
7. Quand toutes les étapes sont faites ou passées, le bandeau, ou la carte de la dernière étape, annonce « Démarrage terminé. Le guide d'utilisation vous accompagne pour la suite. » « Ouvrir le guide » mène à cette section Premiers pas, et « Fermer » masque le parcours 7. Quand toutes les étapes sont faites ou passées, le bandeau, ou la carte de la dernière étape, annonce « Démarrage terminé. Le guide d'utilisation vous accompagne pour la suite. » « Ouvrir le guide » mène à cette section Premiers pas, et « Fermer » masque le parcours

View file

@ -91,12 +91,12 @@
"failed": "The import added no transaction; the report below says why." "failed": "The import added no transaction; the report below says why."
}, },
"transactions": { "transactions": {
"uncategorized": "Only {{filter, lowercase}} transactions are shown. Click the category of one of them and pick one from the list. Simpl'Résultat keeps your choice, and automatic categorization will not replace it.", "uncategorized": "Only {{filter, lowercase}} transactions are shown. Right-click one of them, then “{{addKeyword}}”: the keyword categorizes it and the similar transactions in the preview, and will apply to future imports. Keep a word specific to the merchant, not “PAYMENT”, and uncheck the transactions already correctly categorized. You can also click its category and pick one.",
"all": "Click a transaction's category and pick one from the list. Simpl'Résultat keeps your choice, and automatic categorization will not replace it." "all": "Right-click a transaction, then “{{addKeyword}}”: the keyword categorizes it and the similar transactions in the preview, and will apply to future imports. Keep a word specific to the merchant, not “PAYMENT”, and uncheck the transactions already correctly categorized. You can also click its category and pick one."
}, },
"budget": { "budget": {
"collapsed": "Click {{expand}} to show your categories, then click a month and enter an amount. Enter saves; Tab saves and moves to the next month.", "collapsed": "Click “{{expand}}” to show your categories. Only the last-level ones, without a chevron, take an amount: click a month and type it (Enter saves; Tab saves and moves to the next month). An amount typed in the “{{annual}}” column is split evenly over the 12 months and replaces the monthly amounts.",
"flat": "Click a month and enter an amount. Enter saves; Tab saves and moves to the next month." "flat": "Only last-level categories, without a chevron, take an amount: click a month and type it (Enter saves; Tab saves and moves to the next month). An amount typed in the “{{annual}}” column is split evenly over the 12 months and replaces the monthly amounts."
} }
}, },
"state": { "state": {
@ -916,7 +916,7 @@
"The getting-started banner shows at the top of every page while the path is not hidden. It shows your progress, for instance \"Getting started 3/6\", and the next step, with a link to the page where it is done. It does not show on the dashboard of a profile without transactions, whose welcome card already holds the list", "The getting-started banner shows at the top of every page while the path is not hidden. It shows your progress, for instance \"Getting started 3/6\", and the next step, with a link to the page where it is done. It does not show on the dashboard of a profile without transactions, whose welcome card already holds the list",
"\"Show the list\" unfolds the six steps under the banner and \"Close the list\" folds them; the list also folds when you change pages. Each step still to do leads to the page where it is done", "\"Show the list\" unfolds the six steps under the banner and \"Close the list\" folds them; the list also folds when you change pages. Each step still to do leads to the page where it is done",
"On the Import, Transactions and Budget pages, while the next step is done there, a getting-started card takes the place of the banner. It shows \"Step n of N\", says what to do and puts an outline around the control to use. The outline pulses three times, and not at all if your system asks for reduced motion. When you arrive by a \"Next step\" link, the keyboard focus goes to the card's title", "On the Import, Transactions and Budget pages, while the next step is done there, a getting-started card takes the place of the banner. It shows \"Step n of N\", says what to do and puts an outline around the control to use. The outline pulses three times, and not at all if your system asks for reduced motion. When you arrive by a \"Next step\" link, the keyboard focus goes to the card's title",
"The card follows the step without you changing pages. On the Import page, it goes along with the wizard screen by screen. The link of the \"Categorize a transaction by hand\" step opens the Transactions page on the uncategorized transactions whatever their date, or on every transaction when none is left uncategorized. On the Budget page, the card first points at \"Expand all\", then at the current month's cell of the first category you can fill in; it never unfolds the grid for you", "The card follows the step without you changing pages. On the Import page, it goes along with the wizard screen by screen. The link of the \"Categorize a transaction by hand\" step opens the Transactions page on the uncategorized transactions whatever their date, or on every transaction when none is left uncategorized. On the Budget page, the card first points at \"Expand all\", then at the current month's cell of the first category you can fill in; it never unfolds the grid for you. On the Transactions page, the card suggests right-clicking and “Add as keyword”; on the Budget page, it recalls that only last-level categories take an amount and what the Annual column does",
"As soon as the step is done, the card stays, says \"Done.\" and offers the next step, with a link to its page. On the Import page, it moves from one step to the next without leaving the page and says \"Done.\" once the file is imported; never for an import that added no transaction: the report says why, and the step stays to do", "As soon as the step is done, the card stays, says \"Done.\" and offers the next step, with a link to its page. On the Import page, it moves from one step to the next without leaving the page and says \"Done.\" once the file is imported; never for an import that added no transaction: the report says why, and the step stays to do",
"\"Skip this step\", on the card, marks the step as skipped: it counts towards your progress, and the list shows it with the \"Skipped\" badge and a \"Resume\" link. \"Resume\" puts the step back to do and opens its page", "\"Skip this step\", on the card, marks the step as skipped: it counts towards your progress, and the list shows it with the \"Skipped\" badge and a \"Resume\" link. \"Resume\" puts the step back to do and opens its page",
"Once every step is done or skipped, the banner, or the card of the last step, says \"Getting started complete. The user guide takes it from here.\" \"Open the guide\" leads to this Getting Started section, and \"Close\" hides the path", "Once every step is done or skipped, the banner, or the card of the last step, says \"Getting started complete. The user guide takes it from here.\" \"Open the guide\" leads to this Getting Started section, and \"Close\" hides the path",

View file

@ -91,12 +91,12 @@
"failed": "L'import n'a ajouté aucune transaction ; le rapport ci-dessous dit pourquoi." "failed": "L'import n'a ajouté aucune transaction ; le rapport ci-dessous dit pourquoi."
}, },
"transactions": { "transactions": {
"uncategorized": "Seules les transactions {{filter, lowercase}} sont affichées. Cliquez sur la catégorie de l'une d'elles et choisissez-en une dans la liste. Simpl'Résultat retient ce choix, et la catégorisation automatique ne le remplacera pas.", "uncategorized": "Seules les transactions {{filter, lowercase}} sont affichées. Faites un clic droit sur l'une d'elles, puis « {{addKeyword}} » : le mot-clé classe cette transaction et les transactions semblables de l'aperçu, et servira aux prochains imports. Gardez un mot propre au commerçant, pas « PAIEMENT », et décochez les transactions déjà bien classées. Vous pouvez aussi cliquer sur sa catégorie et en choisir une.",
"all": "Cliquez sur la catégorie d'une transaction et choisissez-en une dans la liste. Simpl'Résultat retient ce choix, et la catégorisation automatique ne le remplacera pas." "all": "Faites un clic droit sur une transaction, puis « {{addKeyword}} » : le mot-clé classe cette transaction et les transactions semblables de l'aperçu, et servira aux prochains imports. Gardez un mot propre au commerçant, pas « PAIEMENT », et décochez les transactions déjà bien classées. Vous pouvez aussi cliquer sur sa catégorie et en choisir une."
}, },
"budget": { "budget": {
"collapsed": "Cliquez {{expand}} pour afficher vos catégories, puis cliquez sur un mois et saisissez un montant. Entrée enregistre ; Tab enregistre et passe au mois suivant.", "collapsed": "Cliquez « {{expand}} » pour afficher vos catégories. Seules celles du dernier niveau, sans chevron, acceptent un montant : cliquez sur un mois et saisissez-le (Entrée enregistre ; Tab enregistre et passe au mois suivant). Un montant saisi dans la colonne « {{annual}} » est réparti également sur les 12 mois et remplace les montants mensuels.",
"flat": "Cliquez sur un mois et saisissez un montant. Entrée enregistre ; Tab enregistre et passe au mois suivant." "flat": "Seules les catégories du dernier niveau, sans chevron, acceptent un montant : cliquez sur un mois et saisissez-le (Entrée enregistre ; Tab enregistre et passe au mois suivant). Un montant saisi dans la colonne « {{annual}} » est réparti également sur les 12 mois et remplace les montants mensuels."
} }
}, },
"state": { "state": {
@ -916,7 +916,7 @@
"Le bandeau de démarrage s'affiche en haut de chaque page tant que le parcours n'est pas masqué. Il indique votre progression, par exemple « Démarrage 3/6 », et l'étape suivante, avec un lien vers la page où la faire. Il n'apparaît pas sur le tableau de bord d'un profil sans transaction, dont la carte d'accueil contient déjà la liste", "Le bandeau de démarrage s'affiche en haut de chaque page tant que le parcours n'est pas masqué. Il indique votre progression, par exemple « Démarrage 3/6 », et l'étape suivante, avec un lien vers la page où la faire. Il n'apparaît pas sur le tableau de bord d'un profil sans transaction, dont la carte d'accueil contient déjà la liste",
"« Voir la liste » déplie les six étapes sous le bandeau et « Fermer la liste » les replie ; la liste se replie aussi quand vous changez de page. Chaque étape à faire mène à la page où la faire", "« Voir la liste » déplie les six étapes sous le bandeau et « Fermer la liste » les replie ; la liste se replie aussi quand vous changez de page. Chaque étape à faire mène à la page où la faire",
"Sur les pages Import, Transactions et Budget, quand l'étape suivante s'y fait, une carte de démarrage remplace le bandeau. Elle indique « Étape n sur N », dit quoi faire et entoure d'un contour le contrôle à utiliser. Le contour pulse trois fois, et pas du tout si votre système demande de réduire les animations. Si vous arrivez par un lien « Étape suivante », le focus du clavier se place sur le titre de la carte", "Sur les pages Import, Transactions et Budget, quand l'étape suivante s'y fait, une carte de démarrage remplace le bandeau. Elle indique « Étape n sur N », dit quoi faire et entoure d'un contour le contrôle à utiliser. Le contour pulse trois fois, et pas du tout si votre système demande de réduire les animations. Si vous arrivez par un lien « Étape suivante », le focus du clavier se place sur le titre de la carte",
"La carte suit l'étape sans que vous changiez de page. Sur la page Import, elle accompagne l'assistant écran par écran. Le lien de l'étape « Classer une transaction à la main » ouvre la page Transactions sur les transactions non catégorisées, toutes dates confondues, ou sur toutes les transactions s'il n'en reste aucune à classer. Sur la page Budget, la carte désigne d'abord « Tout déplier », puis la cellule du mois courant de la première catégorie à remplir ; elle ne déplie jamais la grille à votre place", "La carte suit l'étape sans que vous changiez de page. Sur la page Import, elle accompagne l'assistant écran par écran. Le lien de l'étape « Classer une transaction à la main » ouvre la page Transactions sur les transactions non catégorisées, toutes dates confondues, ou sur toutes les transactions s'il n'en reste aucune à classer. Sur la page Budget, la carte désigne d'abord « Tout déplier », puis la cellule du mois courant de la première catégorie à remplir ; elle ne déplie jamais la grille à votre place. Sur la page Transactions, la carte propose le clic droit et « Ajouter comme mot-clé » ; sur la page Budget, elle rappelle que seules les catégories du dernier niveau acceptent un montant et ce que fait la colonne Annuel",
"Dès que l'étape est faite, la carte reste affichée, annonce « Fait. » et propose l'étape suivante, avec un lien vers sa page. Sur la page Import, elle passe d'une étape à la suivante sans quitter la page et annonce « Fait. » une fois le fichier importé ; jamais pour un import qui n'a ajouté aucune transaction : le rapport dit pourquoi, et l'étape reste à faire", "Dès que l'étape est faite, la carte reste affichée, annonce « Fait. » et propose l'étape suivante, avec un lien vers sa page. Sur la page Import, elle passe d'une étape à la suivante sans quitter la page et annonce « Fait. » une fois le fichier importé ; jamais pour un import qui n'a ajouté aucune transaction : le rapport dit pourquoi, et l'étape reste à faire",
"« Passer cette étape », sur la carte, marque l'étape comme passée : elle compte dans la progression, et la liste l'affiche avec le badge « Passée » et un lien « Reprendre ». « Reprendre » remet l'étape à faire et ouvre sa page", "« Passer cette étape », sur la carte, marque l'étape comme passée : elle compte dans la progression, et la liste l'affiche avec le badge « Passée » et un lien « Reprendre ». « Reprendre » remet l'étape à faire et ouvre sa page",
"Quand toutes les étapes sont faites ou passées, le bandeau, ou la carte de la dernière étape, annonce « Démarrage terminé. Le guide d'utilisation vous accompagne pour la suite. » « Ouvrir le guide » mène à cette section Premiers pas, et « Fermer » masque le parcours", "Quand toutes les étapes sont faites ou passées, le bandeau, ou la carte de la dernière étape, annonce « Démarrage terminé. Le guide d'utilisation vous accompagne pour la suite. » « Ouvrir le guide » mène à cette section Premiers pas, et « Fermer » masque le parcours",

View file

@ -156,26 +156,26 @@ describe("TransactionsPage — the step card (#399)", () => {
expect(html).toContain("t(onboarding.checklist.items.manualCategory)"); expect(html).toContain("t(onboarding.checklist.items.manualCategory)");
}); });
it("the guided entry: only the uncategorized transactions, the filter named by its own label", () => { it("the guided entry: only the uncategorized transactions, the filter and 'Add as keyword' named by their own labels (#430)", () => {
expect(sentence(render(GUIDED))).toBe( expect(sentence(render(GUIDED))).toBe(
"t(onboarding.card.transactions.uncategorized[filter=t(transactions.filters.uncategorized)])", "t(onboarding.card.transactions.uncategorized[filter=t(transactions.filters.uncategorized),addKeyword=t(reports.keyword.addFromTransaction)])",
); );
}); });
it("an ordinary visit: the sentence for every transaction", () => { it("an ordinary visit: the sentence for every transaction, 'Add as keyword' named by its own label (#430)", () => {
expect(sentence(render())).toBe("t(onboarding.card.transactions.all[])"); expect(sentence(render())).toBe("t(onboarding.card.transactions.all[addKeyword=t(reports.keyword.addFromTransaction)])");
}); });
// The sentence follows the list's filter, not the navigation state. // The sentence follows the list's filter, not the navigation state.
it("the guided entry after its fallback: the sentence for every transaction", () => { it("the guided entry after its fallback: the sentence for every transaction", () => {
harness.filters = { uncategorizedOnly: false }; harness.filters = { uncategorizedOnly: false };
expect(sentence(render(GUIDED))).toBe("t(onboarding.card.transactions.all[])"); expect(sentence(render(GUIDED))).toBe("t(onboarding.card.transactions.all[addKeyword=t(reports.keyword.addFromTransaction)])");
}); });
it("an ordinary visit with 'Uncategorized' picked by hand: the uncategorized sentence", () => { it("an ordinary visit with 'Uncategorized' picked by hand: the uncategorized sentence", () => {
harness.filters = { uncategorizedOnly: true }; harness.filters = { uncategorizedOnly: true };
expect(sentence(render())).toBe( expect(sentence(render())).toBe(
"t(onboarding.card.transactions.uncategorized[filter=t(transactions.filters.uncategorized)])", "t(onboarding.card.transactions.uncategorized[filter=t(transactions.filters.uncategorized),addKeyword=t(reports.keyword.addFromTransaction)])",
); );
}); });

View file

@ -112,6 +112,31 @@ describe("applyKeywordWithReassignment", () => {
expect(commands.filter((c) => c.startsWith("UPDATE transactions"))).toHaveLength(2); expect(commands.filter((c) => c.startsWith("UPDATE transactions"))).toHaveLength(2);
}); });
// The Transactions step card leads to this dialog (#430): the transactions it
// categorizes must count as categorized by hand, so the step checks off.
it("marks every checked transaction as categorized by hand, with a literal 1 (#430)", async () => {
mockSelect.mockResolvedValueOnce([]);
mockExecute.mockResolvedValue({ lastInsertId: 77, rowsAffected: 1 });
await applyKeywordWithReassignment({
keyword: "METRO",
categoryId: 3,
transactionIds: [1, 2],
allowReplaceExisting: false,
});
const updates = mockExecute.mock.calls.filter((c) =>
(c[0] as string).trim().startsWith("UPDATE transactions"),
);
expect(updates.map((c) => c[1])).toEqual([
[3, 1],
[3, 2],
]);
for (const [sql] of updates) {
expect((sql as string).replace(/\s+/g, " ")).toContain("is_manually_categorized = 1,");
}
});
it("rolls back when an UPDATE throws", async () => { it("rolls back when an UPDATE throws", async () => {
mockSelect.mockResolvedValueOnce([]); mockSelect.mockResolvedValueOnce([]);
mockExecute mockExecute

View file

@ -9,6 +9,7 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import fr from "../i18n/locales/fr.json"; import fr from "../i18n/locales/fr.json";
import en from "../i18n/locales/en.json"; import en from "../i18n/locales/en.json";
import { budgetCardModel, transactionsCardModel } from "./onboardingPath";
type Cards = Record<string, string>; type Cards = Record<string, string>;
@ -45,3 +46,62 @@ describe("Import step card sentences quote every control they name (#422)", () =
expect(interpolations(IMPORT_CARDS[lang].filePreview).sort()).toEqual(["button", "flip"]); expect(interpolations(IMPORT_CARDS[lang].filePreview).sort()).toEqual(["button", "flip"]);
}); });
}); });
// The Transactions and Budget cards name their controls the same way (#430):
// "Add as keyword" on both Transactions sentences, "Expand all" and the
// "Annual" column on the Budget ones.
type PageCards = { transactions: Cards; budget: Cards };
const PAGE_CARDS: Record<"fr" | "en", PageCards> = {
fr: (fr as { onboarding: { card: PageCards } }).onboarding.card,
en: (en as { onboarding: { card: PageCards } }).onboarding.card,
};
/** Every sentence of the two cards, with the model that names its controls. */
const PAGE_SENTENCES = [
{ page: "transactions", key: "uncategorized", text: transactionsCardModel({ uncategorizedOnly: true }, null).text },
{ page: "transactions", key: "all", text: transactionsCardModel({ uncategorizedOnly: false }, null).text },
{ page: "budget", key: "collapsed", text: budgetCardModel({ target: "expandAll" }, null).text },
{ page: "budget", key: "flat", text: budgetCardModel({ target: "cell" }, null).text },
] as const;
/** The names of every interpolation of a sentence, formatted ones included (`{{filter, lowercase}}`). */
function allInterpolations(sentence: string): string[] {
return [...sentence.matchAll(/\{\{(\w+)(?:,[^}]*)?\}\}/g)].map((match) => match[1]);
}
describe("Transactions and Budget step card sentences quote every control they name (#430)", () => {
it.each(["fr", "en"] as const)("%s: each control interpolation sits between quotation marks", (lang) => {
let named = 0;
for (const page of ["transactions", "budget"] as const) {
for (const [key, sentence] of Object.entries(PAGE_CARDS[lang][page])) {
for (const name of interpolations(sentence)) {
named += 1;
expect(sentence, `onboarding.card.${page}.${key}: ${name}`).toMatch(QUOTED[lang](name));
}
}
}
// transactions: uncategorized 1, all 1; budget: collapsed 2, flat 1.
expect(named).toBe(5);
});
it.each(["fr", "en"] as const)("%s: the model labels exactly the interpolations of its sentence", (lang) => {
for (const { page, key, text } of PAGE_SENTENCES) {
expect(text?.key).toBe(`onboarding.card.${page}.${key}`);
const sentence = PAGE_CARDS[lang][page][key];
expect(Object.keys(text?.labels ?? {}).sort(), `${page}.${key}`).toEqual(
allInterpolations(sentence).sort(),
);
}
});
it.each(["fr", "en"] as const)("%s: both Transactions sentences name 'Add as keyword', both Budget ones the 'Annual' column", (lang) => {
for (const { page, key, text } of PAGE_SENTENCES) {
const expected = page === "transactions" ? "addKeyword" : "annual";
expect(interpolations(PAGE_CARDS[lang][page][key]), `${page}.${key}`).toContain(expected);
expect(text?.labels?.[expected], `${page}.${key}`).toBe(
page === "transactions" ? "reports.keyword.addFromTransaction" : "budget.annual",
);
}
});
});

View file

@ -610,13 +610,19 @@ describe("transactionsCardModel (#399)", () => {
const UNCATEGORIZED: TransactionsCardModel = { const UNCATEGORIZED: TransactionsCardModel = {
text: { text: {
key: "onboarding.card.transactions.uncategorized", key: "onboarding.card.transactions.uncategorized",
labels: { filter: "transactions.filters.uncategorized" }, labels: {
filter: "transactions.filters.uncategorized",
addKeyword: "reports.keyword.addFromTransaction",
},
}, },
done: false, done: false,
target: "firstRowCategory", target: "firstRowCategory",
}; };
const ALL: TransactionsCardModel = { const ALL: TransactionsCardModel = {
text: { key: "onboarding.card.transactions.all" }, text: {
key: "onboarding.card.transactions.all",
labels: { addKeyword: "reports.keyword.addFromTransaction" },
},
done: false, done: false,
target: "firstRowCategory", target: "firstRowCategory",
}; };
@ -654,19 +660,19 @@ describe("transactionsCardModel (#399)", () => {
it("reads the two sentences in French, the filter's label in lowercase", () => { it("reads the two sentences in French, the filter's label in lowercase", () => {
expect(render(UNCATEGORIZED.text, "fr")).toBe( expect(render(UNCATEGORIZED.text, "fr")).toBe(
"Seules les transactions non catégorisées sont affichées. Cliquez sur la catégorie de l'une d'elles et choisissez-en une dans la liste. Simpl'Résultat retient ce choix, et la catégorisation automatique ne le remplacera pas.", "Seules les transactions non catégorisées sont affichées. Faites un clic droit sur l'une d'elles, puis « Ajouter comme mot-clé » : le mot-clé classe cette transaction et les transactions semblables de l'aperçu, et servira aux prochains imports. Gardez un mot propre au commerçant, pas « PAIEMENT », et décochez les transactions déjà bien classées. Vous pouvez aussi cliquer sur sa catégorie et en choisir une.",
); );
expect(render(ALL.text, "fr")).toBe( expect(render(ALL.text, "fr")).toBe(
"Cliquez sur la catégorie d'une transaction et choisissez-en une dans la liste. Simpl'Résultat retient ce choix, et la catégorisation automatique ne le remplacera pas.", "Faites un clic droit sur une transaction, puis « Ajouter comme mot-clé » : le mot-clé classe cette transaction et les transactions semblables de l'aperçu, et servira aux prochains imports. Gardez un mot propre au commerçant, pas « PAIEMENT », et décochez les transactions déjà bien classées. Vous pouvez aussi cliquer sur sa catégorie et en choisir une.",
); );
}); });
it("reads the two sentences in English, the filter's label in lowercase", () => { it("reads the two sentences in English, the filter's label in lowercase", () => {
expect(render(UNCATEGORIZED.text, "en")).toBe( expect(render(UNCATEGORIZED.text, "en")).toBe(
"Only uncategorized transactions are shown. Click the category of one of them and pick one from the list. Simpl'Résultat keeps your choice, and automatic categorization will not replace it.", "Only uncategorized transactions are shown. Right-click one of them, then “Add as keyword”: the keyword categorizes it and the similar transactions in the preview, and will apply to future imports. Keep a word specific to the merchant, not “PAYMENT”, and uncheck the transactions already correctly categorized. You can also click its category and pick one.",
); );
expect(render(ALL.text, "en")).toBe( expect(render(ALL.text, "en")).toBe(
"Click a transaction's category and pick one from the list. Simpl'Résultat keeps your choice, and automatic categorization will not replace it.", "Right-click a transaction, then “Add as keyword”: the keyword categorizes it and the similar transactions in the preview, and will apply to future imports. Keep a word specific to the merchant, not “PAYMENT”, and uncheck the transactions already correctly categorized. You can also click its category and pick one.",
); );
}); });
}); });
@ -677,13 +683,13 @@ describe("budgetCardModel (#400)", () => {
const COLLAPSED: BudgetCardModel = { const COLLAPSED: BudgetCardModel = {
text: { text: {
key: "onboarding.card.budget.collapsed", key: "onboarding.card.budget.collapsed",
labels: { expand: "reports.collapse.expandAll" }, labels: { expand: "reports.collapse.expandAll", annual: "budget.annual" },
}, },
done: false, done: false,
target: "expandAll", target: "expandAll",
}; };
const FLAT: BudgetCardModel = { const FLAT: BudgetCardModel = {
text: { key: "onboarding.card.budget.flat" }, text: { key: "onboarding.card.budget.flat", labels: { annual: "budget.annual" } },
done: false, done: false,
target: "cell", target: "cell",
}; };
@ -724,19 +730,19 @@ describe("budgetCardModel (#400)", () => {
it("reads the two sentences in French, the button named by its label", () => { it("reads the two sentences in French, the button named by its label", () => {
expect(render(COLLAPSED.text, "fr")).toBe( expect(render(COLLAPSED.text, "fr")).toBe(
"Cliquez Tout déplier pour afficher vos catégories, puis cliquez sur un mois et saisissez un montant. Entrée enregistre ; Tab enregistre et passe au mois suivant.", "Cliquez « Tout déplier » pour afficher vos catégories. Seules celles du dernier niveau, sans chevron, acceptent un montant : cliquez sur un mois et saisissez-le (Entrée enregistre ; Tab enregistre et passe au mois suivant). Un montant saisi dans la colonne « Annuel » est réparti également sur les 12 mois et remplace les montants mensuels.",
); );
expect(render(FLAT.text, "fr")).toBe( expect(render(FLAT.text, "fr")).toBe(
"Cliquez sur un mois et saisissez un montant. Entrée enregistre ; Tab enregistre et passe au mois suivant.", "Seules les catégories du dernier niveau, sans chevron, acceptent un montant : cliquez sur un mois et saisissez-le (Entrée enregistre ; Tab enregistre et passe au mois suivant). Un montant saisi dans la colonne « Annuel » est réparti également sur les 12 mois et remplace les montants mensuels.",
); );
}); });
it("reads the two sentences in English, the button named by its label", () => { it("reads the two sentences in English, the button named by its label", () => {
expect(render(COLLAPSED.text, "en")).toBe( expect(render(COLLAPSED.text, "en")).toBe(
"Click Expand all to show your categories, then click a month and enter an amount. Enter saves; Tab saves and moves to the next month.", "Click “Expand all” to show your categories. Only the last-level ones, without a chevron, take an amount: click a month and type it (Enter saves; Tab saves and moves to the next month). An amount typed in the “Annual” column is split evenly over the 12 months and replaces the monthly amounts.",
); );
expect(render(FLAT.text, "en")).toBe( expect(render(FLAT.text, "en")).toBe(
"Click a month and enter an amount. Enter saves; Tab saves and moves to the next month.", "Only last-level categories, without a chevron, take an amount: click a month and type it (Enter saves; Tab saves and moves to the next month). An amount typed in the “Annual” column is split evenly over the 12 months and replaces the monthly amounts.",
); );
}); });
}); });

View file

@ -364,7 +364,8 @@ export interface TransactionsCardModel extends StepCardModel {
* - otherwise, the sentence that matches the list: the uncategorized * - otherwise, the sentence that matches the list: the uncategorized
* transactions only (the guided entry, or the filter picked by hand), the * transactions only (the guided entry, or the filter picked by hand), the
* filter named by its own label, or every transaction (an ordinary visit, * filter named by its own label, or every transaction (an ordinary visit,
* or a guided entry that found none uncategorized); and the category of the * or a guided entry that found none uncategorized); both name the context
* menu's "Add as keyword" by its own label (#430); and the category of the
* first row shown. * first row shown.
*/ */
export function transactionsCardModel( export function transactionsCardModel(
@ -376,9 +377,15 @@ export function transactionsCardModel(
text: filters.uncategorizedOnly text: filters.uncategorizedOnly
? { ? {
key: "onboarding.card.transactions.uncategorized", key: "onboarding.card.transactions.uncategorized",
labels: { filter: "transactions.filters.uncategorized" }, labels: {
filter: "transactions.filters.uncategorized",
addKeyword: "reports.keyword.addFromTransaction",
},
} }
: { key: "onboarding.card.transactions.all" }, : {
key: "onboarding.card.transactions.all",
labels: { addKeyword: "reports.keyword.addFromTransaction" },
},
done: false, done: false,
target: "firstRowCategory", target: "firstRowCategory",
}; };
@ -398,6 +405,8 @@ export interface BudgetCardModel extends StepCardModel {
* grid, the button named by its own label; * grid, the button named by its own label;
* - a month cell pointed at: the sentence for a grid whose editable rows * - a month cell pointed at: the sentence for a grid whose editable rows
* show; * show;
* - both sentences say only the last-level rows take an amount and name the
* "Annual" column by its own label (#430);
* - no target (the grid is not on the page, has no row, or has not said yet): * - no target (the grid is not on the page, has no row, or has not said yet):
* no sentence and no ring; the title and "Skip this step" stay. * no sentence and no ring; the title and "Skip this step" stay.
*/ */
@ -411,13 +420,17 @@ export function budgetCardModel(
return { return {
text: { text: {
key: "onboarding.card.budget.collapsed", key: "onboarding.card.budget.collapsed",
labels: { expand: "reports.collapse.expandAll" }, labels: { expand: "reports.collapse.expandAll", annual: "budget.annual" },
}, },
done: false, done: false,
target: "expandAll", target: "expandAll",
}; };
case "cell": case "cell":
return { text: { key: "onboarding.card.budget.flat" }, done: false, target: "cell" }; return {
text: { key: "onboarding.card.budget.flat", labels: { annual: "budget.annual" } },
done: false,
target: "cell",
};
default: default:
return { text: null, done: false, target: null }; return { text: null, done: false, target: null };
} }