chore(docs): consolidate documentation, drop dead files
Cleanup - Remove src/backend/Dockerfile.legacy and src/frontend/flask_app/Dockerfile.legacy (history is in git, build uses the new uv-based root Dockerfile / Dockerfile.frontend). - Remove src/frontend/flask_app/verify_i18n.py (had hardcoded paths pointing at the old client/ tree). Group docs/ - New docs/README.md indexes everything in one place. - New docs/architecture/STATO_PROGETTO.md: snapshot of what works in V2.0.0 (inherited V1.0.7 features, rev04 Phase 1 stations, worker scaling, src/ restructure, test status, stack, decisions). - New docs/architecture/ROADMAP.md: what's next — Phases 2-7 of the rev04 migration with status, open client decisions (D-0.1 through D-0.10), tech debt and time estimates for M1 / M2. - Move PIANO_IMPLEMENTAZIONE.md (90KB V1.0.0 plan) to docs/archive/2026-02-06-piano-implementazione-v1.md (historical). - Move Schema sviluppo SW TieFlow_rev04-2026.docx to docs/specs/ with ISO date filename so the customer spec is now tracked. - Move src/frontend/flask_app/I18N_SETUP.md to docs/I18N_SETUP.md and rewrite paths to the new src/frontend/flask_app/ tree. .dockerignore: simplified now that legacy Dockerfiles are gone; docs/ stays excluded from the build context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Installa dipendenze sistema per WeasyPrint
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libcairo2 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libffi-dev \
|
||||
shared-mime-info \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Crea directory uploads
|
||||
RUN mkdir -p uploads/images uploads/pdfs uploads/logos uploads/reports
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Entry point: Alembic upgrade + Uvicorn
|
||||
CMD ["sh", "-c", "alembic -c migrations/alembic.ini upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 --proxy-headers --forwarded-allow-ips='*'"]
|
||||
@@ -1,33 +0,0 @@
|
||||
FROM python:3.11-slim AS base
|
||||
|
||||
# Installa Node.js per Tailwind CSS build
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt gunicorn
|
||||
|
||||
COPY . .
|
||||
|
||||
# Install and build Tailwind CSS
|
||||
RUN npm install tailwindcss@3 && npx tailwindcss -i static/css/input.css -o static/css/tailwind.css --minify
|
||||
|
||||
# Compile Flask-Babel translations
|
||||
RUN pybabel compile -d translations
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["gunicorn", \
|
||||
"--workers", "5", \
|
||||
"--threads", "4", \
|
||||
"--worker-class", "gthread", \
|
||||
"--timeout", "60", \
|
||||
"--bind", "0.0.0.0:5000", \
|
||||
"--access-logfile", "-", \
|
||||
"--forwarded-allow-ips", "*", \
|
||||
"app:create_app()"]
|
||||
@@ -1,240 +0,0 @@
|
||||
# TieMeasureFlow i18n Setup Guide
|
||||
|
||||
## Overview
|
||||
|
||||
TieMeasureFlow supports complete internationalization (i18n) with:
|
||||
- **Server-side**: Flask-Babel for Python/Jinja2 templates
|
||||
- **Client-side**: alpinejs-i18n for JavaScript/Alpine.js
|
||||
- **Supported languages**: Italian (IT - default) and English (EN)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
client/
|
||||
├── translations/ # Flask-Babel translations
|
||||
│ ├── babel.cfg # Extraction config
|
||||
│ ├── it/LC_MESSAGES/
|
||||
│ │ ├── messages.po # Italian strings
|
||||
│ │ └── messages.mo # Compiled binary (generated)
|
||||
│ └── en/LC_MESSAGES/
|
||||
│ ├── messages.po # English translations
|
||||
│ └── messages.mo # Compiled binary (generated)
|
||||
├── static/js/locales/ # Alpine.js i18n
|
||||
│ ├── it.json # Italian client-side strings
|
||||
│ └── en.json # English client-side strings
|
||||
└── compile_translations.py # Utility to compile .po → .mo
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Language Selection
|
||||
|
||||
1. **User session**: Language stored in `session["language"]` (persistent)
|
||||
2. **Browser fallback**: Uses `Accept-Language` header if no session
|
||||
3. **Default**: Italian (IT)
|
||||
|
||||
### Server-Side (Flask-Babel)
|
||||
|
||||
**In Python code:**
|
||||
```python
|
||||
from flask_babel import gettext as _
|
||||
|
||||
flash(_("Profilo aggiornato con successo"))
|
||||
```
|
||||
|
||||
**In Jinja2 templates:**
|
||||
```jinja2
|
||||
<h1>{{ _('Accedi al sistema') }}</h1>
|
||||
|
||||
{# With variables #}
|
||||
<p>{{ _('Benvenuto, %(name)s!', name=user.display_name) }}</p>
|
||||
```
|
||||
|
||||
### Client-Side (Alpine.js i18n)
|
||||
|
||||
**Load i18n in base template:**
|
||||
```html
|
||||
<script src="/static/js/lib/alpinejs-i18n.min.js"></script>
|
||||
<script>
|
||||
// Initialize i18n with current language
|
||||
Alpine.plugin(AlpineI18n);
|
||||
Alpine.store('i18n', {
|
||||
locale: '{{ current_language }}',
|
||||
fallbackLocale: 'it',
|
||||
messages: {}
|
||||
});
|
||||
|
||||
// Load locale files
|
||||
fetch('/static/js/locales/{{ current_language }}.json')
|
||||
.then(r => r.json())
|
||||
.then(data => Alpine.store('i18n').messages['{{ current_language }}'] = data);
|
||||
</script>
|
||||
```
|
||||
|
||||
**Use in templates:**
|
||||
```html
|
||||
<button @click="submit" x-text="$t('numpad.submit')"></button>
|
||||
|
||||
<p x-text="$t('auth.welcome', {name: userName})"></p>
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Add New Strings
|
||||
|
||||
**Python/Templates:**
|
||||
1. Wrap strings with `_()` or `{{ _() }}`
|
||||
2. Extract strings: `pybabel extract -F translations/babel.cfg -o translations/messages.pot .`
|
||||
3. Update catalogs: `pybabel update -i translations/messages.pot -d translations`
|
||||
4. Edit `translations/*/LC_MESSAGES/messages.po` files
|
||||
5. Compile: `python compile_translations.py`
|
||||
|
||||
**JavaScript/Alpine:**
|
||||
1. Add keys to `static/js/locales/it.json` (Italian strings)
|
||||
2. Add translations to `static/js/locales/en.json`
|
||||
3. No compilation needed (loaded at runtime)
|
||||
|
||||
### 2. Compile Translations
|
||||
|
||||
After editing .po files:
|
||||
|
||||
```bash
|
||||
cd client
|
||||
python compile_translations.py
|
||||
```
|
||||
|
||||
This generates `.mo` binary files that Flask-Babel uses at runtime.
|
||||
|
||||
### 3. Language Switching
|
||||
|
||||
**Endpoint**: `GET /set-language/<lang>`
|
||||
|
||||
**Example**:
|
||||
```html
|
||||
<a href="{{ url_for('set_language', lang='en') }}">English</a>
|
||||
<a href="{{ url_for('set_language', lang='it') }}">Italiano</a>
|
||||
```
|
||||
|
||||
**With Alpine.js**:
|
||||
```html
|
||||
<button @click="$store.i18n.locale = 'en'; window.location.href='/set-language/en'">
|
||||
English
|
||||
</button>
|
||||
```
|
||||
|
||||
## Translation Keys
|
||||
|
||||
### Server-Side (messages.po)
|
||||
|
||||
56 strings covering:
|
||||
- **Login**: "Accedi al sistema", "Credenziali non valide", etc.
|
||||
- **Navbar**: "Misure", "Ricette", "Statistiche", etc.
|
||||
- **Profile**: "Profilo Utente", "Salva Modifiche", etc.
|
||||
- **Flash messages**: "Benvenuto, %(name)s!", "Logout effettuato", etc.
|
||||
- **Common UI**: "Caricamento...", "Salva", "Annulla", "Conferma", etc.
|
||||
- **Measurements**: "Conforme", "Non Conforme", etc.
|
||||
|
||||
### Client-Side (locales/*.json)
|
||||
|
||||
Structured keys:
|
||||
```json
|
||||
{
|
||||
"numpad": { "title", "clear", "submit", "decimal", "close" },
|
||||
"measurement": { "pass", "warning", "fail", "value", "next" },
|
||||
"common": { "loading", "save", "cancel", "confirm", "delete", "edit", ... },
|
||||
"theme": { "light", "dark", "toggle" },
|
||||
"language": { "switch", "it", "en" },
|
||||
"auth": { "login", "username", "password", "signIn", ... },
|
||||
"nav": { "measurements", "recipes", "statistics", ... },
|
||||
"profile": { "title", "displayName", "language", "theme", ... }
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
**config.py** must define:
|
||||
```python
|
||||
BABEL_DEFAULT_LOCALE = 'it'
|
||||
BABEL_TRANSLATION_DIRECTORIES = 'translations'
|
||||
LANGUAGES = {
|
||||
'it': 'Italiano',
|
||||
'en': 'English'
|
||||
}
|
||||
```
|
||||
|
||||
**app.py** provides:
|
||||
- `get_locale()`: Language selector function
|
||||
- `/set-language/<lang>`: Language switch endpoint
|
||||
- `inject_globals()`: Exposes `current_language` and `languages` to templates
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use Italian as msgid**: Italian strings are the canonical keys
|
||||
2. **Keep client/server keys synced**: Use similar naming across .po and .json
|
||||
3. **Test both languages**: Switch languages and verify all pages
|
||||
4. **Recompile after editing**: Run `compile_translations.py` after changing .po files
|
||||
5. **Use variables for dynamic content**: `_('Welcome, %(name)s!', name=user)` or `$t('auth.welcome', {name})`
|
||||
6. **Avoid hardcoded strings**: Always use translation functions
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Translations not appearing:**
|
||||
- Verify `.mo` files exist: `find translations -name "*.mo"`
|
||||
- Check session language: `session["language"]` or browser console
|
||||
- Recompile: `python compile_translations.py`
|
||||
- Restart Flask server
|
||||
|
||||
**Client-side i18n not working:**
|
||||
- Check browser console for fetch errors
|
||||
- Verify JSON files exist: `static/js/locales/{it,en}.json`
|
||||
- Check Alpine.js i18n plugin is loaded
|
||||
- Verify `Alpine.store('i18n')` is initialized
|
||||
|
||||
**Missing translations show as keys:**
|
||||
- Add missing keys to .po or .json files
|
||||
- Recompile .po files if server-side
|
||||
- Reload page if client-side
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Extract translatable strings from code
|
||||
pybabel extract -F translations/babel.cfg -o translations/messages.pot .
|
||||
|
||||
# Initialize new language (first time)
|
||||
pybabel init -i translations/messages.pot -d translations -l en
|
||||
|
||||
# Update existing catalogs with new strings
|
||||
pybabel update -i translations/messages.pot -d translations
|
||||
|
||||
# Compile .po to .mo (required after editing)
|
||||
python compile_translations.py
|
||||
|
||||
# Check translation statistics
|
||||
msgfmt --statistics translations/it/LC_MESSAGES/messages.po
|
||||
msgfmt --statistics translations/en/LC_MESSAGES/messages.po
|
||||
```
|
||||
|
||||
## Future Expansion
|
||||
|
||||
To add a new language (e.g., German):
|
||||
|
||||
1. **Server-side**:
|
||||
```bash
|
||||
pybabel init -i translations/messages.pot -d translations -l de
|
||||
# Edit translations/de/LC_MESSAGES/messages.po
|
||||
python compile_translations.py
|
||||
```
|
||||
|
||||
2. **Client-side**:
|
||||
- Create `static/js/locales/de.json`
|
||||
- Add to config: `LANGUAGES = {..., 'de': 'Deutsch'}`
|
||||
|
||||
3. **Update loader**: Modify base template to support new locale
|
||||
|
||||
## Resources
|
||||
|
||||
- Flask-Babel: https://python-babel.github.io/flask-babel/
|
||||
- Babel: http://babel.pocoo.org/
|
||||
- Alpine.js i18n: https://github.com/alpine-collective/alpine-i18n
|
||||
- PO file format: https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Verify i18n setup for TieMeasureFlow."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
def check_file(path: Path, description: str) -> bool:
|
||||
"""Check if a file exists and report."""
|
||||
exists = path.exists()
|
||||
status = "[OK]" if exists else "[FAIL]"
|
||||
print(f"{status} {description}: {path}")
|
||||
return exists
|
||||
|
||||
def check_json_valid(path: Path) -> bool:
|
||||
"""Check if JSON file is valid."""
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
json.load(f)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Verify i18n setup."""
|
||||
print("=== TieMeasureFlow i18n Verification ===\n")
|
||||
|
||||
root = Path(__file__).parent
|
||||
all_good = True
|
||||
|
||||
# Check Flask-Babel files
|
||||
print("Flask-Babel (Server-side):")
|
||||
files = [
|
||||
(root / "translations/babel.cfg", "Babel config"),
|
||||
(root / "translations/it/LC_MESSAGES/messages.po", "Italian .po"),
|
||||
(root / "translations/it/LC_MESSAGES/messages.mo", "Italian .mo"),
|
||||
(root / "translations/en/LC_MESSAGES/messages.po", "English .po"),
|
||||
(root / "translations/en/LC_MESSAGES/messages.mo", "English .mo"),
|
||||
]
|
||||
|
||||
for path, desc in files:
|
||||
if not check_file(path, desc):
|
||||
all_good = False
|
||||
|
||||
# Count messages in .po files
|
||||
if (root / "translations/it/LC_MESSAGES/messages.po").exists():
|
||||
with open(root / "translations/it/LC_MESSAGES/messages.po", 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
msgid_count = content.count('msgid "') - 1 # Exclude header
|
||||
print(f" Italian: {msgid_count} messages")
|
||||
|
||||
if (root / "translations/en/LC_MESSAGES/messages.po").exists():
|
||||
with open(root / "translations/en/LC_MESSAGES/messages.po", 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
msgid_count = content.count('msgid "') - 1
|
||||
print(f" English: {msgid_count} messages")
|
||||
|
||||
# Check Alpine.js i18n files
|
||||
print("\nAlpine.js i18n (Client-side):")
|
||||
json_files = [
|
||||
(root / "static/js/locales/it.json", "Italian locale"),
|
||||
(root / "static/js/locales/en.json", "English locale"),
|
||||
]
|
||||
|
||||
for path, desc in json_files:
|
||||
if check_file(path, desc):
|
||||
if not check_json_valid(path):
|
||||
all_good = False
|
||||
else:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
keys = count_keys(data)
|
||||
print(f" {keys} total keys")
|
||||
|
||||
# Check app.py integration
|
||||
print("\nApp Integration:")
|
||||
app_py = root / "app.py"
|
||||
if check_file(app_py, "app.py"):
|
||||
with open(app_py, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
checks = [
|
||||
("from flask_babel import Babel", "Flask-Babel imported"),
|
||||
("def get_locale()", "get_locale() function"),
|
||||
("Babel(app, locale_selector=get_locale)", "Babel initialized"),
|
||||
("def set_language(lang)", "set_language endpoint"),
|
||||
]
|
||||
for check_str, check_desc in checks:
|
||||
found = check_str in content
|
||||
status = "[OK]" if found else "[FAIL]"
|
||||
print(f" {status} {check_desc}")
|
||||
if not found:
|
||||
all_good = False
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*40)
|
||||
if all_good:
|
||||
print("[OK] i18n setup verified successfully!")
|
||||
print("\nNext steps:")
|
||||
print("1. Use _() in Python code and templates")
|
||||
print("2. Use $t() in Alpine.js components")
|
||||
print("3. Test language switching: /set-language/en or /set-language/it")
|
||||
else:
|
||||
print("[FAIL] Some checks failed - review errors above")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
def count_keys(obj, depth=0):
|
||||
"""Recursively count keys in nested dict."""
|
||||
if not isinstance(obj, dict):
|
||||
return 0
|
||||
count = len(obj)
|
||||
for value in obj.values():
|
||||
if isinstance(value, dict):
|
||||
count += count_keys(value, depth + 1)
|
||||
return count
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
Reference in New Issue
Block a user