mcp-cronos 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mcp_cronos/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ MCP Cronos - Server MCP per la gestione del diario di lavoro.
3
+
4
+ Fornisce tool per:
5
+ - Aggiungere entry al diario giornaliero
6
+ - Aggiungere contenuto a entry di progetto esistenti
7
+ - Leggere entry per data o range
8
+ - Generare riassunti discorsivi per lo standup
9
+ - Cercare testo nelle entry
10
+ - Generare riassunti settimanali
11
+ - Gestire bloccanti
12
+ - Elencare progetti menzionati
13
+ - Chiudere la giornata con riassunti e consolidamento
14
+ """
15
+
16
+ __version__ = "1.0.0"
mcp_cronos/config.py ADDED
@@ -0,0 +1,232 @@
1
+ """
2
+ Configuration for MCP Cronos.
3
+
4
+ Loads settings from a cronos.toml file (searched in priority order: explicit
5
+ CRONOS_CONFIG_PATH env var, diary root, ~/.config/cronos/) and merges them with
6
+ language-specific defaults from the i18n module.
7
+
8
+ The diary path is still provided by the original helper functions, which are kept
9
+ as-is because other modules import them directly.
10
+ """
11
+
12
+ import os
13
+ import sys
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any, Optional
17
+
18
+ if sys.version_info >= (3, 11):
19
+ import tomllib
20
+ else:
21
+ try:
22
+ import tomli as tomllib # type: ignore[no-redef]
23
+ except ImportError:
24
+ tomllib = None # type: ignore[assignment]
25
+
26
+ from mcp_cronos.i18n import get_language_pack
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Diary path helpers (unchanged — other modules depend on these)
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def get_diario_path() -> Path:
34
+ """
35
+ Return the diary root path from the CRONOS_DIARIO_PATH environment variable.
36
+
37
+ Returns:
38
+ Path to the diary working directory.
39
+
40
+ Raises:
41
+ RuntimeError: If CRONOS_DIARIO_PATH is not set.
42
+ """
43
+ path_str = os.environ.get("CRONOS_DIARIO_PATH")
44
+ if not path_str:
45
+ raise RuntimeError(
46
+ "Variabile d'ambiente CRONOS_DIARIO_PATH non impostata. "
47
+ "Imposta il path del diario di lavoro, es: "
48
+ "CRONOS_DIARIO_PATH=/path/to/Diario"
49
+ )
50
+ return Path(path_str)
51
+
52
+
53
+ def ensure_diario_exists() -> bool:
54
+ """
55
+ Check whether the diary directory exists.
56
+
57
+ Returns:
58
+ True if the directory exists, False otherwise.
59
+ """
60
+ return get_diario_path().exists()
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Config dataclass
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ @dataclass
69
+ class CronosConfig:
70
+ """Full application configuration, merged from TOML file and language defaults."""
71
+
72
+ lang: str
73
+ section_entries: str
74
+ section_blockers: str
75
+ section_day_summary: str
76
+ section_tech_summary: str
77
+ section_standup_message: str
78
+ blockers_default: str
79
+ title_format: str
80
+ git_enabled: bool
81
+ auto_push: bool
82
+ commit_message: str
83
+
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Singleton
87
+ # ---------------------------------------------------------------------------
88
+
89
+ _config: Optional[CronosConfig] = None
90
+
91
+
92
+ def _reset_config() -> None:
93
+ """
94
+ Clear the cached CronosConfig singleton.
95
+
96
+ Intended exclusively for tests that need to reload config between cases.
97
+ Should not be called in production code.
98
+ """
99
+ global _config
100
+ _config = None
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Internal helpers
105
+ # ---------------------------------------------------------------------------
106
+
107
+
108
+ def _find_config_file() -> Optional[Path]:
109
+ """
110
+ Search for a cronos.toml file in priority order.
111
+
112
+ Search order:
113
+ 1. CRONOS_CONFIG_PATH env var (explicit path, highest priority)
114
+ 2. {CRONOS_DIARIO_PATH}/cronos.toml
115
+ 3. ~/.config/cronos/cronos.toml
116
+
117
+ Returns the first existing Path found, or None if none exist.
118
+ """
119
+ explicit = os.environ.get("CRONOS_CONFIG_PATH")
120
+ if explicit:
121
+ p = Path(explicit)
122
+ if p.exists():
123
+ return p
124
+
125
+ # Diary root — only attempt if CRONOS_DIARIO_PATH is set (avoid RuntimeError)
126
+ diario_env = os.environ.get("CRONOS_DIARIO_PATH")
127
+ if diario_env:
128
+ candidate = Path(diario_env) / "cronos.toml"
129
+ if candidate.exists():
130
+ return candidate
131
+
132
+ # XDG-style user config
133
+ xdg = Path.home() / ".config" / "cronos" / "cronos.toml"
134
+ if xdg.exists():
135
+ return xdg
136
+
137
+ return None
138
+
139
+
140
+ def _parse_toml(path: Path) -> dict[str, Any]:
141
+ """
142
+ Parse a TOML file and return its contents as a dict.
143
+
144
+ Returns an empty dict on any error (missing file, invalid syntax, missing
145
+ tomllib/tomli library) so that callers can always fall back to defaults
146
+ without special-casing the error.
147
+ """
148
+ if tomllib is None:
149
+ return {}
150
+ try:
151
+ with open(path, "rb") as fh:
152
+ return tomllib.load(fh)
153
+ except Exception: # noqa: BLE001 — intentional catch-all for TOML parse errors
154
+ return {}
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Public loader
159
+ # ---------------------------------------------------------------------------
160
+
161
+
162
+ def load_config() -> CronosConfig:
163
+ """
164
+ Load, merge, and cache the application configuration.
165
+
166
+ Resolution order for each setting:
167
+ - Explicit user value in cronos.toml beats language default.
168
+ - Language defaults come from get_language_pack(lang).
169
+ - Hard-coded fallbacks apply when neither source provides a value.
170
+
171
+ The result is cached in the module-level _config singleton. Call
172
+ _reset_config() to force a fresh load (useful in tests).
173
+
174
+ Returns:
175
+ A fully populated CronosConfig instance.
176
+ """
177
+ global _config
178
+ if _config is not None:
179
+ return _config
180
+
181
+ # Parse the config file (empty dict if not found or unreadable)
182
+ config_path = _find_config_file()
183
+ raw: dict[str, Any] = _parse_toml(config_path) if config_path is not None else {}
184
+
185
+ cronos_section: dict[str, Any] = raw.get("cronos", {})
186
+
187
+ # Language
188
+ lang: str = cronos_section.get("lang", "it")
189
+ pack = get_language_pack(lang)
190
+
191
+ # Section names: user overrides > language defaults
192
+ user_sections: dict[str, Any] = cronos_section.get("sections", {})
193
+ section_entries = user_sections.get("entries", pack.sections["entries"])
194
+ section_blockers = user_sections.get("blockers", pack.sections["blockers"])
195
+ section_day_summary = user_sections.get("day_summary", pack.sections["day_summary"])
196
+ section_tech_summary = user_sections.get("tech_summary", pack.sections["tech_summary"])
197
+ section_standup_message = user_sections.get("standup_message", pack.sections["standup_message"])
198
+
199
+ # Diary settings
200
+ user_diary: dict[str, Any] = cronos_section.get("diary", {})
201
+ title_format: str = user_diary.get("title_format", f"{pack.title_prefix} - {{date}}")
202
+
203
+ # Git settings: user overrides > defaults.
204
+ # The [cronos] section may carry a top-level scalar `git = false/true` as a
205
+ # shorthand for git_enabled, or a full [cronos.git] sub-table. Both forms
206
+ # are supported; the sub-table form takes precedence when present.
207
+ raw_git = cronos_section.get("git", {})
208
+ if isinstance(raw_git, dict):
209
+ user_git: dict[str, Any] = raw_git
210
+ git_enabled_default: bool = True
211
+ else:
212
+ # Scalar shorthand: `git = false` under [cronos]
213
+ user_git = {}
214
+ git_enabled_default = bool(raw_git)
215
+ git_enabled: bool = bool(user_git.get("enabled", git_enabled_default))
216
+ auto_push: bool = bool(user_git.get("auto_push", True))
217
+ commit_message: str = user_git.get("commit_message", "diario: fine giornata {date}")
218
+
219
+ _config = CronosConfig(
220
+ lang=lang,
221
+ section_entries=section_entries,
222
+ section_blockers=section_blockers,
223
+ section_day_summary=section_day_summary,
224
+ section_tech_summary=section_tech_summary,
225
+ section_standup_message=section_standup_message,
226
+ blockers_default=pack.blockers_default,
227
+ title_format=title_format,
228
+ git_enabled=git_enabled,
229
+ auto_push=auto_push,
230
+ commit_message=commit_message,
231
+ )
232
+ return _config
@@ -0,0 +1,50 @@
1
+ ISTRUZIONI PER IL CONSOLIDAMENTO DEL DIARIO:
2
+
3
+ Hai ricevuto il contenuto completo del diario di oggi. Il file potrebbe contenere:
4
+ - Entry separate che trattano lo stesso argomento (es. analisi iniziale + approfondimento + verifica)
5
+ - Ripetizioni di dati e conclusioni tra entry diverse
6
+ - Informazioni sparse che andrebbero raggruppate
7
+ - Sezioni aggiunte in momenti diversi senza coerenza complessiva
8
+
9
+ Il tuo compito e' riscrivere il file consolidando tutto in modo coerente.
10
+
11
+ === REGOLE DI CONSOLIDAMENTO ===
12
+
13
+ 1. RAGGRUPPA PER PROGETTO E TEMA: entry diverse sullo stesso argomento vanno fuse in una
14
+ singola sezione. Ad esempio, "analisi ticket X", "approfondimento ticket X",
15
+ "verifica evidenze ticket X" diventano un'unica sezione "Ticket X" con la storia
16
+ completa dall'inizio alla fine.
17
+
18
+ 2. ELIMINA RIPETIZIONI: se lo stesso dato, conclusione o evidenza appare in piu' entry,
19
+ tienilo una sola volta nel punto piu' logico.
20
+
21
+ 3. MANTIENI TUTTI I DATI: non perdere informazioni. Se un'entry contiene un URL, un ID,
22
+ una query, un riferimento tecnico, deve restare nel file consolidato.
23
+
24
+ 4. ORDINE CRONOLOGICO E LOGICO: organizza le sezioni seguendo il flusso della giornata.
25
+ Dentro ogni sezione, racconta la storia dall'inizio alla fine, non in ordine di
26
+ quando le entry sono state scritte.
27
+
28
+ 5. FORMATO:
29
+ - Un H3 (###) per ogni progetto/tema principale
30
+ - Testo discorsivo, non elenchi puntati infiniti
31
+ - Sezioni "Dove verificare" con URL e query raggruppate alla fine della sezione
32
+ - Riferimenti (repository, branch, Jira, MR) alla fine della sezione
33
+ - Separatore --- tra sezioni di progetti diversi
34
+
35
+ 6. NON AGGIUNGERE CONTENUTO: non inventare, non interpretare, non aggiungere
36
+ conclusioni che non erano nel diario originale. Solo riorganizzare.
37
+
38
+ 7. PRESERVA LE SEZIONI DI CHIUSURA: se il diario ha gia' un "{section_day_summary}",
39
+ "{section_tech_summary}", "{section_standup_message}", lasciali invariati.
40
+ Se non li ha, non aggiungerli (per quelli c'e' il tool di fine giornata).
41
+
42
+ 8. SEZIONE {section_blockers}: mantienila sempre alla fine.
43
+
44
+ === PROCEDURA ===
45
+
46
+ 1. Leggi tutto il contenuto
47
+ 2. Identifica i temi/progetti trattati
48
+ 3. Per ogni tema, raccogli tutte le informazioni sparse nel file
49
+ 4. Riscrivi ogni tema come una sezione unica, coerente e completa
50
+ 5. Scrivi il file consolidato al path indicato nel campo `file`
@@ -0,0 +1,146 @@
1
+ ISTRUZIONI PER LA CHIUSURA DI FINE GIORNATA:
2
+
3
+ Hai ricevuto le entry grezze del diario di oggi. Devi produrre un file markdown
4
+ completo con CINQUE sezioni, poi scriverlo al path indicato in `file`.
5
+
6
+ === STRUTTURA DEL FILE DA SCRIVERE ===
7
+
8
+ ```
9
+ # {titolo_standup}
10
+
11
+ ## {section_day_summary}
12
+
13
+ {riassunto_giornata}
14
+
15
+ ## {section_tech_summary}
16
+
17
+ {riassunto_tecnico}
18
+
19
+ ---
20
+
21
+ ## {section_standup_message}
22
+
23
+ {messaggio_standup}
24
+
25
+ ---
26
+
27
+ ## {section_entries}
28
+
29
+ {entries_riscritte}
30
+
31
+ ---
32
+
33
+ ## {section_blockers}
34
+
35
+ {bloccanti}
36
+ ```
37
+
38
+ === SEZIONE 1: ENTRIES RISCRITTE ===
39
+
40
+ Riscrivi le entry in ordine cronologico e logico. Le entry originali potrebbero essere
41
+ state aggiunte a casaccio durante la giornata — il tuo compito è riordinarle e
42
+ ristrutturarle in un racconto coerente della giornata.
43
+
44
+ Formato:
45
+ - Un unico H3 per progetto: `### {Progetto} - {Descrizione generale della giornata su quel progetto}`
46
+ - Paragrafo introduttivo che riassume il lavoro complessivo sul progetto
47
+ - Sotto-sezioni H4 (`####`) per ogni fase/attività distinta, in ordine cronologico:
48
+ `#### Fase 1 — {Titolo fase}`
49
+ - Dentro ogni fase: descrizione dettagliata con bullet points, blocchi codice se utili,
50
+ nomi di file, commit, comandi, config — tutto ciò che serve a ricostruire cosa è stato fatto
51
+ - Sezione `**Riferimenti:**` alla fine dell'entry con repository, branch, Jira, MR
52
+ - Separatore `---` tra entry di progetti diversi
53
+
54
+ Livello di dettaglio: MASSIMO. Questo è il log tecnico completo della giornata.
55
+ Includi commit hash, nomi file, classi, configurazioni, comandi eseguiti, errori
56
+ incontrati e come sono stati risolti.
57
+
58
+ === SEZIONE 2: RIASSUNTO DELLA GIORNATA ===
59
+
60
+ Un paragrafo unico, denso e fluido che racconta l'intera giornata. Scritto come
61
+ se stessi raccontando a un collega tecnico cosa hai fatto, in modo scorrevole
62
+ ma completo.
63
+
64
+ Stile:
65
+ - Un solo paragrafo continuo (può essere lungo)
66
+ - Segui l'ordine cronologico della giornata
67
+ - Menziona i progetti, cosa è stato fatto e perché
68
+ - Includi problemi incontrati e come sono stati risolti
69
+ - Livello medio-alto: abbastanza tecnico da capire COSA è stato fatto,
70
+ senza entrare nel dettaglio di COME (niente nomi file, commit, config)
71
+ - Menziona ticket Jira e MR solo come riferimento generico ("ho creato i task Jira")
72
+ - Non usare elenchi puntati, solo prosa fluida
73
+
74
+ Esempio di tono (dal diario reale):
75
+ "Giornata interamente dedicata a Pollicino (RapsodiaTrace), proseguendo il lavoro
76
+ Keycloak del giorno precedente. La mattina è partita con un audit di tutti i README
77
+ del progetto per allinearli alle modifiche KC introdotte il giorno prima, seguito
78
+ dal merge di develop in master che era rimasto indietro di ~20 commit. Poi ho
79
+ affrontato l'analisi e implementazione di tre funzionalità KC avanzate..."
80
+
81
+ === SEZIONE 3: RIASSUNTO TECNICO ===
82
+
83
+ Un riassunto estremamente denso e tecnico. Scritto per uno sviluppatore che deve
84
+ capire esattamente cosa è stato fatto, con tutti i dettagli implementativi.
85
+
86
+ Stile:
87
+ - Uno o due paragrafi densi (non elenchi puntati)
88
+ - Includi: commit hash, nomi file, classi, funzioni, configurazioni specifiche,
89
+ versioni, comandi, flag, variabili d'ambiente, endpoint API
90
+ - Includi errori specifici incontrati (messaggi di errore, status code)
91
+ - Includi workaround e soluzioni tecniche precise
92
+ - Includi nomi di tool, librerie, framework con versioni
93
+ - Usa parentesi e trattini per compattare le informazioni
94
+ - Non spiegare il "perché" — solo il "cosa" e il "come"
95
+
96
+ Esempio di tono (dal diario reale):
97
+ "Audit e allineamento di 6 README (commit `08687a8`), merge develop in master
98
+ (`85920da`). Analisi e piano per 3 funzionalità KC: SMTP AWS SES con placeholder
99
+ `$(env:VAR)` risolti a runtime da config-cli (`IMPORT_VARSUBSTITUTION_ENABLED=true`),
100
+ flow custom `browser-dashboard-mfa` con doppio nesting..."
101
+
102
+ === SEZIONE 4: MESSAGGIO PER LO STANDUP ===
103
+
104
+ Messaggio discorsivo che può essere usato sia per lo standup che inviato
105
+ direttamente su Slack a un collega. Deve sembrare scritto da una persona,
106
+ non da un'AI. Seguire queste regole:
107
+
108
+ - Scritto in prima persona, tono naturale e colloquiale
109
+ - Continuità discorsiva assoluta: un flusso di frasi che scorrono l'una nell'altra,
110
+ MAI elenchi puntati, MAI strutture rigide con grassetto per progetto
111
+ - Alto livello — racconta cosa hai fatto e perché, non come
112
+ - Niente dettagli implementativi (niente nomi file, classi, funzioni, commit, MR, Jira)
113
+ - Niente strumenti interni (MCP, tool CLI, script, automazioni)
114
+ - Dettagli tecnici SOLO se servono a far capire il contesto o sono
115
+ interessanti per decisioni future
116
+ - Niente convenevoli, niente firme, niente saluti finali
117
+ - Se ci sono più progetti, collegali con transizioni naturali
118
+ ("Finito quello...", "Nel pomeriggio...", "Sul fronte supporto...")
119
+ - Se ci sono bloccanti, menzionali alla fine in modo naturale
120
+ - Menziona le persone coinvolte quando rilevante (chi ha chiesto, chi lavora in parallelo)
121
+ - ATTENZIONE MASSIMA ad accenti e spaziature: usare sempre gli accenti
122
+ corretti (è, à, ò, ù, perché, cioè, può, già, più, ecc.), MAI apostrofi
123
+ al posto degli accenti (e' NO, è SÌ). Niente spazi mancanti o doppi,
124
+ punteggiatura italiana corretta. Rileggere il testo prima di produrlo.
125
+
126
+ Esempio di tono (messaggio reale inviato su Slack):
127
+ "Ieri ho lavorato tutto il giorno su IoPollicino. La mattina ho chiuso la feature
128
+ del codice referral facoltativo, mettendo il backend su stage presto così Matteo
129
+ poteva procedere in parallelo, e nel pomeriggio ho completato la parte dashboard
130
+ con le nuove metriche, i filtri per tipo utente e un warning che avvisa che siccome
131
+ gli utenti con referral code non compilano il questionario sull'app mentre gli altri
132
+ sì, le statistiche potrebbero essere sbilanciate verso gli utenti non-referral. La
133
+ situazione si normalizzerà quando verranno importati i dati dei questionari degli
134
+ utenti referral, ma nel frattempo il warning avvisa di leggere i numeri con cautela.
135
+ Finito quello, ho iniziato la nuova lavorazione sulle metriche della landing page.
136
+ Riccardo mi ha informato su quali statistiche servono — mezzo prevalente,
137
+ distribuzione modalità, motivo prevalente e tipo di mobilità attiva/motorizzata,
138
+ entro stamattina dovrei terminare."
139
+
140
+ === PROCEDURA ===
141
+
142
+ 1. Leggi attentamente tutte le entry grezze
143
+ 2. Identifica l'ordine cronologico e i raggruppamenti logici
144
+ 3. Genera le cinque sezioni (entries riscritte, riassunto giornata, riassunto tecnico, messaggio standup, bloccanti)
145
+ 4. Assembla il file markdown completo seguendo la struttura indicata sopra
146
+ 5. Chiama cronos_scrivi_fine_giornata con il contenuto generato per scrivere il file
@@ -0,0 +1,45 @@
1
+ ISTRUZIONI PER LA GENERAZIONE DEL RIASSUNTO:
2
+
3
+ Genera un messaggio discorsivo per lo standup o da inviare su Slack.
4
+ Deve sembrare scritto da una persona, non da un'AI.
5
+
6
+ REGOLE:
7
+ - Scritto in prima persona, tono naturale e colloquiale
8
+ - Continuità discorsiva assoluta: un flusso di frasi che scorrono l'una nell'altra,
9
+ MAI elenchi puntati, MAI strutture rigide con grassetto per progetto
10
+ - Alto livello — racconta cosa hai fatto e perché, non come
11
+ - Niente dettagli implementativi (niente nomi file, classi, funzioni, MR, Jira)
12
+ - Niente strumenti interni (MCP, tool CLI, script, automazioni)
13
+ - Dettagli tecnici solo se servono a far capire il contesto o sono interessanti
14
+ per decisioni future
15
+ - Se ci sono più progetti, collegali con transizioni naturali
16
+ ("Finito quello...", "Nel pomeriggio...", "Sul fronte supporto...")
17
+ - Menziona le persone coinvolte quando rilevante
18
+ - Niente convenevoli, firme, saluti finali
19
+ - Se ci sono bloccanti, menzionali alla fine in modo naturale
20
+ - ATTENZIONE MASSIMA ad accenti e spaziature: usare sempre gli accenti
21
+ corretti (è, à, ò, ù, perché, cioè, può, già, più, ecc.), MAI apostrofi
22
+ al posto degli accenti (e' NO, è SI). Niente spazi mancanti o doppi,
23
+ punteggiatura italiana corretta. Rileggere il testo prima di produrlo.
24
+
25
+ ESEMPIO DI TONO (messaggio reale inviato su Slack):
26
+ "Ieri ho lavorato tutto il giorno su IoPollicino. La mattina ho chiuso la feature
27
+ del codice referral facoltativo, mettendo il backend su stage presto così Matteo
28
+ poteva procedere in parallelo, e nel pomeriggio ho completato la parte dashboard
29
+ con le nuove metriche, i filtri per tipo utente e un warning che avvisa che siccome
30
+ gli utenti con referral code non compilano il questionario sull'app mentre gli altri
31
+ sì, le statistiche potrebbero essere sbilanciate verso gli utenti non-referral. La
32
+ situazione si normalizzerà quando verranno importati i dati dei questionari degli
33
+ utenti referral, ma nel frattempo il warning avvisa di leggere i numeri con cautela.
34
+ Finito quello, ho iniziato la nuova lavorazione sulle metriche della landing page.
35
+ Riccardo mi ha informato su quali statistiche servono — mezzo prevalente,
36
+ distribuzione modalità, motivo prevalente e tipo di mobilità attiva/motorizzata,
37
+ entro stamattina dovrei terminare."
38
+
39
+ COSA EVITARE:
40
+ - Elenchi puntati (MAI)
41
+ - Strutture con **Progetto** in grassetto seguite da descrizione
42
+ - Dettagli di implementazione
43
+ - Linguaggio burocratico
44
+ - Convenevoli e formule di cortesia
45
+ - Riferimenti a strumenti interni o automazioni
mcp_cronos/i18n.py ADDED
@@ -0,0 +1,153 @@
1
+ """
2
+ Internationalisation support for mcp-cronos.
3
+
4
+ Provides a frozen LanguagePack dataclass that bundles all locale-specific
5
+ strings used when rendering diary entries and standup messages. Two packs are
6
+ shipped — Italian ("it", the application default) and English ("en").
7
+
8
+ Design notes
9
+ ------------
10
+ - LanguagePack is frozen so it can be used as a dict key or cached safely.
11
+ - list fields (months, weekdays) use a plain list rather than a tuple so
12
+ callers can index them with the 0-based int values returned by datetime.
13
+ - format_date/format_title are thin convenience methods; heavy formatting
14
+ logic lives in the calling modules to keep this file declarative.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from datetime import date
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class LanguagePack:
25
+ """Locale-specific strings and formatting rules for a single language."""
26
+
27
+ code: str
28
+ months: list[str] # 12 month names, index 0 = January
29
+ weekdays: list[str] # 7 weekday names, index 0 = Monday
30
+ title_prefix: str # e.g. "Per lo Stand-up"
31
+ date_format: str # template with {day}, {month}, {year} placeholders
32
+ sections: dict[str, str] # UI section labels; required keys documented below
33
+ blockers_default: str # default text when no blockers are present
34
+ temporal: dict[str, str] # relative-time expressions; required keys below
35
+
36
+ # sections keys: entries, blockers, day_summary, tech_summary, standup_message
37
+ # temporal keys: yesterday, day_before, last_weekday, from_to
38
+
39
+ def format_date(self, d: date) -> str:
40
+ """Return a human-readable date string according to this language's date_format.
41
+
42
+ Uses 1-based day, a localised month name, and the four-digit year.
43
+ The month index is derived from d.month (1–12) mapped to months[0–11].
44
+ """
45
+ return self.date_format.format(
46
+ day=d.day,
47
+ month=self.months[d.month - 1],
48
+ year=d.year,
49
+ )
50
+
51
+ def format_title(self, standup_date: date) -> str:
52
+ """Return the full standup title for the given date.
53
+
54
+ Combines title_prefix with the formatted date, separated by " - ".
55
+ """
56
+ return f"{self.title_prefix} - {self.format_date(standup_date)}"
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Italian language pack (application default)
61
+ # ---------------------------------------------------------------------------
62
+
63
+ _IT = LanguagePack(
64
+ code="it",
65
+ months=[
66
+ "Gennaio",
67
+ "Febbraio",
68
+ "Marzo",
69
+ "Aprile",
70
+ "Maggio",
71
+ "Giugno",
72
+ "Luglio",
73
+ "Agosto",
74
+ "Settembre",
75
+ "Ottobre",
76
+ "Novembre",
77
+ "Dicembre",
78
+ ],
79
+ weekdays=["lunedi", "martedi", "mercoledi", "giovedi", "venerdi", "sabato", "domenica"],
80
+ title_prefix="Per lo Stand-up",
81
+ date_format="{day} {month} {year}",
82
+ sections={
83
+ "entries": "Cosa ho fatto ieri",
84
+ "blockers": "Bloccanti",
85
+ "day_summary": "Riassunto della giornata",
86
+ "tech_summary": "Riassunto tecnico",
87
+ "standup_message": "Messaggio per lo standup",
88
+ },
89
+ blockers_default="Nessuno",
90
+ temporal={
91
+ "yesterday": "Ieri",
92
+ "day_before": "L'altro ieri",
93
+ "last_weekday": "{weekday} scorso",
94
+ "from_to": "Dal {start} al {end}",
95
+ },
96
+ )
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # English language pack
100
+ # ---------------------------------------------------------------------------
101
+
102
+ _EN = LanguagePack(
103
+ code="en",
104
+ months=[
105
+ "January",
106
+ "February",
107
+ "March",
108
+ "April",
109
+ "May",
110
+ "June",
111
+ "July",
112
+ "August",
113
+ "September",
114
+ "October",
115
+ "November",
116
+ "December",
117
+ ],
118
+ weekdays=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
119
+ title_prefix="For Stand-up",
120
+ date_format="{month} {day}, {year}",
121
+ sections={
122
+ "entries": "What I did yesterday",
123
+ "blockers": "Blockers",
124
+ "day_summary": "Daily summary",
125
+ "tech_summary": "Technical summary",
126
+ "standup_message": "Standup message",
127
+ },
128
+ blockers_default="None",
129
+ temporal={
130
+ "yesterday": "Yesterday",
131
+ "day_before": "Day before yesterday",
132
+ "last_weekday": "Last {weekday}",
133
+ "from_to": "From {start} to {end}",
134
+ },
135
+ )
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Public registry
139
+ # ---------------------------------------------------------------------------
140
+
141
+ LANGUAGES: dict[str, LanguagePack] = {
142
+ "it": _IT,
143
+ "en": _EN,
144
+ }
145
+
146
+
147
+ def get_language_pack(lang: str) -> LanguagePack:
148
+ """Return the LanguagePack for the given language code.
149
+
150
+ Falls back to the Italian pack for any unrecognised or empty code, because
151
+ Italian is the application default language.
152
+ """
153
+ return LANGUAGES.get(lang, _IT)