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.8a6909f parent: ce3c0cd modified
README.md +51 -22 | @@ -4,8 +4,9 @@ | ||
| 4 | 4 | *ex-ego* (le moyen : on dépose l'ego et on mesure de bonne foi). |
| 5 | 5 | |
| 6 | 6 | 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. | |
| 9 | 10 | |
| 10 | 11 | Stack : Rust (axum, askama, sqlx), PostgreSQL, biscuit-auth. Aucun framework |
| 11 | 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 | 59 | ### English |
| 59 | 60 | |
| 60 | 61 | Team time tracker and shared todo list. |
| 62 | + | |
| 61 | 63 | The core idea is comparing *time spent* on household tasks |
| 62 | 64 | (the fairest measure of domestic work, as long as everyone acts in good |
| 63 | 65 | faith). Log time per category, run a timer, take dated tasks and |
| 64 | 66 | 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 | |
| 67 | 71 | 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 | |
| 69 | 75 | 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). | |
| 75 | 79 | |
| 76 | 80 | ### Deustch |
| 77 | 81 | |
| 78 | 82 | Zeiterfassung und geteilte Aufgabenliste fürs Team. |
| 83 | + | |
| 79 | 84 | Kern der Idee ist der Vergleich der *aufgewendeten Zeit* für Hausarbeit (das fairste Maß für |
| 80 | 85 | häusliche Arbeit, solange alle guten Willens sind). Zeiten pro Kategorie |
| 81 | 86 | 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 | |
| 85 | 91 | 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. | |
| 89 | 96 | 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 | |
| 91 | 98 | 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). | |
| 94 | 123 | |
| 95 | 124 | ## Fonctionnalités |
| 96 | 125 | |
| @@ -114,8 +143,8 @@ Schwarz-Weiß-Thema (ebenfalls pro Benutzer, inklusive Diagramme). Rust | ||
| 114 | 143 | - **Contraste élevé** : un thème noir et blanc, réglage **par utilisateur** |
| 115 | 144 | (Réglages → Affichage), pour les écrans très sombres ou les filtres de |
| 116 | 145 | 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 | |
| 119 | 148 | modifiable par la variable d'environnement `DEFAULT_LANG`. |
| 120 | 149 | |
| 121 | 150 | ## Captures d'écran |
| @@ -155,7 +184,7 @@ Variables d'environnement du serveur : | ||
| 155 | 184 | | `POSTGRESQL_ADDON_URI` ou `DATABASE_URL` | connexion PostgreSQL (TLS imposé côté client quoi que dise l'URL) | |
| 156 | 185 | | `BISCUIT_PRIVATE_KEY` | clé privée racine (à générer une fois avec `exego keygen`) | |
| 157 | 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 | 189 | ### Développement local |
| 161 | 190 | |
| @@ -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, en | 6 | 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 | 7 | +é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 framework | 11 | 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 | ### English | 59 | ### 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 tasks | 63 | The core idea is comparing *time spent* on household tasks |
| 62 | (the fairest measure of domestic work, as long as everyone acts in good | 64 | (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 and | 65 | faith). Log time per category, run a timer, take dated tasks and |
| 64 | reschedule them at their observed rhythm, and read stacked-bar statistics | 66 | reschedule them at their observed rhythm, and read stacked-bar statistics |
| 65 | -per member. Having *data* about time spent on housework changes the nature | 67 | +per member. |
| 66 | -of the conversation: instead of debating feelings and recollections, you | 68 | + |
| 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 talk | 71 | 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 | ||
| 69 | from the command line (no e-mail, no sign-up); a demo account can be | 75 | from the command line (no e-mail, no sign-up); a demo account can be |
| 70 | -shared on request. Mobile-first | 76 | +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 optional | 78 | +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 | ### Deustch | 80 | ### 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ür | 84 | 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 Kategorie | 85 | häusliche Arbeit, solange alle guten Willens sind). Zeiten pro Kategorie |
| 81 | erfassen, Timer laufen lassen, datierte Aufgaben übernehmen und im | 86 | 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: Statt | 88 | + |
| 84 | -über Gefühle und Erinnerungen zu streiten, schaut man gemeinsam auf in | 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 | ||
| 85 | gutem Glauben erfasste Zahlen (eine ruhigere, sachlichere Art, über die | 91 | gutem Glauben erfasste Zahlen (eine ruhigere, sachlichere Art, über die |
| 86 | -Aufgabenverteilung zu sprechen). Vorerst kein SaaS: Konten werden manuell | 92 | +Aufgabenverteilung zu sprechen). |
| 87 | -über die Kommandozeile angelegt (keine E-Mail, keine Registrierung); ein | 93 | + |
| 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 als | 96 | 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 |
| 91 | Einstellung pro Benutzer), mit optionalem kontrastreichem | 98 | Einstellung pro Benutzer), mit optionalem kontrastreichem |
| 92 | -Schwarz-Weiß-Thema (ebenfalls pro Benutzer, inklusive Diagramme). Rust | 99 | +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és | 124 | ## 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 de | 144 | (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églage | 146 | +- **Quadrilingue** : français, anglais, allemand, portugais du Brésil. La |
| 118 | - **par utilisateur** (page Réglages). Langue par défaut à la création | 147 | + 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'écran | 150 | ## 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 local | 189 | ### 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 | |
| 2 | 2 | //! (`users.lang`) — jamais par navigateur. Défaut à la création d'un compte : |
| 3 | 3 | //! `DEFAULT_LANG` (fr sinon). Les pages anonymes (login, erreurs) utilisent |
| 4 | 4 | //! la langue par défaut. |
| @@ -12,6 +12,7 @@ pub enum Lang { | ||
| 12 | 12 | Fr, |
| 13 | 13 | En, |
| 14 | 14 | De, |
| 15 | + PtBr, | |
| 15 | 16 | } |
| 16 | 17 | |
| 17 | 18 | impl Lang { |
| @@ -20,6 +21,7 @@ impl Lang { | ||
| 20 | 21 | "fr" => Some(Lang::Fr), |
| 21 | 22 | "en" => Some(Lang::En), |
| 22 | 23 | "de" => Some(Lang::De), |
| 24 | + "pt-br" => Some(Lang::PtBr), | |
| 23 | 25 | _ => None, |
| 24 | 26 | } |
| 25 | 27 | } |
| @@ -29,6 +31,7 @@ impl Lang { | ||
| 29 | 31 | Lang::Fr => "fr", |
| 30 | 32 | Lang::En => "en", |
| 31 | 33 | Lang::De => "de", |
| 34 | + Lang::PtBr => "pt-br", | |
| 32 | 35 | } |
| 33 | 36 | } |
| 34 | 37 | |
| @@ -37,6 +40,7 @@ impl Lang { | ||
| 37 | 40 | Lang::Fr => &FR, |
| 38 | 41 | Lang::En => &EN, |
| 39 | 42 | Lang::De => &DE, |
| 43 | + Lang::PtBr => &PT_BR, | |
| 40 | 44 | } |
| 41 | 45 | } |
| 42 | 46 | |
| @@ -46,10 +50,11 @@ impl Lang { | ||
| 46 | 50 | Lang::Fr => "Français", |
| 47 | 51 | Lang::En => "English", |
| 48 | 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 | 60 | /// Interpolation à un trou (motifs `{}` des champs `*_pat`). |
| @@ -311,6 +316,8 @@ pub struct Tr { | ||
| 311 | 316 | pub months: [&'static str; 12], |
| 312 | 317 | pub today_suffix: &'static str, |
| 313 | 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 | 321 | pub year_pat: &'static str, |
| 315 | 322 | } |
| 316 | 323 | |
| @@ -399,10 +406,10 @@ pub static FR: Tr = Tr { | ||
| 399 | 406 | toast_timer_cancelled: "Minuteur annulé", |
| 400 | 407 | title_stats: "Statistiques", |
| 401 | 408 | 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", | |
| 406 | 413 | all_time: "Depuis le début", |
| 407 | 414 | no_time_period: "Aucun temps enregistré sur cette période.", |
| 408 | 415 | all_history: "Tout l'historique", |
| @@ -573,6 +580,7 @@ pub static FR: Tr = Tr { | ||
| 573 | 580 | ], |
| 574 | 581 | today_suffix: " — aujourd'hui", |
| 575 | 582 | week_range_pat: "Semaine du {} au {}", |
| 583 | + month_year_pat: "{} {}", | |
| 576 | 584 | year_pat: "Année {}", |
| 577 | 585 | }; |
| 578 | 586 | |
| @@ -633,10 +641,10 @@ pub static EN: Tr = Tr { | ||
| 633 | 641 | toast_timer_cancelled: "Timer cancelled", |
| 634 | 642 | title_stats: "Statistics", |
| 635 | 643 | 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", | |
| 640 | 648 | all_time: "All time", |
| 641 | 649 | no_time_period: "No time recorded in this period.", |
| 642 | 650 | all_history: "Entire history", |
| @@ -807,6 +815,7 @@ pub static EN: Tr = Tr { | ||
| 807 | 815 | ], |
| 808 | 816 | today_suffix: " — today", |
| 809 | 817 | week_range_pat: "Week of {} to {}", |
| 818 | + month_year_pat: "{} {}", | |
| 810 | 819 | year_pat: "Year {}", |
| 811 | 820 | }; |
| 812 | 821 | |
| @@ -867,10 +876,10 @@ pub static DE: Tr = Tr { | ||
| 867 | 876 | toast_timer_cancelled: "Timer abgebrochen", |
| 868 | 877 | title_stats: "Statistik", |
| 869 | 878 | 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", | |
| 874 | 883 | all_time: "Seit Beginn", |
| 875 | 884 | no_time_period: "In diesem Zeitraum wurde keine Zeit erfasst.", |
| 876 | 885 | all_history: "Gesamter Verlauf", |
| @@ -1041,9 +1050,249 @@ pub static DE: Tr = Tr { | ||
| 1041 | 1050 | ], |
| 1042 | 1051 | today_suffix: " — heute", |
| 1043 | 1052 | week_range_pat: "Woche vom {} bis {}", |
| 1053 | + month_year_pat: "{} {}", | |
| 1044 | 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 | 1296 | #[cfg(test)] |
| 1048 | 1297 | mod tests { |
| 1049 | 1298 | use super::*; |
| @@ -1064,5 +1313,8 @@ mod tests { | ||
| 1064 | 1313 | assert_eq!(Lang::from_code(l.code()), Some(l)); |
| 1065 | 1314 | } |
| 1066 | 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 | } |
| @@ -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 |
| 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) utilisent | 3 | //! `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 { | ||
| 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 | 23 | pub fn day_month(lang: Lang, date: NaiveDate) -> String { |
| 24 | 24 | let month = lang.tr().months[(date.month() - 1) as usize]; |
| 25 | 25 | match lang { |
| 26 | 26 | Lang::De => format!("{}. {}", date.day(), month), |
| 27 | + Lang::PtBr => format!("{} de {}", date.day(), month), | |
| 27 | 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 | 34 | pub fn day_full(lang: Lang, date: NaiveDate) -> String { |
| 33 | 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 | 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 | 181 | pub fn label(&self, lang: Lang) -> String { |
| 180 | - capitalize(&format!( | |
| 181 | - "{} {}", | |
| 182 | + capitalize(&p2( | |
| 183 | + lang.tr().month_year_pat, | |
| 182 | 184 | lang.tr().months[(self.month - 1) as usize], |
| 183 | - self.year | |
| 185 | + self.year, | |
| 184 | 186 | )) |
| 185 | 187 | } |
| 186 | 188 | } |
| @@ -317,7 +319,13 @@ mod tests { | ||
| 317 | 319 | assert_eq!(day_month(Lang::Fr, d(2026, 8, 3)), "3 août"); |
| 318 | 320 | assert_eq!(day_month(Lang::En, d(2026, 8, 3)), "3 August"); |
| 319 | 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 | 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 | 329 | // day_full = day_label sans le suffixe « aujourd'hui ». |
| 322 | 330 | assert_eq!(day_full(Lang::Fr, d(2026, 3, 5)), "Jeudi 5 mars"); |
| 323 | 331 | 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.year | 185 | + 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"); |