judu/ex-egopublic Fork
⑂ 8a6909f
Commits
⬇ Clone ▾
git clone https://git.rickub.com/judu/ex-ego.git
git clone ssh://git@rickub.com/judu/ex-ego.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(i18n): ajoute le portugais du BrésilUnverified

Nouvelle langue pt-br : variante Lang::PtBr, table Tr complète, et
migration 0010 qui élargit le CHECK de users.lang.

Les dates portugaises intercalent « de » (« 3 de agosto », « Agosto de
2026 ») : day_month gagne un arm PtBr, day_full partage le séparateur
virgule avec l'allemand, et Month::label passe par un nouveau champ
month_year_pat (« {} {} » pour FR/EN/DE).

README : paragraphe portugais et mentions « trilingue » corrigées.
Julien Durillon committed 2026-08-16T23:39:33+02:00 Browse files
8a6909f parent: ce3c0cd
modified README.md +51 -22
@@ -4,8 +4,9 @@
44 *ex-ego* (le moyen : on dépose l'ego et on mesure de bonne foi).
55
66 Application web de comptage de temps par catégorie et de tâches partagées, en
7-équipe. Mobile-first (PWA), UI trilingue (français, anglais, allemand), rendu
8-100 % côté serveur, amélioration progressive en JavaScript.
7+équipe. Mobile-first (PWA), UI en quatre langues (français, anglais, allemand,
8+portugais du Brésil), rendu 100 % côté serveur, amélioration progressive en
9+JavaScript.
910
1011 Stack : Rust (axum, askama, sqlx), PostgreSQL, biscuit-auth. Aucun framework
1112 front, une seule feuille de CSS.
@@ -58,39 +59,67 @@ En utilisant Maydée puis Ex Ego, nous avons remarqué différentes choses chez
5859 ### English
5960
6061 Team time tracker and shared todo list.
62+
6163 The core idea is comparing *time spent* on household tasks
6264 (the fairest measure of domestic work, as long as everyone acts in good
6365 faith). Log time per category, run a timer, take dated tasks and
6466 reschedule them at their observed rhythm, and read stacked-bar statistics
65-per member. Having *data* about time spent on housework changes the nature
66-of the conversation: instead of debating feelings and recollections, you
67+per member.
68+
69+Having *data* about time spent on housework changes the nature
70+of the conversation. Instead of debating feelings and recollections, you
6771 look at numbers gathered in good faith (a calmer, less heated way to talk
68-about who does what). Not a SaaS for now: accounts are created manually
72+about who does what).
73+
74+Not a SaaS for now: accounts are created manually
6975 from the command line (no e-mail, no sign-up); a demo account can be
70-shared on request. Mobile-first
71-PWA, fully server-rendered (JavaScript is progressive enhancement only),
72-trilingual (FR/EN/DE, language is a per-user setting), with an optional
73-high-contrast black-and-white theme (per-user too, charts included). Rust
74-(axum, askama, sqlx), PostgreSQL, no front-end framework.
76+shared on request. Mobile-first PWA, fully server-rendered (JavaScript is progressive enhancement only),
77+available in four languages (FR/EN/DE/PT-BR, language is a per-user setting), with an optional
78+high-contrast black-and-white theme (per-user too, charts included).
7579
7680 ### Deustch
7781
7882 Zeiterfassung und geteilte Aufgabenliste fürs Team.
83+
7984 Kern der Idee ist der Vergleich der *aufgewendeten Zeit* für Hausarbeit (das fairste Maß für
8085 häusliche Arbeit, solange alle guten Willens sind). Zeiten pro Kategorie
8186 erfassen, Timer laufen lassen, datierte Aufgaben übernehmen und im
82-beobachteten Rhythmus neu planen, Statistiken pro Mitglied lesen. *Daten*
83-über die für Hausarbeit aufgewendete Zeit verändern das Gespräch: Statt
84-über Gefühle und Erinnerungen zu streiten, schaut man gemeinsam auf in
87+beobachteten Rhythmus neu planen, Statistiken pro Mitglied lesen.
88+
89+*Daten* über die für Hausarbeit aufgewendete Zeit verändern das Gespräch.
90+Statt über Gefühle und Erinnerungen zu streiten, schaut man gemeinsam auf in
8591 gutem Glauben erfasste Zahlen (eine ruhigere, sachlichere Art, über die
86-Aufgabenverteilung zu sprechen). Vorerst kein SaaS: Konten werden manuell
87-über die Kommandozeile angelegt (keine E-Mail, keine Registrierung); ein
88-Demo-Konto gibt es auf Anfrage.
92+Aufgabenverteilung zu sprechen).
93+
94+Vorerst kein SaaS: Konten werden manuell über die Kommandozeile angelegt
95+(keine E-Mail, keine Registrierung); ein Demo-Konto gibt es auf Anfrage.
8996 Mobile-first-PWA, vollständig serverseitig gerendert (JavaScript nur als
90-Progressive Enhancement), dreisprachig (FR/EN/DE, die Sprache ist eine
97+Progressive Enhancement), in vier Sprachen (FR/EN/DE/PT-BR, die Sprache ist eine
9198 Einstellung pro Benutzer), mit optionalem kontrastreichem
92-Schwarz-Weiß-Thema (ebenfalls pro Benutzer, inklusive Diagramme). Rust
93-(axum, askama, sqlx), PostgreSQL, ohne Frontend-Framework.
99+Schwarz-Weiß-Thema (ebenfalls pro Benutzer, inklusive Diagramme).
100+
101+### Português (Brasil)
102+
103+Contador de tempo em equipe e lista de tarefas compartilhada.
104+
105+A ideia central é comparar o *tempo gasto* nas tarefas domésticas (a medida
106+mais justa do trabalho de casa, desde que todo mundo aja de boa-fé). Dá para
107+registrar tempo por categoria, deixar um cronômetro rodando, pegar tarefas
108+datadas e reprogramá-las no ritmo observado, e ler estatísticas em barras
109+empilhadas por membro.
110+
111+Ter *dados* sobre o tempo gasto com a casa muda a natureza da conversa.
112+Em vez de discutir impressões e lembranças, vocês olham
113+juntos para números registrados de boa-fé (um jeito mais calmo e menos
114+inflamado de falar sobre quem faz o quê).
115+
116+Por enquanto não é um SaaS: as contas
117+são criadas manualmente pela linha de comando (sem e-mail, sem cadastro); uma
118+conta de demonstração pode ser compartilhada mediante pedido.
119+PWA mobile-first, renderizado 100 % no servidor (o JavaScript é só melhoria
120+progressiva), em quatro idiomas (FR/EN/DE/PT-BR, o idioma é uma configuração
121+por usuário), com um tema opcional em preto e branco de alto contraste (também
122+por usuário, gráficos incluídos).
94123
95124 ## Fonctionnalités
96125
@@ -114,8 +143,8 @@ Schwarz-Weiß-Thema (ebenfalls pro Benutzer, inklusive Diagramme). Rust
114143 - **Contraste élevé** : un thème noir et blanc, réglage **par utilisateur**
115144 (Réglages → Affichage), pour les écrans très sombres ou les filtres de
116145 lumière chaude.
117-- **Trilingue** : français, anglais, allemand. La langue est un réglage
118- **par utilisateur** (page Réglages). Langue par défaut à la création
146+- **Quadrilingue** : français, anglais, allemand, portugais du Brésil. La
147+ langue est un réglage **par utilisateur** (page Réglages). Langue par défaut à la création
119148 modifiable par la variable d'environnement `DEFAULT_LANG`.
120149
121150 ## Captures d'écran
@@ -155,7 +184,7 @@ Variables d'environnement du serveur :
155184 | `POSTGRESQL_ADDON_URI` ou `DATABASE_URL` | connexion PostgreSQL (TLS imposé côté client quoi que dise l'URL) |
156185 | `BISCUIT_PRIVATE_KEY` | clé privée racine (à générer une fois avec `exego keygen`) |
157186 | `PORT` | port d'écoute (défaut 8080) |
158-| `DEFAULT_LANG` | langue des nouveaux comptes et des pages anonymes : `fr` (défaut), `en` ou `de` |
187+| `DEFAULT_LANG` | langue des nouveaux comptes et des pages anonymes : `fr` (défaut), `en`, `de` ou `pt-br` |
159188
160189 ### Développement local
161190
@@ -4,8 +4,9 @@
4 *ex-ego* (le moyen : on dépose l'ego et on mesure de bonne foi).4 *ex-ego* (le moyen : on dépose l'ego et on mesure de bonne foi).
5 5
6 Application web de comptage de temps par catégorie et de tâches partagées, en6 Application web de comptage de temps par catégorie et de tâches partagées, en
7-équipe. Mobile-first (PWA), UI trilingue (français, anglais, allemand), rendu7+équipe. Mobile-first (PWA), UI en quatre langues (français, anglais, allemand,
8-100 % côté serveur, amélioration progressive en JavaScript.8+portugais du Brésil), rendu 100 % côté serveur, amélioration progressive en
9+JavaScript.
9 10
10 Stack : Rust (axum, askama, sqlx), PostgreSQL, biscuit-auth. Aucun framework11 Stack : Rust (axum, askama, sqlx), PostgreSQL, biscuit-auth. Aucun framework
11 front, une seule feuille de CSS.12 front, une seule feuille de CSS.
@@ -58,39 +59,67 @@ En utilisant Maydée puis Ex Ego, nous avons remarqué différentes choses chez
58 ### English59 ### English
59 60
60 Team time tracker and shared todo list.61 Team time tracker and shared todo list.
62+
61 The core idea is comparing *time spent* on household tasks63 The core idea is comparing *time spent* on household tasks
62 (the fairest measure of domestic work, as long as everyone acts in good64 (the fairest measure of domestic work, as long as everyone acts in good
63 faith). Log time per category, run a timer, take dated tasks and65 faith). Log time per category, run a timer, take dated tasks and
64 reschedule them at their observed rhythm, and read stacked-bar statistics66 reschedule them at their observed rhythm, and read stacked-bar statistics
65-per member. Having *data* about time spent on housework changes the nature67+per member.
66-of the conversation: instead of debating feelings and recollections, you68+
69+Having *data* about time spent on housework changes the nature
70+of the conversation. Instead of debating feelings and recollections, you
67 look at numbers gathered in good faith (a calmer, less heated way to talk71 look at numbers gathered in good faith (a calmer, less heated way to talk
68-about who does what). Not a SaaS for now: accounts are created manually72+about who does what).
73+
74+Not a SaaS for now: accounts are created manually
69 from the command line (no e-mail, no sign-up); a demo account can be75 from the command line (no e-mail, no sign-up); a demo account can be
70-shared on request. Mobile-first76+shared on request. Mobile-first PWA, fully server-rendered (JavaScript is progressive enhancement only),
71-PWA, fully server-rendered (JavaScript is progressive enhancement only),77+available in four languages (FR/EN/DE/PT-BR, language is a per-user setting), with an optional
72-trilingual (FR/EN/DE, language is a per-user setting), with an optional78+high-contrast black-and-white theme (per-user too, charts included).
73-high-contrast black-and-white theme (per-user too, charts included). Rust
74-(axum, askama, sqlx), PostgreSQL, no front-end framework.
75 79
76 ### Deustch80 ### Deustch
77 81
78 Zeiterfassung und geteilte Aufgabenliste fürs Team.82 Zeiterfassung und geteilte Aufgabenliste fürs Team.
83+
79 Kern der Idee ist der Vergleich der *aufgewendeten Zeit* für Hausarbeit (das fairste Maß für84 Kern der Idee ist der Vergleich der *aufgewendeten Zeit* für Hausarbeit (das fairste Maß für
80 häusliche Arbeit, solange alle guten Willens sind). Zeiten pro Kategorie85 häusliche Arbeit, solange alle guten Willens sind). Zeiten pro Kategorie
81 erfassen, Timer laufen lassen, datierte Aufgaben übernehmen und im86 erfassen, Timer laufen lassen, datierte Aufgaben übernehmen und im
82-beobachteten Rhythmus neu planen, Statistiken pro Mitglied lesen. *Daten*87+beobachteten Rhythmus neu planen, Statistiken pro Mitglied lesen.
83-über die für Hausarbeit aufgewendete Zeit verändern das Gespräch: Statt88+
84-über Gefühle und Erinnerungen zu streiten, schaut man gemeinsam auf in89+*Daten* über die für Hausarbeit aufgewendete Zeit verändern das Gespräch.
90+Statt über Gefühle und Erinnerungen zu streiten, schaut man gemeinsam auf in
85 gutem Glauben erfasste Zahlen (eine ruhigere, sachlichere Art, über die91 gutem Glauben erfasste Zahlen (eine ruhigere, sachlichere Art, über die
86-Aufgabenverteilung zu sprechen). Vorerst kein SaaS: Konten werden manuell92+Aufgabenverteilung zu sprechen).
87-über die Kommandozeile angelegt (keine E-Mail, keine Registrierung); ein93+
88-Demo-Konto gibt es auf Anfrage.94+Vorerst kein SaaS: Konten werden manuell über die Kommandozeile angelegt
95+(keine E-Mail, keine Registrierung); ein Demo-Konto gibt es auf Anfrage.
89 Mobile-first-PWA, vollständig serverseitig gerendert (JavaScript nur als96 Mobile-first-PWA, vollständig serverseitig gerendert (JavaScript nur als
90-Progressive Enhancement), dreisprachig (FR/EN/DE, die Sprache ist eine97+Progressive Enhancement), in vier Sprachen (FR/EN/DE/PT-BR, die Sprache ist eine
91 Einstellung pro Benutzer), mit optionalem kontrastreichem98 Einstellung pro Benutzer), mit optionalem kontrastreichem
92-Schwarz-Weiß-Thema (ebenfalls pro Benutzer, inklusive Diagramme). Rust99+Schwarz-Weiß-Thema (ebenfalls pro Benutzer, inklusive Diagramme).
93-(axum, askama, sqlx), PostgreSQL, ohne Frontend-Framework.100+
101+### Português (Brasil)
102+
103+Contador de tempo em equipe e lista de tarefas compartilhada.
104+
105+A ideia central é comparar o *tempo gasto* nas tarefas domésticas (a medida
106+mais justa do trabalho de casa, desde que todo mundo aja de boa-fé). Dá para
107+registrar tempo por categoria, deixar um cronômetro rodando, pegar tarefas
108+datadas e reprogramá-las no ritmo observado, e ler estatísticas em barras
109+empilhadas por membro.
110+
111+Ter *dados* sobre o tempo gasto com a casa muda a natureza da conversa.
112+Em vez de discutir impressões e lembranças, vocês olham
113+juntos para números registrados de boa-fé (um jeito mais calmo e menos
114+inflamado de falar sobre quem faz o quê).
115+
116+Por enquanto não é um SaaS: as contas
117+são criadas manualmente pela linha de comando (sem e-mail, sem cadastro); uma
118+conta de demonstração pode ser compartilhada mediante pedido.
119+PWA mobile-first, renderizado 100 % no servidor (o JavaScript é só melhoria
120+progressiva), em quatro idiomas (FR/EN/DE/PT-BR, o idioma é uma configuração
121+por usuário), com um tema opcional em preto e branco de alto contraste (também
122+por usuário, gráficos incluídos).
94 123
95 ## Fonctionnalités124 ## Fonctionnalités
96 125
@@ -114,8 +143,8 @@ Schwarz-Weiß-Thema (ebenfalls pro Benutzer, inklusive Diagramme). Rust
114 - **Contraste élevé** : un thème noir et blanc, réglage **par utilisateur**143 - **Contraste élevé** : un thème noir et blanc, réglage **par utilisateur**
115 (Réglages → Affichage), pour les écrans très sombres ou les filtres de144 (Réglages → Affichage), pour les écrans très sombres ou les filtres de
116 lumière chaude.145 lumière chaude.
117-- **Trilingue** : français, anglais, allemand. La langue est un réglage146+- **Quadrilingue** : français, anglais, allemand, portugais du Brésil. La
118- **par utilisateur** (page Réglages). Langue par défaut à la création147+ langue est un réglage **par utilisateur** (page Réglages). Langue par défaut à la création
119 modifiable par la variable d'environnement `DEFAULT_LANG`.148 modifiable par la variable d'environnement `DEFAULT_LANG`.
120 149
121 ## Captures d'écran150 ## Captures d'écran
@@ -155,7 +184,7 @@ Variables d'environnement du serveur :
155 | `POSTGRESQL_ADDON_URI` ou `DATABASE_URL` | connexion PostgreSQL (TLS imposé côté client quoi que dise l'URL) |184 | `POSTGRESQL_ADDON_URI` ou `DATABASE_URL` | connexion PostgreSQL (TLS imposé côté client quoi que dise l'URL) |
156 | `BISCUIT_PRIVATE_KEY` | clé privée racine (à générer une fois avec `exego keygen`) |185 | `BISCUIT_PRIVATE_KEY` | clé privée racine (à générer une fois avec `exego keygen`) |
157 | `PORT` | port d'écoute (défaut 8080) |186 | `PORT` | port d'écoute (défaut 8080) |
158-| `DEFAULT_LANG` | langue des nouveaux comptes et des pages anonymes : `fr` (défaut), `en` ou `de` |187+| `DEFAULT_LANG` | langue des nouveaux comptes et des pages anonymes : `fr` (défaut), `en`, `de` ou `pt-br` |
159 188
160 ### Développement local189 ### Développement local
161 190
added migrations/0010_lang_pt_br.sql +7 -0
new file mode 100644
@@ -0,0 +1,7 @@
1+-- Ajout du portugais du Brésil aux langues d'interface. Le code stocké est
2+-- 'pt-br' (tag BCP-47, repris tel quel dans <html lang>). L'ancien binaire
3+-- cohabite quelques secondes avec le nouveau : élargir le CHECK est sans
4+-- risque, aucune ligne existante n'est touchée.
5+ALTER TABLE users DROP CONSTRAINT IF EXISTS users_lang_check;
6+ALTER TABLE users ADD CONSTRAINT users_lang_check
7+ CHECK (lang IN ('fr', 'en', 'de', 'pt-br'));
new file mode 100644
@@ -0,0 +1,7 @@
1+-- Ajout du portugais du Brésil aux langues d'interface. Le code stocké est
2+-- 'pt-br' (tag BCP-47, repris tel quel dans <html lang>). L'ancien binaire
3+-- cohabite quelques secondes avec le nouveau : élargir le CHECK est sans
4+-- risque, aucune ligne existante n'est touchée.
5+ALTER TABLE users DROP CONSTRAINT IF EXISTS users_lang_check;
6+ALTER TABLE users ADD CONSTRAINT users_lang_check
7+ CHECK (lang IN ('fr', 'en', 'de', 'pt-br'));
modified src/i18n.rs +266 -14
@@ -1,4 +1,4 @@
1-//! Traductions FR / EN / DE. La langue est PAR UTILISATEUR, stockée en base
1+//! Traductions FR / EN / DE / PT-BR. La langue est PAR UTILISATEUR, stockée en base
22 //! (`users.lang`) — jamais par navigateur. Défaut à la création d'un compte :
33 //! `DEFAULT_LANG` (fr sinon). Les pages anonymes (login, erreurs) utilisent
44 //! la langue par défaut.
@@ -12,6 +12,7 @@ pub enum Lang {
1212 Fr,
1313 En,
1414 De,
15+ PtBr,
1516 }
1617
1718 impl Lang {
@@ -20,6 +21,7 @@ impl Lang {
2021 "fr" => Some(Lang::Fr),
2122 "en" => Some(Lang::En),
2223 "de" => Some(Lang::De),
24+ "pt-br" => Some(Lang::PtBr),
2325 _ => None,
2426 }
2527 }
@@ -29,6 +31,7 @@ impl Lang {
2931 Lang::Fr => "fr",
3032 Lang::En => "en",
3133 Lang::De => "de",
34+ Lang::PtBr => "pt-br",
3235 }
3336 }
3437
@@ -37,6 +40,7 @@ impl Lang {
3740 Lang::Fr => &FR,
3841 Lang::En => &EN,
3942 Lang::De => &DE,
43+ Lang::PtBr => &PT_BR,
4044 }
4145 }
4246
@@ -46,10 +50,11 @@ impl Lang {
4650 Lang::Fr => "Français",
4751 Lang::En => "English",
4852 Lang::De => "Deutsch",
53+ Lang::PtBr => "Português (BR)",
4954 }
5055 }
5156
52- pub const ALL: [Lang; 3] = [Lang::Fr, Lang::En, Lang::De];
57+ pub const ALL: [Lang; 4] = [Lang::Fr, Lang::En, Lang::De, Lang::PtBr];
5358 }
5459
5560 /// Interpolation à un trou (motifs `{}` des champs `*_pat`).
@@ -311,6 +316,8 @@ pub struct Tr {
311316 pub months: [&'static str; 12],
312317 pub today_suffix: &'static str,
313318 pub week_range_pat: &'static str,
319+ /// p2 : mois puis année — « Mars 2026 », mais „agosto de 2026“ en pt-BR.
320+ pub month_year_pat: &'static str,
314321 pub year_pat: &'static str,
315322 }
316323
@@ -399,10 +406,10 @@ pub static FR: Tr = Tr {
399406 toast_timer_cancelled: "Minuteur annulé",
400407 title_stats: "Statistiques",
401408 my_entries_arrow: "Mes saisies →",
402- today: "Aujourd'hui",
403- this_week: "Cette semaine",
404- this_month: "Ce mois-ci",
405- this_year: "Cette année",
409+ today: "À la journée",
410+ this_week: "À la semaine",
411+ this_month: "Au mois",
412+ this_year: "À l’année",
406413 all_time: "Depuis le début",
407414 no_time_period: "Aucun temps enregistré sur cette période.",
408415 all_history: "Tout l'historique",
@@ -573,6 +580,7 @@ pub static FR: Tr = Tr {
573580 ],
574581 today_suffix: " — aujourd'hui",
575582 week_range_pat: "Semaine du {} au {}",
583+ month_year_pat: "{} {}",
576584 year_pat: "Année {}",
577585 };
578586
@@ -633,10 +641,10 @@ pub static EN: Tr = Tr {
633641 toast_timer_cancelled: "Timer cancelled",
634642 title_stats: "Statistics",
635643 my_entries_arrow: "My entries →",
636- today: "Today",
637- this_week: "This week",
638- this_month: "This month",
639- this_year: "This year",
644+ today: "Daily",
645+ this_week: "Weekly",
646+ this_month: "Monthly",
647+ this_year: "Yearly",
640648 all_time: "All time",
641649 no_time_period: "No time recorded in this period.",
642650 all_history: "Entire history",
@@ -807,6 +815,7 @@ pub static EN: Tr = Tr {
807815 ],
808816 today_suffix: " — today",
809817 week_range_pat: "Week of {} to {}",
818+ month_year_pat: "{} {}",
810819 year_pat: "Year {}",
811820 };
812821
@@ -867,10 +876,10 @@ pub static DE: Tr = Tr {
867876 toast_timer_cancelled: "Timer abgebrochen",
868877 title_stats: "Statistik",
869878 my_entries_arrow: "Meine Einträge →",
870- today: "Heute",
871- this_week: "Diese Woche",
872- this_month: "Dieser Monat",
873- this_year: "Dieses Jahr",
879+ today: "Pro Tag",
880+ this_week: "Pro Woche",
881+ this_month: "Pro Monat",
882+ this_year: "Pro Jahr",
874883 all_time: "Seit Beginn",
875884 no_time_period: "In diesem Zeitraum wurde keine Zeit erfasst.",
876885 all_history: "Gesamter Verlauf",
@@ -1041,9 +1050,249 @@ pub static DE: Tr = Tr {
10411050 ],
10421051 today_suffix: " — heute",
10431052 week_range_pat: "Woche vom {} bis {}",
1053+ month_year_pat: "{} {}",
10441054 year_pat: "Jahr {}",
10451055 };
10461056
1057+pub static PT_BR: Tr = Tr {
1058+ tab_add: "Adicionar",
1059+ tab_stats: "Stats",
1060+ tab_todos: "Tarefas",
1061+ tab_settings: "Ajustes",
1062+ change_team: "Trocar de equipe",
1063+ close: "Fechar",
1064+ member_one: "membro",
1065+ member_many: "membros",
1066+ start_timer: "Iniciar o cronômetro",
1067+ stop_timer_aria: "Parar o cronômetro",
1068+ timer_running_note: "em andamento",
1069+ record_measured_aria: "Registrar o tempo medido",
1070+ record_measured_switch_aria: "Registrar o tempo medido (troca de equipe)",
1071+ measured_pat: "{} medidos",
1072+ in_team_pat: "na equipe {}",
1073+ timer_in_team_pat: "cronômetro na equipe {}",
1074+ login_title: "Entrar",
1075+ tagline: "O tempo dedicado a cada categoria, compartilhado com a sua equipe.",
1076+ username_label: "Usuário",
1077+ password_label: "Senha",
1078+ sign_in: "Entrar",
1079+ bad_credentials: "Dados de acesso incorretos.",
1080+ title_add: "Registrar tempo",
1081+ title_picked: "Categoria escolhida",
1082+ title_select_duration: "Quanto tempo?",
1083+ add_choice_hint: "Meça o tempo enquanto ele passa, ou informe uma duração que você já gastou.",
1084+ choice_timer_sub: "Você escolhe a categoria ao parar.",
1085+ choice_manual_sub: "Uma duração já gasta, em duas etapas.",
1086+ back: "← Voltar",
1087+ step1_hint: "Etapa 1 de 2 — escolha a categoria",
1088+ no_categories_yet: "Nenhuma categoria por enquanto — comece criando uma.",
1089+ manage_categories: "Gerenciar categorias",
1090+ change_category: "← Trocar de categoria",
1091+ duration_label: "Duração",
1092+ minutes_unit: "minutos",
1093+ date_label: "Data",
1094+ five_less: "Cinco minutos a menos",
1095+ five_more: "Cinco minutos a mais",
1096+ day_prev: "Dia anterior",
1097+ day_next: "Próximo dia",
1098+ forgot_timer: "Esqueceu o cronômetro? Dá para corrigir a duração na etapa seguinte.",
1099+ cancel_measured: "Descartar este tempo",
1100+ measured_pick_pat: "{} medidos — escolha a categoria",
1101+ week_hours_pat: "{} esta semana",
1102+ added_pat: "{} registrado ✓",
1103+ err_duration_min: "A duração deve ser de pelo menos 1 minuto.",
1104+ err_cat_team: "Esta categoria não pertence à equipe ativa.",
1105+ chrono_kicker: "Cronômetro em andamento",
1106+ started_at_pat: "Iniciado às {}",
1107+ chrono_hint: "Encerre o cronômetro para escolher a categoria e registrar este tempo.",
1108+ finish_timer: "Encerrar o cronômetro",
1109+ cancel_timer: "Cancelar o cronômetro",
1110+ toast_measure_discarded: "Tempo medido descartado",
1111+ toast_timer_cancelled: "Cronômetro cancelado",
1112+ title_stats: "Estatísticas",
1113+ my_entries_arrow: "Meus registros →",
1114+ today: "Por dia",
1115+ this_week: "Por semana",
1116+ this_month: "Por mês",
1117+ this_year: "Por ano",
1118+ all_time: "Desde o início",
1119+ no_time_period: "Nenhum tempo registrado neste período.",
1120+ all_history: "Todo o histórico",
1121+ prev_period: "Período anterior",
1122+ next_period: "Próximo período",
1123+ share_of_time: "Proporção do tempo",
1124+ detail: "Detalhe",
1125+ entry_one: "registro",
1126+ entry_many: "registros",
1127+ week_activity_pat: "Atividade da semana — {}",
1128+ no_time_week: "Nenhum tempo nesta semana.",
1129+ share_of_day: "Proporção do dia",
1130+ title_times: "Meus tempos",
1131+ times_empty: "Você ainda não registrou nenhum tempo nesta equipe.",
1132+ see_more: "Ver mais",
1133+ edit_aria: "Editar",
1134+ title_edit: "Editar o tempo",
1135+ back_times: "← Meus tempos",
1136+ category_label: "Categoria",
1137+ duration_min_label: "Duração (min)",
1138+ save: "Salvar",
1139+ delete: "Excluir",
1140+ archived_suffix_pat: "{} (arquivada)",
1141+ toast_time_edited: "Tempo atualizado",
1142+ toast_time_deleted: "Tempo excluído",
1143+ title_categories: "Categorias",
1144+ categories_hint_pat: "As categorias pertencem à equipe {} e podem ser editadas por todos os seus membros. Arquivar uma categoria a tira das listas; os tempos já registrados continuam nas estatísticas.",
1145+ new_category_ph: "Nova categoria",
1146+ add: "Adicionar",
1147+ rename: "Renomear",
1148+ rename_cat_aria: "Renomear a categoria (Enter para confirmar)",
1149+ restore: "Restaurar",
1150+ archive_aria: "Arquivar",
1151+ rename_help: "Renomear: altere o nome e aperte Enter.",
1152+ back_add: "← Voltar ao registro",
1153+ toast_cat_added: "Categoria adicionada",
1154+ toast_cat_renamed: "Categoria renomeada",
1155+ toast_cat_archived: "Categoria arquivada — os tempos continuam nas estatísticas",
1156+ toast_cat_restored: "Categoria restaurada",
1157+ err_cat_dup: "Já existe uma categoria com esse nome",
1158+ err_cat_empty: "O nome não pode ficar vazio",
1159+ groups_title: "Grupos",
1160+ groups_hint: "Um grupo separa certas categorias: o tempo delas sai das estatísticas da equipe e aparece numa visão dedicada. Útil para um assunto que você quer acompanhar à parte — raramente necessário.",
1161+ group_name_label: "Nome do grupo",
1162+ group_name_ph: "ex.: Cuidados com a Gisèle",
1163+ set_apart: "Separar",
1164+ ungroup_aria_pat: "Retirar {} do grupo {}",
1165+ toast_grouped_pat: "Separada em “{}”",
1166+ toast_ungrouped_pat: "{} volta para as estatísticas da equipe",
1167+ scope_pills_aria: "Escopo das estatísticas",
1168+ title_settings: "Configurações",
1169+ invitation_kicker: "Convite",
1170+ invite_text_pat: "{} convida você para entrar na equipe “{}”.",
1171+ accept: "Aceitar",
1172+ refuse: "Recusar",
1173+ account_kicker: "Conta",
1174+ account_line_pat: "Membro de {} equipes. As contas são criadas pela linha de comando.",
1175+ account_line_single: "As contas são criadas pela linha de comando.",
1176+ my_entries: "Meus registros",
1177+ my_entries_desc: "Ver e corrigir os tempos que eu registrei.",
1178+ manage_categories_desc: "Adicionar, renomear ou arquivar as categorias da equipe.",
1179+ team_name_label: "Nome da equipe",
1180+ rename_team_note: "O novo nome fica visível para todos os membros.",
1181+ members_of_pat: "Membros de {}",
1182+ invite_user_label: "Convidar alguém",
1183+ username_ph: "quem convidar",
1184+ invite: "Convidar",
1185+ invite_note: "A pessoa precisa já ter uma conta. Ela recebe um convite para aceitar do lado dela.",
1186+ pending_invites: "Convites pendentes",
1187+ invited_by_pat: "convidado por {} · pendente",
1188+ cancel: "Cancelar",
1189+ create_team: "Criar uma equipe",
1190+ create: "Criar",
1191+ new_team_aria: "Nome da nova equipe",
1192+ create_team_note: "Você já muda para ela — depois convide quem quiser.",
1193+ leave_team_pat: "Sair de “{}”",
1194+ leave_note: "Os tempos que você já registrou continuam nas estatísticas da equipe.",
1195+ language: "Idioma",
1196+ display: "Exibição",
1197+ display_hint: "O modo de alto contraste deixa o aplicativo em preto e branco: legível numa tela bem escura ou com um filtro de luz quente.",
1198+ theme_organic: "Organic",
1199+ theme_organic_desc: "Creme e terracota, o estilo padrão.",
1200+ theme_hc: "Alto contraste",
1201+ theme_hc_desc: "Preto e branco, legível com pouquíssima luz ou com filtro quente.",
1202+ toast_theme_updated: "Exibição atualizada",
1203+ session: "Sessão",
1204+ current_password: "Senha atual",
1205+ new_password: "Nova senha",
1206+ confirmation: "Confirmação",
1207+ change_password: "Alterar a senha",
1208+ sign_out: "Sair da conta",
1209+ signout_note: "O biscuit da sessão será revogado.",
1210+ toast_lang_updated: "Idioma atualizado",
1211+ toast_password_updated: "Senha atualizada",
1212+ toast_invite_sent: "Convite enviado",
1213+ toast_invite_refused: "Convite recusado",
1214+ toast_invite_cancelled: "Convite cancelado",
1215+ team_created_pat: "Equipe {} criada",
1216+ toast_team_renamed: "Equipe renomeada",
1217+ toast_joined_pat: "Você entrou em {}",
1218+ toast_team_left: "Você saiu da equipe",
1219+ err_team_name_taken: "Esse nome de equipe já está em uso",
1220+ err_team_name_empty: "O nome da equipe não pode ficar vazio",
1221+ err_last_team: "Você não pode sair da sua última equipe",
1222+ err_password_short: "A nova senha deve ter pelo menos 8 caracteres.",
1223+ err_password_mismatch: "A confirmação não confere.",
1224+ err_password_wrong: "Senha atual incorreta.",
1225+ invite_unknown_pat: "Nenhuma conta “{}”.",
1226+ already_member_pat: "{} já faz parte da equipe.",
1227+ already_invited_pat: "Já existe um convite pendente para {}.",
1228+ title_todos: "Tarefas",
1229+ new_todo: "+ Nova tarefa",
1230+ todos_empty_title: "Nenhuma tarefa por enquanto",
1231+ todos_empty_body_pat: "Anote o que há para fazer em {}: qualquer pessoa pode pegar a tarefa, fazer, e o tempo gasto vai para as estatísticas.",
1232+ doing_block: "Em andamento",
1233+ todo_block: "A fazer",
1234+ my_tasks: "Minhas tarefas",
1235+ doing_filter_empty: "Nenhuma tarefa em andamento está atribuída a você.",
1236+ open_filter_empty: "Nenhuma tarefa a fazer está atribuída a você.",
1237+ upcoming: "Em breve",
1238+ take: "Pegar",
1239+ done_check: "Feita ✓",
1240+ release: "Soltar",
1241+ edit_todo_aria_pat: "Editar a tarefa {}",
1242+ taken_by_pat: "pega por {} · {}",
1243+ since_pat: "desde {}",
1244+ activated_today: "ativa desde hoje",
1245+ from_pat: "a partir de {}",
1246+ back_todos: "← Tarefas",
1247+ title_new_todo: "Nova tarefa",
1248+ title_edit_todo: "Editar a tarefa",
1249+ todo_name_label: "Nome da tarefa",
1250+ todo_name_ph: "Limpar o banheiro",
1251+ activation_date: "Data de ativação",
1252+ assign_to: "Atribuir a",
1253+ assign_note: "Opcional. Os outros membros podem pegar a tarefa mesmo assim.",
1254+ you_suffix: " (você)",
1255+ create_todo: "Criar a tarefa",
1256+ delete_todo: "Excluir a tarefa",
1257+ err_todo_incomplete: "Nome, categoria e data são obrigatórios.",
1258+ title_todo_done: "Tarefa concluída",
1259+ duration_spent: "Tempo gasto",
1260+ record_time: "Registrar um tempo",
1261+ time_date: "Data do tempo",
1262+ reschedule: "Reprogramar",
1263+ next_activation_aria: "Próxima ativação",
1264+ minutes_spent_aria: "Minutos gastos",
1265+ validate: "Confirmar",
1266+ rhythm_every_pat: "Ritmo observado: a cada ~{} dias",
1267+ rhythm_every_one: "Ritmo observado: mais ou menos todo dia",
1268+ rhythm_none_pat: "Nenhum ritmo observado ainda — proposta de {} dias.",
1269+ toast_todo_created: "Tarefa criada ✓",
1270+ toast_todo_edited: "Tarefa atualizada ✓",
1271+ toast_todo_deleted: "Tarefa excluída",
1272+ toast_todo_taken: "Tarefa pega ✓",
1273+ toast_todo_released: "Tarefa solta",
1274+ toast_todo_done: "Tarefa feita ✓",
1275+ toast_todo_done_reprog: "Feita e reprogramada ✓",
1276+ err_404: "Página não encontrada.",
1277+ err_csrf: "O formulário expirou, tente de novo.",
1278+ err_500: "Erro interno.",
1279+ back_home: "← Início",
1280+ weekdays: [
1281+ "segunda-feira", "terça-feira", "quarta-feira", "quinta-feira",
1282+ "sexta-feira", "sábado", "domingo",
1283+ ],
1284+ // Usage brésilien courant : « 2ª » = segunda-feira, etc.
1285+ weekdays_short: ["2ª", "3ª", "4ª", "5ª", "6ª", "Sá", "Do"],
1286+ months: [
1287+ "janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto",
1288+ "setembro", "outubro", "novembro", "dezembro",
1289+ ],
1290+ today_suffix: " — hoje",
1291+ week_range_pat: "Semana de {} a {}",
1292+ month_year_pat: "{} de {}",
1293+ year_pat: "Ano {}",
1294+};
1295+
10471296 #[cfg(test)]
10481297 mod tests {
10491298 use super::*;
@@ -1064,5 +1313,8 @@ mod tests {
10641313 assert_eq!(Lang::from_code(l.code()), Some(l));
10651314 }
10661315 assert_eq!(Lang::from_code("es"), None);
1316+ // Le code pt-br est aussi la valeur stockée en base (CHECK de 0010).
1317+ assert_eq!(Lang::from_code("pt-br"), Some(Lang::PtBr));
1318+ assert_eq!(Lang::from_code("pt"), None);
10671319 }
10681320 }
@@ -1,4 +1,4 @@
1-//! Traductions FR / EN / DE. La langue est PAR UTILISATEUR, stockée en base1+//! Traductions FR / EN / DE / PT-BR. La langue est PAR UTILISATEUR, stockée en base
2 //! (`users.lang`) — jamais par navigateur. Défaut à la création d'un compte :2 //! (`users.lang`) — jamais par navigateur. Défaut à la création d'un compte :
3 //! `DEFAULT_LANG` (fr sinon). Les pages anonymes (login, erreurs) utilisent3 //! `DEFAULT_LANG` (fr sinon). Les pages anonymes (login, erreurs) utilisent
4 //! la langue par défaut.4 //! la langue par défaut.
@@ -12,6 +12,7 @@ pub enum Lang {
12 Fr,12 Fr,
13 En,13 En,
14 De,14 De,
15+ PtBr,
15 }16 }
16 17
17 impl Lang {18 impl Lang {
@@ -20,6 +21,7 @@ impl Lang {
20 "fr" => Some(Lang::Fr),21 "fr" => Some(Lang::Fr),
21 "en" => Some(Lang::En),22 "en" => Some(Lang::En),
22 "de" => Some(Lang::De),23 "de" => Some(Lang::De),
24+ "pt-br" => Some(Lang::PtBr),
23 _ => None,25 _ => None,
24 }26 }
25 }27 }
@@ -29,6 +31,7 @@ impl Lang {
29 Lang::Fr => "fr",31 Lang::Fr => "fr",
30 Lang::En => "en",32 Lang::En => "en",
31 Lang::De => "de",33 Lang::De => "de",
34+ Lang::PtBr => "pt-br",
32 }35 }
33 }36 }
34 37
@@ -37,6 +40,7 @@ impl Lang {
37 Lang::Fr => &FR,40 Lang::Fr => &FR,
38 Lang::En => &EN,41 Lang::En => &EN,
39 Lang::De => &DE,42 Lang::De => &DE,
43+ Lang::PtBr => &PT_BR,
40 }44 }
41 }45 }
42 46
@@ -46,10 +50,11 @@ impl Lang {
46 Lang::Fr => "Français",50 Lang::Fr => "Français",
47 Lang::En => "English",51 Lang::En => "English",
48 Lang::De => "Deutsch",52 Lang::De => "Deutsch",
53+ Lang::PtBr => "Português (BR)",
49 }54 }
50 }55 }
51 56
52- pub const ALL: [Lang; 3] = [Lang::Fr, Lang::En, Lang::De];57+ pub const ALL: [Lang; 4] = [Lang::Fr, Lang::En, Lang::De, Lang::PtBr];
53 }58 }
54 59
55 /// Interpolation à un trou (motifs `{}` des champs `*_pat`).60 /// Interpolation à un trou (motifs `{}` des champs `*_pat`).
@@ -311,6 +316,8 @@ pub struct Tr {
311 pub months: [&'static str; 12],316 pub months: [&'static str; 12],
312 pub today_suffix: &'static str,317 pub today_suffix: &'static str,
313 pub week_range_pat: &'static str,318 pub week_range_pat: &'static str,
319+ /// p2 : mois puis année — « Mars 2026 », mais „agosto de 2026“ en pt-BR.
320+ pub month_year_pat: &'static str,
314 pub year_pat: &'static str,321 pub year_pat: &'static str,
315 }322 }
316 323
@@ -399,10 +406,10 @@ pub static FR: Tr = Tr {
399 toast_timer_cancelled: "Minuteur annulé",406 toast_timer_cancelled: "Minuteur annulé",
400 title_stats: "Statistiques",407 title_stats: "Statistiques",
401 my_entries_arrow: "Mes saisies →",408 my_entries_arrow: "Mes saisies →",
402- today: "Aujourd'hui",409+ today: "À la journée",
403- this_week: "Cette semaine",410+ this_week: "À la semaine",
404- this_month: "Ce mois-ci",411+ this_month: "Au mois",
405- this_year: "Cette année",412+ this_year: "À l’année",
406 all_time: "Depuis le début",413 all_time: "Depuis le début",
407 no_time_period: "Aucun temps enregistré sur cette période.",414 no_time_period: "Aucun temps enregistré sur cette période.",
408 all_history: "Tout l'historique",415 all_history: "Tout l'historique",
@@ -573,6 +580,7 @@ pub static FR: Tr = Tr {
573 ],580 ],
574 today_suffix: " — aujourd'hui",581 today_suffix: " — aujourd'hui",
575 week_range_pat: "Semaine du {} au {}",582 week_range_pat: "Semaine du {} au {}",
583+ month_year_pat: "{} {}",
576 year_pat: "Année {}",584 year_pat: "Année {}",
577 };585 };
578 586
@@ -633,10 +641,10 @@ pub static EN: Tr = Tr {
633 toast_timer_cancelled: "Timer cancelled",641 toast_timer_cancelled: "Timer cancelled",
634 title_stats: "Statistics",642 title_stats: "Statistics",
635 my_entries_arrow: "My entries →",643 my_entries_arrow: "My entries →",
636- today: "Today",644+ today: "Daily",
637- this_week: "This week",645+ this_week: "Weekly",
638- this_month: "This month",646+ this_month: "Monthly",
639- this_year: "This year",647+ this_year: "Yearly",
640 all_time: "All time",648 all_time: "All time",
641 no_time_period: "No time recorded in this period.",649 no_time_period: "No time recorded in this period.",
642 all_history: "Entire history",650 all_history: "Entire history",
@@ -807,6 +815,7 @@ pub static EN: Tr = Tr {
807 ],815 ],
808 today_suffix: " — today",816 today_suffix: " — today",
809 week_range_pat: "Week of {} to {}",817 week_range_pat: "Week of {} to {}",
818+ month_year_pat: "{} {}",
810 year_pat: "Year {}",819 year_pat: "Year {}",
811 };820 };
812 821
@@ -867,10 +876,10 @@ pub static DE: Tr = Tr {
867 toast_timer_cancelled: "Timer abgebrochen",876 toast_timer_cancelled: "Timer abgebrochen",
868 title_stats: "Statistik",877 title_stats: "Statistik",
869 my_entries_arrow: "Meine Einträge →",878 my_entries_arrow: "Meine Einträge →",
870- today: "Heute",879+ today: "Pro Tag",
871- this_week: "Diese Woche",880+ this_week: "Pro Woche",
872- this_month: "Dieser Monat",881+ this_month: "Pro Monat",
873- this_year: "Dieses Jahr",882+ this_year: "Pro Jahr",
874 all_time: "Seit Beginn",883 all_time: "Seit Beginn",
875 no_time_period: "In diesem Zeitraum wurde keine Zeit erfasst.",884 no_time_period: "In diesem Zeitraum wurde keine Zeit erfasst.",
876 all_history: "Gesamter Verlauf",885 all_history: "Gesamter Verlauf",
@@ -1041,9 +1050,249 @@ pub static DE: Tr = Tr {
1041 ],1050 ],
1042 today_suffix: " — heute",1051 today_suffix: " — heute",
1043 week_range_pat: "Woche vom {} bis {}",1052 week_range_pat: "Woche vom {} bis {}",
1053+ month_year_pat: "{} {}",
1044 year_pat: "Jahr {}",1054 year_pat: "Jahr {}",
1045 };1055 };
1046 1056
1057+pub static PT_BR: Tr = Tr {
1058+ tab_add: "Adicionar",
1059+ tab_stats: "Stats",
1060+ tab_todos: "Tarefas",
1061+ tab_settings: "Ajustes",
1062+ change_team: "Trocar de equipe",
1063+ close: "Fechar",
1064+ member_one: "membro",
1065+ member_many: "membros",
1066+ start_timer: "Iniciar o cronômetro",
1067+ stop_timer_aria: "Parar o cronômetro",
1068+ timer_running_note: "em andamento",
1069+ record_measured_aria: "Registrar o tempo medido",
1070+ record_measured_switch_aria: "Registrar o tempo medido (troca de equipe)",
1071+ measured_pat: "{} medidos",
1072+ in_team_pat: "na equipe {}",
1073+ timer_in_team_pat: "cronômetro na equipe {}",
1074+ login_title: "Entrar",
1075+ tagline: "O tempo dedicado a cada categoria, compartilhado com a sua equipe.",
1076+ username_label: "Usuário",
1077+ password_label: "Senha",
1078+ sign_in: "Entrar",
1079+ bad_credentials: "Dados de acesso incorretos.",
1080+ title_add: "Registrar tempo",
1081+ title_picked: "Categoria escolhida",
1082+ title_select_duration: "Quanto tempo?",
1083+ add_choice_hint: "Meça o tempo enquanto ele passa, ou informe uma duração que você já gastou.",
1084+ choice_timer_sub: "Você escolhe a categoria ao parar.",
1085+ choice_manual_sub: "Uma duração já gasta, em duas etapas.",
1086+ back: "← Voltar",
1087+ step1_hint: "Etapa 1 de 2 — escolha a categoria",
1088+ no_categories_yet: "Nenhuma categoria por enquanto — comece criando uma.",
1089+ manage_categories: "Gerenciar categorias",
1090+ change_category: "← Trocar de categoria",
1091+ duration_label: "Duração",
1092+ minutes_unit: "minutos",
1093+ date_label: "Data",
1094+ five_less: "Cinco minutos a menos",
1095+ five_more: "Cinco minutos a mais",
1096+ day_prev: "Dia anterior",
1097+ day_next: "Próximo dia",
1098+ forgot_timer: "Esqueceu o cronômetro? Dá para corrigir a duração na etapa seguinte.",
1099+ cancel_measured: "Descartar este tempo",
1100+ measured_pick_pat: "{} medidos — escolha a categoria",
1101+ week_hours_pat: "{} esta semana",
1102+ added_pat: "{} registrado ✓",
1103+ err_duration_min: "A duração deve ser de pelo menos 1 minuto.",
1104+ err_cat_team: "Esta categoria não pertence à equipe ativa.",
1105+ chrono_kicker: "Cronômetro em andamento",
1106+ started_at_pat: "Iniciado às {}",
1107+ chrono_hint: "Encerre o cronômetro para escolher a categoria e registrar este tempo.",
1108+ finish_timer: "Encerrar o cronômetro",
1109+ cancel_timer: "Cancelar o cronômetro",
1110+ toast_measure_discarded: "Tempo medido descartado",
1111+ toast_timer_cancelled: "Cronômetro cancelado",
1112+ title_stats: "Estatísticas",
1113+ my_entries_arrow: "Meus registros →",
1114+ today: "Por dia",
1115+ this_week: "Por semana",
1116+ this_month: "Por mês",
1117+ this_year: "Por ano",
1118+ all_time: "Desde o início",
1119+ no_time_period: "Nenhum tempo registrado neste período.",
1120+ all_history: "Todo o histórico",
1121+ prev_period: "Período anterior",
1122+ next_period: "Próximo período",
1123+ share_of_time: "Proporção do tempo",
1124+ detail: "Detalhe",
1125+ entry_one: "registro",
1126+ entry_many: "registros",
1127+ week_activity_pat: "Atividade da semana — {}",
1128+ no_time_week: "Nenhum tempo nesta semana.",
1129+ share_of_day: "Proporção do dia",
1130+ title_times: "Meus tempos",
1131+ times_empty: "Você ainda não registrou nenhum tempo nesta equipe.",
1132+ see_more: "Ver mais",
1133+ edit_aria: "Editar",
1134+ title_edit: "Editar o tempo",
1135+ back_times: "← Meus tempos",
1136+ category_label: "Categoria",
1137+ duration_min_label: "Duração (min)",
1138+ save: "Salvar",
1139+ delete: "Excluir",
1140+ archived_suffix_pat: "{} (arquivada)",
1141+ toast_time_edited: "Tempo atualizado",
1142+ toast_time_deleted: "Tempo excluído",
1143+ title_categories: "Categorias",
1144+ categories_hint_pat: "As categorias pertencem à equipe {} e podem ser editadas por todos os seus membros. Arquivar uma categoria a tira das listas; os tempos já registrados continuam nas estatísticas.",
1145+ new_category_ph: "Nova categoria",
1146+ add: "Adicionar",
1147+ rename: "Renomear",
1148+ rename_cat_aria: "Renomear a categoria (Enter para confirmar)",
1149+ restore: "Restaurar",
1150+ archive_aria: "Arquivar",
1151+ rename_help: "Renomear: altere o nome e aperte Enter.",
1152+ back_add: "← Voltar ao registro",
1153+ toast_cat_added: "Categoria adicionada",
1154+ toast_cat_renamed: "Categoria renomeada",
1155+ toast_cat_archived: "Categoria arquivada — os tempos continuam nas estatísticas",
1156+ toast_cat_restored: "Categoria restaurada",
1157+ err_cat_dup: "Já existe uma categoria com esse nome",
1158+ err_cat_empty: "O nome não pode ficar vazio",
1159+ groups_title: "Grupos",
1160+ groups_hint: "Um grupo separa certas categorias: o tempo delas sai das estatísticas da equipe e aparece numa visão dedicada. Útil para um assunto que você quer acompanhar à parte — raramente necessário.",
1161+ group_name_label: "Nome do grupo",
1162+ group_name_ph: "ex.: Cuidados com a Gisèle",
1163+ set_apart: "Separar",
1164+ ungroup_aria_pat: "Retirar {} do grupo {}",
1165+ toast_grouped_pat: "Separada em “{}”",
1166+ toast_ungrouped_pat: "{} volta para as estatísticas da equipe",
1167+ scope_pills_aria: "Escopo das estatísticas",
1168+ title_settings: "Configurações",
1169+ invitation_kicker: "Convite",
1170+ invite_text_pat: "{} convida você para entrar na equipe “{}”.",
1171+ accept: "Aceitar",
1172+ refuse: "Recusar",
1173+ account_kicker: "Conta",
1174+ account_line_pat: "Membro de {} equipes. As contas são criadas pela linha de comando.",
1175+ account_line_single: "As contas são criadas pela linha de comando.",
1176+ my_entries: "Meus registros",
1177+ my_entries_desc: "Ver e corrigir os tempos que eu registrei.",
1178+ manage_categories_desc: "Adicionar, renomear ou arquivar as categorias da equipe.",
1179+ team_name_label: "Nome da equipe",
1180+ rename_team_note: "O novo nome fica visível para todos os membros.",
1181+ members_of_pat: "Membros de {}",
1182+ invite_user_label: "Convidar alguém",
1183+ username_ph: "quem convidar",
1184+ invite: "Convidar",
1185+ invite_note: "A pessoa precisa já ter uma conta. Ela recebe um convite para aceitar do lado dela.",
1186+ pending_invites: "Convites pendentes",
1187+ invited_by_pat: "convidado por {} · pendente",
1188+ cancel: "Cancelar",
1189+ create_team: "Criar uma equipe",
1190+ create: "Criar",
1191+ new_team_aria: "Nome da nova equipe",
1192+ create_team_note: "Você já muda para ela — depois convide quem quiser.",
1193+ leave_team_pat: "Sair de “{}”",
1194+ leave_note: "Os tempos que você já registrou continuam nas estatísticas da equipe.",
1195+ language: "Idioma",
1196+ display: "Exibição",
1197+ display_hint: "O modo de alto contraste deixa o aplicativo em preto e branco: legível numa tela bem escura ou com um filtro de luz quente.",
1198+ theme_organic: "Organic",
1199+ theme_organic_desc: "Creme e terracota, o estilo padrão.",
1200+ theme_hc: "Alto contraste",
1201+ theme_hc_desc: "Preto e branco, legível com pouquíssima luz ou com filtro quente.",
1202+ toast_theme_updated: "Exibição atualizada",
1203+ session: "Sessão",
1204+ current_password: "Senha atual",
1205+ new_password: "Nova senha",
1206+ confirmation: "Confirmação",
1207+ change_password: "Alterar a senha",
1208+ sign_out: "Sair da conta",
1209+ signout_note: "O biscuit da sessão será revogado.",
1210+ toast_lang_updated: "Idioma atualizado",
1211+ toast_password_updated: "Senha atualizada",
1212+ toast_invite_sent: "Convite enviado",
1213+ toast_invite_refused: "Convite recusado",
1214+ toast_invite_cancelled: "Convite cancelado",
1215+ team_created_pat: "Equipe {} criada",
1216+ toast_team_renamed: "Equipe renomeada",
1217+ toast_joined_pat: "Você entrou em {}",
1218+ toast_team_left: "Você saiu da equipe",
1219+ err_team_name_taken: "Esse nome de equipe já está em uso",
1220+ err_team_name_empty: "O nome da equipe não pode ficar vazio",
1221+ err_last_team: "Você não pode sair da sua última equipe",
1222+ err_password_short: "A nova senha deve ter pelo menos 8 caracteres.",
1223+ err_password_mismatch: "A confirmação não confere.",
1224+ err_password_wrong: "Senha atual incorreta.",
1225+ invite_unknown_pat: "Nenhuma conta “{}”.",
1226+ already_member_pat: "{} já faz parte da equipe.",
1227+ already_invited_pat: "Já existe um convite pendente para {}.",
1228+ title_todos: "Tarefas",
1229+ new_todo: "+ Nova tarefa",
1230+ todos_empty_title: "Nenhuma tarefa por enquanto",
1231+ todos_empty_body_pat: "Anote o que há para fazer em {}: qualquer pessoa pode pegar a tarefa, fazer, e o tempo gasto vai para as estatísticas.",
1232+ doing_block: "Em andamento",
1233+ todo_block: "A fazer",
1234+ my_tasks: "Minhas tarefas",
1235+ doing_filter_empty: "Nenhuma tarefa em andamento está atribuída a você.",
1236+ open_filter_empty: "Nenhuma tarefa a fazer está atribuída a você.",
1237+ upcoming: "Em breve",
1238+ take: "Pegar",
1239+ done_check: "Feita ✓",
1240+ release: "Soltar",
1241+ edit_todo_aria_pat: "Editar a tarefa {}",
1242+ taken_by_pat: "pega por {} · {}",
1243+ since_pat: "desde {}",
1244+ activated_today: "ativa desde hoje",
1245+ from_pat: "a partir de {}",
1246+ back_todos: "← Tarefas",
1247+ title_new_todo: "Nova tarefa",
1248+ title_edit_todo: "Editar a tarefa",
1249+ todo_name_label: "Nome da tarefa",
1250+ todo_name_ph: "Limpar o banheiro",
1251+ activation_date: "Data de ativação",
1252+ assign_to: "Atribuir a",
1253+ assign_note: "Opcional. Os outros membros podem pegar a tarefa mesmo assim.",
1254+ you_suffix: " (você)",
1255+ create_todo: "Criar a tarefa",
1256+ delete_todo: "Excluir a tarefa",
1257+ err_todo_incomplete: "Nome, categoria e data são obrigatórios.",
1258+ title_todo_done: "Tarefa concluída",
1259+ duration_spent: "Tempo gasto",
1260+ record_time: "Registrar um tempo",
1261+ time_date: "Data do tempo",
1262+ reschedule: "Reprogramar",
1263+ next_activation_aria: "Próxima ativação",
1264+ minutes_spent_aria: "Minutos gastos",
1265+ validate: "Confirmar",
1266+ rhythm_every_pat: "Ritmo observado: a cada ~{} dias",
1267+ rhythm_every_one: "Ritmo observado: mais ou menos todo dia",
1268+ rhythm_none_pat: "Nenhum ritmo observado ainda — proposta de {} dias.",
1269+ toast_todo_created: "Tarefa criada ✓",
1270+ toast_todo_edited: "Tarefa atualizada ✓",
1271+ toast_todo_deleted: "Tarefa excluída",
1272+ toast_todo_taken: "Tarefa pega ✓",
1273+ toast_todo_released: "Tarefa solta",
1274+ toast_todo_done: "Tarefa feita ✓",
1275+ toast_todo_done_reprog: "Feita e reprogramada ✓",
1276+ err_404: "Página não encontrada.",
1277+ err_csrf: "O formulário expirou, tente de novo.",
1278+ err_500: "Erro interno.",
1279+ back_home: "← Início",
1280+ weekdays: [
1281+ "segunda-feira", "terça-feira", "quarta-feira", "quinta-feira",
1282+ "sexta-feira", "sábado", "domingo",
1283+ ],
1284+ // Usage brésilien courant : « 2ª » = segunda-feira, etc.
1285+ weekdays_short: ["2ª", "3ª", "4ª", "5ª", "6ª", "Sá", "Do"],
1286+ months: [
1287+ "janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto",
1288+ "setembro", "outubro", "novembro", "dezembro",
1289+ ],
1290+ today_suffix: " — hoje",
1291+ week_range_pat: "Semana de {} a {}",
1292+ month_year_pat: "{} de {}",
1293+ year_pat: "Ano {}",
1294+};
1295+
1047 #[cfg(test)]1296 #[cfg(test)]
1048 mod tests {1297 mod tests {
1049 use super::*;1298 use super::*;
@@ -1064,5 +1313,8 @@ mod tests {
1064 assert_eq!(Lang::from_code(l.code()), Some(l));1313 assert_eq!(Lang::from_code(l.code()), Some(l));
1065 }1314 }
1066 assert_eq!(Lang::from_code("es"), None);1315 assert_eq!(Lang::from_code("es"), None);
1316+ // Le code pt-br est aussi la valeur stockée en base (CHECK de 0010).
1317+ assert_eq!(Lang::from_code("pt-br"), Some(Lang::PtBr));
1318+ assert_eq!(Lang::from_code("pt"), None);
1067 }1319 }
1068 }1320 }
modified src/period.rs +15 -7
@@ -19,19 +19,21 @@ fn capitalize(s: &str) -> String {
1919 }
2020 }
2121
22-/// « 3 août » / "3 August" / „3. August“
22+/// « 3 août » / "3 August" / „3. August“ / « 3 de agosto »
2323 pub fn day_month(lang: Lang, date: NaiveDate) -> String {
2424 let month = lang.tr().months[(date.month() - 1) as usize];
2525 match lang {
2626 Lang::De => format!("{}. {}", date.day(), month),
27+ Lang::PtBr => format!("{} de {}", date.day(), month),
2728 _ => format!("{} {}", date.day(), month),
2829 }
2930 }
3031
31-/// « Mercredi 6 août » / "Wednesday 6 August" / „Mittwoch, 6. August“
32+/// « Mercredi 6 août » / "Wednesday 6 August" / „Mittwoch, 6. August“ /
33+/// « Quarta-feira, 6 de agosto »
3234 pub fn day_full(lang: Lang, date: NaiveDate) -> String {
3335 let weekday = lang.tr().weekdays[date.weekday().num_days_from_monday() as usize];
34- let sep = if lang == Lang::De { ", " } else { " " };
36+ let sep = if matches!(lang, Lang::De | Lang::PtBr) { ", " } else { " " };
3537 capitalize(&format!("{weekday}{sep}{}", day_month(lang, date)))
3638 }
3739
@@ -175,12 +177,12 @@ impl Month {
175177 }
176178 }
177179
178- /// « Mars 2026 »
180+ /// « Mars 2026 » / « Agosto de 2026 »
179181 pub fn label(&self, lang: Lang) -> String {
180- capitalize(&format!(
181- "{} {}",
182+ capitalize(&p2(
183+ lang.tr().month_year_pat,
182184 lang.tr().months[(self.month - 1) as usize],
183- self.year
185+ self.year,
184186 ))
185187 }
186188 }
@@ -317,7 +319,13 @@ mod tests {
317319 assert_eq!(day_month(Lang::Fr, d(2026, 8, 3)), "3 août");
318320 assert_eq!(day_month(Lang::En, d(2026, 8, 3)), "3 August");
319321 assert_eq!(day_month(Lang::De, d(2026, 8, 3)), "3. August");
322+ assert_eq!(day_month(Lang::PtBr, d(2026, 8, 3)), "3 de agosto");
320323 assert_eq!(day_label(Lang::De, d(2026, 3, 5)), "Donnerstag, 5. März");
324+ assert_eq!(day_full(Lang::PtBr, d(2026, 3, 5)), "Quinta-feira, 5 de março");
325+ assert_eq!(
326+ Month::parse("2026-03").unwrap().label(Lang::PtBr),
327+ "Março de 2026"
328+ );
321329 // day_full = day_label sans le suffixe « aujourd'hui ».
322330 assert_eq!(day_full(Lang::Fr, d(2026, 3, 5)), "Jeudi 5 mars");
323331 assert_eq!(day_full(Lang::En, d(2026, 3, 5)), "Thursday 5 March");
@@ -19,19 +19,21 @@ fn capitalize(s: &str) -> String {
19 }19 }
20 }20 }
21 21
22-/// « 3 août » / "3 August" / „3. August“22+/// « 3 août » / "3 August" / „3. August“ / « 3 de agosto »
23 pub fn day_month(lang: Lang, date: NaiveDate) -> String {23 pub fn day_month(lang: Lang, date: NaiveDate) -> String {
24 let month = lang.tr().months[(date.month() - 1) as usize];24 let month = lang.tr().months[(date.month() - 1) as usize];
25 match lang {25 match lang {
26 Lang::De => format!("{}. {}", date.day(), month),26 Lang::De => format!("{}. {}", date.day(), month),
27+ Lang::PtBr => format!("{} de {}", date.day(), month),
27 _ => format!("{} {}", date.day(), month),28 _ => format!("{} {}", date.day(), month),
28 }29 }
29 }30 }
30 31
31-/// « Mercredi 6 août » / "Wednesday 6 August" / „Mittwoch, 6. August“32+/// « Mercredi 6 août » / "Wednesday 6 August" / „Mittwoch, 6. August“ /
33+/// « Quarta-feira, 6 de agosto »
32 pub fn day_full(lang: Lang, date: NaiveDate) -> String {34 pub fn day_full(lang: Lang, date: NaiveDate) -> String {
33 let weekday = lang.tr().weekdays[date.weekday().num_days_from_monday() as usize];35 let weekday = lang.tr().weekdays[date.weekday().num_days_from_monday() as usize];
34- let sep = if lang == Lang::De { ", " } else { " " };36+ let sep = if matches!(lang, Lang::De | Lang::PtBr) { ", " } else { " " };
35 capitalize(&format!("{weekday}{sep}{}", day_month(lang, date)))37 capitalize(&format!("{weekday}{sep}{}", day_month(lang, date)))
36 }38 }
37 39
@@ -175,12 +177,12 @@ impl Month {
175 }177 }
176 }178 }
177 179
178- /// « Mars 2026 »180+ /// « Mars 2026 » / « Agosto de 2026 »
179 pub fn label(&self, lang: Lang) -> String {181 pub fn label(&self, lang: Lang) -> String {
180- capitalize(&format!(182+ capitalize(&p2(
181- "{} {}",183+ lang.tr().month_year_pat,
182 lang.tr().months[(self.month - 1) as usize],184 lang.tr().months[(self.month - 1) as usize],
183- self.year185+ self.year,
184 ))186 ))
185 }187 }
186 }188 }
@@ -317,7 +319,13 @@ mod tests {
317 assert_eq!(day_month(Lang::Fr, d(2026, 8, 3)), "3 août");319 assert_eq!(day_month(Lang::Fr, d(2026, 8, 3)), "3 août");
318 assert_eq!(day_month(Lang::En, d(2026, 8, 3)), "3 August");320 assert_eq!(day_month(Lang::En, d(2026, 8, 3)), "3 August");
319 assert_eq!(day_month(Lang::De, d(2026, 8, 3)), "3. August");321 assert_eq!(day_month(Lang::De, d(2026, 8, 3)), "3. August");
322+ assert_eq!(day_month(Lang::PtBr, d(2026, 8, 3)), "3 de agosto");
320 assert_eq!(day_label(Lang::De, d(2026, 3, 5)), "Donnerstag, 5. März");323 assert_eq!(day_label(Lang::De, d(2026, 3, 5)), "Donnerstag, 5. März");
324+ assert_eq!(day_full(Lang::PtBr, d(2026, 3, 5)), "Quinta-feira, 5 de março");
325+ assert_eq!(
326+ Month::parse("2026-03").unwrap().label(Lang::PtBr),
327+ "Março de 2026"
328+ );
321 // day_full = day_label sans le suffixe « aujourd'hui ».329 // day_full = day_label sans le suffixe « aujourd'hui ».
322 assert_eq!(day_full(Lang::Fr, d(2026, 3, 5)), "Jeudi 5 mars");330 assert_eq!(day_full(Lang::Fr, d(2026, 3, 5)), "Jeudi 5 mars");
323 assert_eq!(day_full(Lang::En, d(2026, 3, 5)), "Thursday 5 March");331 assert_eq!(day_full(Lang::En, d(2026, 3, 5)), "Thursday 5 March");