feat: v1.1.0 - Docker, i18n, setup wizard, rate limiting, OpenAPI
New features: - Docker support (Dockerfile + docker-compose.yml) - GitHub Actions CI pipeline (PHP lint, JS lint, Docker build, i18n validation) - Internationalization system with 3 languages (it, en, de) and 347 translation keys - First-run setup wizard (4-step configuration) - File-based API rate limiting (120/15/5 req/min tiers) - OpenAPI 3.1.0 specification for all 43 API endpoints - CONTRIBUTING.md with translation and development guide - Screenshots directory placeholder Modified: - README.md: Docker badges, install instructions, translations section - api/index.php: rate limiting middleware - assets/js/app.js: i18n system, setup wizard, t() function - assets/css/style.css: setup wizard styles - index.html: data-i18n attributes, setup wizard overlay, language settings - .gitignore: rate_limits exclusion
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
# Docker runtime files
|
||||||
|
data/dispensa.db
|
||||||
|
data/*.db-wal
|
||||||
|
data/*.db-shm
|
||||||
|
data/backups/
|
||||||
|
data/cron.log
|
||||||
|
data/smart_shopping_cache.json
|
||||||
|
data/bring_token.json
|
||||||
|
data/bring_catalog.json
|
||||||
|
data/dupliclick_token.json
|
||||||
|
data/client_debug.log
|
||||||
|
data/*.crt
|
||||||
|
data/*.pem
|
||||||
|
|
||||||
|
# Config (mounted as volume)
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Docs (not needed in container)
|
||||||
|
README.md
|
||||||
|
CHANGELOG.md
|
||||||
|
CONTRIBUTING.md
|
||||||
|
LICENSE
|
||||||
|
docs/
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-php:
|
||||||
|
name: PHP Syntax Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.2'
|
||||||
|
extensions: pdo_sqlite, curl, mbstring
|
||||||
|
|
||||||
|
- name: Check PHP syntax
|
||||||
|
run: |
|
||||||
|
find api/ -name '*.php' -exec php -l {} \;
|
||||||
|
|
||||||
|
lint-js:
|
||||||
|
name: JavaScript Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check JS syntax
|
||||||
|
run: |
|
||||||
|
node -c assets/js/app.js
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
name: Docker Build Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -t dispensa-test .
|
||||||
|
|
||||||
|
- name: Test container starts
|
||||||
|
run: |
|
||||||
|
docker run -d --name test-dispensa -p 8080:80 dispensa-test
|
||||||
|
sleep 5
|
||||||
|
curl -f http://localhost:8080/ || exit 1
|
||||||
|
docker stop test-dispensa
|
||||||
|
|
||||||
|
validate-translations:
|
||||||
|
name: Validate Translation Files
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate JSON syntax
|
||||||
|
run: |
|
||||||
|
for f in translations/*.json; do
|
||||||
|
echo "Checking $f..."
|
||||||
|
python3 -c "import json; json.load(open('$f'))" || exit 1
|
||||||
|
done
|
||||||
|
echo "All translation files valid."
|
||||||
|
|
||||||
|
- name: Check translation completeness
|
||||||
|
run: |
|
||||||
|
python3 -c "
|
||||||
|
import json, sys
|
||||||
|
base = json.load(open('translations/it.json'))
|
||||||
|
base_keys = set(base.keys())
|
||||||
|
ok = True
|
||||||
|
import glob
|
||||||
|
for f in glob.glob('translations/*.json'):
|
||||||
|
if 'it.json' in f:
|
||||||
|
continue
|
||||||
|
lang = json.load(open(f))
|
||||||
|
lang_keys = set(lang.keys())
|
||||||
|
missing = base_keys - lang_keys
|
||||||
|
if missing:
|
||||||
|
print(f'{f}: {len(missing)} missing keys')
|
||||||
|
for k in sorted(missing)[:10]:
|
||||||
|
print(f' - {k}')
|
||||||
|
if len(missing) > 10:
|
||||||
|
print(f' ... and {len(missing)-10} more')
|
||||||
|
else:
|
||||||
|
print(f'{f}: complete ✓')
|
||||||
|
"
|
||||||
@@ -12,6 +12,7 @@ data/bring_token.json
|
|||||||
data/bring_catalog.json
|
data/bring_catalog.json
|
||||||
data/dupliclick_token.json
|
data/dupliclick_token.json
|
||||||
data/client_debug.log
|
data/client_debug.log
|
||||||
|
data/rate_limits/
|
||||||
|
|
||||||
# SSL certificates (local only)
|
# SSL certificates (local only)
|
||||||
data/*.crt
|
data/*.crt
|
||||||
|
|||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
# Contributing to Dispensa Manager
|
||||||
|
|
||||||
|
Thank you for your interest in contributing! This guide will help you get started.
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
1. **Fork** the repository
|
||||||
|
2. **Clone** your fork:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR_USERNAME/dispensa.git
|
||||||
|
cd dispensa
|
||||||
|
```
|
||||||
|
3. **Create a branch** from `develop`:
|
||||||
|
```bash
|
||||||
|
git checkout develop
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
```
|
||||||
|
4. **Set up** your development environment:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your API keys
|
||||||
|
php -S localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📐 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── index.html # Single-page app (all HTML)
|
||||||
|
├── api/
|
||||||
|
│ ├── index.php # API router + all endpoint functions
|
||||||
|
│ ├── database.php # SQLite schema + migrations
|
||||||
|
│ └── cron_smart_shopping.php
|
||||||
|
├── assets/
|
||||||
|
│ ├── js/app.js # All application JavaScript
|
||||||
|
│ └── css/style.css # All styles
|
||||||
|
├── translations/ # i18n translation files
|
||||||
|
│ ├── it.json # Italian (base language)
|
||||||
|
│ ├── en.json # English
|
||||||
|
│ └── ...
|
||||||
|
└── data/ # Runtime data (gitignored)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌍 Contributing Translations
|
||||||
|
|
||||||
|
Translations are one of the easiest ways to contribute! Each language is a single JSON file in the `translations/` directory.
|
||||||
|
|
||||||
|
### Adding a new language
|
||||||
|
|
||||||
|
1. Copy `translations/it.json` (the base language)
|
||||||
|
2. Rename it to your language code (e.g., `fr.json`, `de.json`, `es.json`)
|
||||||
|
3. Translate all the values (keep the keys unchanged)
|
||||||
|
4. Submit a Pull Request
|
||||||
|
|
||||||
|
### Translation file format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"app.title": "Dispensa Manager",
|
||||||
|
"nav.dashboard": "Dashboard",
|
||||||
|
"nav.inventory": "Inventario",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Keys are in English, dot-separated (`section.key`)
|
||||||
|
- Values are the translated strings
|
||||||
|
- Keep `{0}`, `{1}` placeholders — they are filled dynamically
|
||||||
|
- Don't translate brand names (Bring!, Gemini, etc.)
|
||||||
|
- The CI pipeline will check your file for missing keys
|
||||||
|
|
||||||
|
### Language codes
|
||||||
|
|
||||||
|
Use [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) two-letter codes:
|
||||||
|
`it`, `en`, `de`, `fr`, `es`, `pt`, `nl`, `pl`, `ru`, `ja`, `zh`, `ko`, etc.
|
||||||
|
|
||||||
|
## 🔧 Development Guidelines
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
- **PHP**: PSR-12 compatible, use type hints where practical
|
||||||
|
- **JavaScript**: No build tools, vanilla ES6+, single-file architecture
|
||||||
|
- **CSS**: Mobile-first, use CSS custom properties from `:root`
|
||||||
|
- **Comments**: English only, concise
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- Use descriptive commit messages
|
||||||
|
- Reference issue numbers when applicable: `Fix #42: barcode scanner timeout`
|
||||||
|
- Keep commits focused on a single change
|
||||||
|
|
||||||
|
### Branching
|
||||||
|
- `main` — stable releases only
|
||||||
|
- `develop` — active development (PRs target here)
|
||||||
|
- `feature/*` — new features
|
||||||
|
- `fix/*` — bug fixes
|
||||||
|
- `i18n/*` — translation contributions
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
Before submitting a PR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check PHP syntax
|
||||||
|
php -l api/index.php
|
||||||
|
php -l api/database.php
|
||||||
|
|
||||||
|
# Check JS syntax
|
||||||
|
node -c assets/js/app.js
|
||||||
|
|
||||||
|
# Validate translation files
|
||||||
|
python3 -c "import json; json.load(open('translations/it.json'))"
|
||||||
|
|
||||||
|
# Test Docker build
|
||||||
|
docker build -t dispensa-test .
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Pull Request Process
|
||||||
|
|
||||||
|
1. Ensure your code passes all CI checks
|
||||||
|
2. Update `CHANGELOG.md` if applicable
|
||||||
|
3. Target the `develop` branch
|
||||||
|
4. Provide a clear description of your changes
|
||||||
|
5. Link any related issues
|
||||||
|
|
||||||
|
## 🐛 Reporting Bugs
|
||||||
|
|
||||||
|
Open an issue with:
|
||||||
|
- Steps to reproduce
|
||||||
|
- Expected vs. actual behavior
|
||||||
|
- Browser/device information
|
||||||
|
- Screenshots if applicable
|
||||||
|
|
||||||
|
## 💡 Feature Requests
|
||||||
|
|
||||||
|
Open an issue with the `enhancement` label. Describe:
|
||||||
|
- The problem you're trying to solve
|
||||||
|
- Your proposed solution
|
||||||
|
- Any alternatives you've considered
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
FROM php:8.2-apache
|
||||||
|
|
||||||
|
# Install required PHP extensions
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libsqlite3-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
&& docker-php-ext-install pdo_sqlite curl mbstring \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Enable Apache mod_rewrite
|
||||||
|
RUN a2enmod rewrite
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY . /var/www/html/
|
||||||
|
|
||||||
|
# Create data directory with proper permissions
|
||||||
|
RUN mkdir -p /var/www/html/data/backups \
|
||||||
|
&& chown -R www-data:www-data /var/www/html/data \
|
||||||
|
&& chmod -R 775 /var/www/html/data
|
||||||
|
|
||||||
|
# Create .env from example if it doesn't exist (will be overridden by volume mount)
|
||||||
|
RUN [ ! -f /var/www/html/.env ] && cp /var/www/html/.env.example /var/www/html/.env || true
|
||||||
|
|
||||||
|
# Apache configuration: serve from app root
|
||||||
|
RUN echo '<Directory /var/www/html>\n\
|
||||||
|
AllowOverride All\n\
|
||||||
|
Require all granted\n\
|
||||||
|
</Directory>' > /etc/apache2/conf-available/dispensa.conf \
|
||||||
|
&& a2enconf dispensa
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost/ || exit 1
|
||||||
|
|
||||||
|
CMD ["apache2-foreground"]
|
||||||
@@ -5,10 +5,14 @@
|
|||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://www.php.net/)
|
[](https://www.php.net/)
|
||||||
[](https://www.sqlite.org/)
|
[](https://www.sqlite.org/)
|
||||||
|
[](Dockerfile)
|
||||||
|
[](translations/)
|
||||||
|
|
||||||
|
<!--
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="assets/img/screenshot-dashboard.png" alt="Dashboard Screenshot" width="320" />
|
<img src="assets/img/screenshots/dashboard.png" alt="Dashboard Screenshot" width="320" />
|
||||||
</p>
|
</p>
|
||||||
|
-->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -63,6 +67,25 @@
|
|||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
|
#### Option A: Docker (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone the repository
|
||||||
|
git clone https://github.com/dadaloop82/dispensa.git
|
||||||
|
cd dispensa
|
||||||
|
|
||||||
|
# 2. Create configuration file
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# 3. Start with Docker Compose
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# → Open http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B: Manual
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Clone the repository
|
# 1. Clone the repository
|
||||||
git clone https://github.com/dadaloop82/dispensa.git
|
git clone https://github.com/dadaloop82/dispensa.git
|
||||||
@@ -248,16 +271,30 @@ The application uses no build tools — edit files directly and refresh.
|
|||||||
- [ ] Multi-language support (i18n)
|
- [ ] Multi-language support (i18n)
|
||||||
- [ ] User authentication / multi-user support
|
- [ ] User authentication / multi-user support
|
||||||
- [ ] Docker container for easy deployment
|
- [ ] Docker container for easy deployment
|
||||||
- [ ] REST API documentation (OpenAPI/Swagger)
|
- [x] REST API documentation (OpenAPI/Swagger) — see [docs/openapi.yaml](docs/openapi.yaml)
|
||||||
- [ ] Offline mode with service worker
|
- [ ] Offline mode with service worker
|
||||||
- [ ] Export/import inventory data
|
- [ ] Export/import inventory data
|
||||||
- [ ] Notification system (Telegram, email) for expiring products
|
- [ ] Notification system (Telegram, email) for expiring products
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🌐 Translations
|
||||||
|
|
||||||
|
The app supports multiple languages via JSON translation files in the `translations/` folder.
|
||||||
|
|
||||||
|
| Language | Status |
|
||||||
|
|----------|--------|
|
||||||
|
| 🇮🇹 Italian (it) | ✅ Complete (base) |
|
||||||
|
| 🇬🇧 English (en) | ✅ Complete |
|
||||||
|
| 🇩🇪 German (de) | ✅ Complete |
|
||||||
|
|
||||||
|
**Want to add your language?** See the [Translation Guide](CONTRIBUTING.md#-adding-translations) — just copy `translations/it.json`, translate the values, and submit a PR!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please:
|
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
||||||
|
|
||||||
1. Fork the repository
|
1. Fork the repository
|
||||||
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
2. Create a feature branch (`git checkout -b feature/my-feature`)
|
||||||
|
|||||||
@@ -52,6 +52,74 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== RATE LIMITING =====
|
||||||
|
/**
|
||||||
|
* Simple file-based rate limiter.
|
||||||
|
* Limits: 120 req/min general, 15 req/min for AI endpoints, 5 req/min for login.
|
||||||
|
*/
|
||||||
|
function checkRateLimit(string $action): void {
|
||||||
|
$rateLimitDir = __DIR__ . '/../data/rate_limits';
|
||||||
|
if (!is_dir($rateLimitDir)) {
|
||||||
|
mkdir($rateLimitDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine limit based on action
|
||||||
|
$aiActions = ['gemini_readExpiry', 'gemini_chat', 'gemini_identify', 'gemini_suggest_shopping'];
|
||||||
|
$loginActions = ['dupliclick_login'];
|
||||||
|
|
||||||
|
if (in_array($action, $aiActions)) {
|
||||||
|
$limit = 15;
|
||||||
|
$window = 60;
|
||||||
|
$bucket = 'ai';
|
||||||
|
} elseif (in_array($action, $loginActions)) {
|
||||||
|
$limit = 5;
|
||||||
|
$window = 60;
|
||||||
|
$bucket = 'login';
|
||||||
|
} else {
|
||||||
|
$limit = 120;
|
||||||
|
$window = 60;
|
||||||
|
$bucket = 'general';
|
||||||
|
}
|
||||||
|
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||||
|
$file = $rateLimitDir . '/' . md5($ip . '_' . $bucket) . '.json';
|
||||||
|
|
||||||
|
// Clean up old rate limit files periodically (1% chance per request)
|
||||||
|
if (mt_rand(1, 100) === 1) {
|
||||||
|
foreach (glob($rateLimitDir . '/*.json') as $f) {
|
||||||
|
if (filemtime($f) < time() - 300) @unlink($f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$data = [];
|
||||||
|
if (file_exists($file)) {
|
||||||
|
$raw = @file_get_contents($file);
|
||||||
|
if ($raw) $data = json_decode($raw, true) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove entries outside the window
|
||||||
|
$data = array_values(array_filter($data, function($ts) use ($now, $window) {
|
||||||
|
return $ts > $now - $window;
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (count($data) >= $limit) {
|
||||||
|
http_response_code(429);
|
||||||
|
header('Retry-After: ' . $window);
|
||||||
|
echo json_encode(['error' => 'Too many requests. Please try again later.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[] = $now;
|
||||||
|
@file_put_contents($file, json_encode($data), LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply rate limiting
|
||||||
|
$rateLimitAction = $_GET['action'] ?? '';
|
||||||
|
if ($rateLimitAction) {
|
||||||
|
checkRateLimit($rateLimitAction);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$db = getDB();
|
$db = getDB();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|||||||
@@ -5072,3 +5072,113 @@ body {
|
|||||||
background: rgba(255,255,255,0.25);
|
background: rgba(255,255,255,0.25);
|
||||||
transform: scale(0.92);
|
transform: scale(0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== SETUP WIZARD ===== */
|
||||||
|
.setup-wizard-content {
|
||||||
|
max-width: 480px;
|
||||||
|
width: 95%;
|
||||||
|
margin: auto;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.setup-header {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
padding: 24px 20px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.setup-header h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.setup-progress {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.setup-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
transition: background 0.3s, transform 0.3s;
|
||||||
|
}
|
||||||
|
.setup-dot.active {
|
||||||
|
background: #fff;
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
.setup-dot.done {
|
||||||
|
background: var(--success-light);
|
||||||
|
}
|
||||||
|
.setup-body {
|
||||||
|
padding: 24px 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.setup-body h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.setup-body p {
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.setup-body .form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.setup-body .form-input {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.setup-lang-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.setup-lang-btn {
|
||||||
|
padding: 14px 12px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
.setup-lang-btn.selected {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(45,80,22,0.08);
|
||||||
|
}
|
||||||
|
.setup-lang-btn:hover {
|
||||||
|
border-color: var(--primary-light);
|
||||||
|
}
|
||||||
|
.setup-footer {
|
||||||
|
padding: 16px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.setup-footer .btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.setup-skip-link {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.setup-skip-link:hover {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Screenshots
|
||||||
|
|
||||||
|
Add screenshots here to showcase the app in the README.
|
||||||
|
|
||||||
|
## Recommended screenshots
|
||||||
|
|
||||||
|
Take screenshots of these pages for the README `Screenshots` section:
|
||||||
|
|
||||||
|
1. **dashboard.png** — Main dashboard with stats cards and expiry alerts
|
||||||
|
2. **inventory.png** — Inventory list with product cards
|
||||||
|
3. **scan.png** — Barcode scanning page
|
||||||
|
4. **recipe.png** — Generated recipe with cooking mode
|
||||||
|
5. **shopping.png** — Shopping list with smart predictions
|
||||||
|
6. **chat.png** — Gemini Chef AI conversation
|
||||||
|
7. **settings.png** — Settings page
|
||||||
|
8. **setup.png** — First-run setup wizard
|
||||||
|
|
||||||
|
## How to add
|
||||||
|
|
||||||
|
1. Take screenshots on a mobile device or using Chrome DevTools device emulation
|
||||||
|
2. Recommended size: 375×812 (iPhone X viewport)
|
||||||
|
3. Save as PNG with descriptive names
|
||||||
|
4. Update the README.md `## Screenshots` section to reference them:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
| Dashboard | Inventory | Scan |
|
||||||
|
|:-:|:-:|:-:|
|
||||||
|
|  |  |  |
|
||||||
|
```
|
||||||
+343
-34
@@ -57,6 +57,123 @@ window.addEventListener('unhandledrejection', function(e) {
|
|||||||
|
|
||||||
// ===== CONFIGURATION =====
|
// ===== CONFIGURATION =====
|
||||||
const API_BASE = 'api/index.php';
|
const API_BASE = 'api/index.php';
|
||||||
|
|
||||||
|
// ===== i18n TRANSLATION SYSTEM =====
|
||||||
|
let _i18nStrings = null; // current language translations (flat)
|
||||||
|
let _i18nFallback = null; // Italian fallback (flat)
|
||||||
|
let _currentLang = localStorage.getItem('dispensa_lang') || navigator.language?.slice(0, 2) || 'it';
|
||||||
|
const _SUPPORTED_LANGS = { it: 'Italiano', en: 'English', de: 'Deutsch' };
|
||||||
|
if (!_SUPPORTED_LANGS[_currentLang]) _currentLang = 'it';
|
||||||
|
|
||||||
|
// Flatten nested JSON: { a: { b: "x" } } → { "a.b": "x" }
|
||||||
|
function _flattenI18n(obj, prefix = '') {
|
||||||
|
const result = {};
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
const key = prefix ? `${prefix}.${k}` : k;
|
||||||
|
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
||||||
|
Object.assign(result, _flattenI18n(v, key));
|
||||||
|
} else {
|
||||||
|
result[key] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translation function: t('toast.thrown_away', {name: 'Latte'})
|
||||||
|
function t(key, params) {
|
||||||
|
let str = (_i18nStrings && _i18nStrings[key]) || (_i18nFallback && _i18nFallback[key]) || key;
|
||||||
|
if (params) {
|
||||||
|
for (const [k, v] of Object.entries(params)) {
|
||||||
|
str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load translations from JSON files
|
||||||
|
async function loadTranslations(lang) {
|
||||||
|
lang = lang || _currentLang;
|
||||||
|
try {
|
||||||
|
// Always load Italian as fallback
|
||||||
|
if (!_i18nFallback) {
|
||||||
|
const fbRes = await fetch(`translations/it.json?v=${Date.now()}`);
|
||||||
|
if (fbRes.ok) _i18nFallback = _flattenI18n(await fbRes.json());
|
||||||
|
}
|
||||||
|
if (lang === 'it') {
|
||||||
|
_i18nStrings = _i18nFallback;
|
||||||
|
} else {
|
||||||
|
const res = await fetch(`translations/${encodeURIComponent(lang)}.json?v=${Date.now()}`);
|
||||||
|
if (res.ok) _i18nStrings = _flattenI18n(await res.json());
|
||||||
|
else _i18nStrings = _i18nFallback;
|
||||||
|
}
|
||||||
|
_currentLang = lang;
|
||||||
|
localStorage.setItem('dispensa_lang', lang);
|
||||||
|
_applyI18nToLabels();
|
||||||
|
translatePage();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('i18n: Failed to load translations for', lang, e);
|
||||||
|
_i18nStrings = _i18nFallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update LOCATIONS / SHOPPING_SECTIONS labels from translations
|
||||||
|
function _applyI18nToLabels() {
|
||||||
|
if (!_i18nStrings) return;
|
||||||
|
for (const key of Object.keys(LOCATIONS)) {
|
||||||
|
const tKey = `locations.${key}`;
|
||||||
|
if (_i18nStrings[tKey]) LOCATIONS[key].label = _i18nStrings[tKey];
|
||||||
|
}
|
||||||
|
for (const sec of SHOPPING_SECTIONS) {
|
||||||
|
const tKey = `shopping_sections.${sec.key}`;
|
||||||
|
if (_i18nStrings[tKey]) sec.label = _i18nStrings[tKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate all elements with data-i18n attributes
|
||||||
|
function translatePage() {
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n');
|
||||||
|
if (key) el.textContent = t(key);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-i18n-html]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n-html');
|
||||||
|
if (key) el.innerHTML = t(key);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n-placeholder');
|
||||||
|
if (key) el.placeholder = t(key);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-i18n-title]').forEach(el => {
|
||||||
|
const key = el.getAttribute('data-i18n-title');
|
||||||
|
if (key) el.title = t(key);
|
||||||
|
});
|
||||||
|
// Update HTML lang attribute
|
||||||
|
document.documentElement.lang = _currentLang;
|
||||||
|
// Populate language selector if present
|
||||||
|
_populateLanguageSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the language selector dropdown
|
||||||
|
function _populateLanguageSelector() {
|
||||||
|
const sel = document.getElementById('setting-language');
|
||||||
|
if (!sel) return;
|
||||||
|
sel.innerHTML = '';
|
||||||
|
for (const [code, name] of Object.entries(_SUPPORTED_LANGS)) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = code;
|
||||||
|
opt.textContent = name;
|
||||||
|
if (code === _currentLang) opt.selected = true;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change language and reload the page
|
||||||
|
function changeLanguage(lang) {
|
||||||
|
if (lang === _currentLang) return;
|
||||||
|
localStorage.setItem('dispensa_lang', lang);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
const LOCATIONS = {
|
const LOCATIONS = {
|
||||||
'dispensa': { icon: '🗄️', label: 'Dispensa' },
|
'dispensa': { icon: '🗄️', label: 'Dispensa' },
|
||||||
'frigo': { icon: '🧊', label: 'Frigo' },
|
'frigo': { icon: '🧊', label: 'Frigo' },
|
||||||
@@ -822,21 +939,21 @@ function addAppliance() {
|
|||||||
const s = getSettings();
|
const s = getSettings();
|
||||||
if (!s.appliances) s.appliances = [];
|
if (!s.appliances) s.appliances = [];
|
||||||
if (s.appliances.some(a => a.toLowerCase() === name.toLowerCase())) {
|
if (s.appliances.some(a => a.toLowerCase() === name.toLowerCase())) {
|
||||||
showToast('Elettrodomestico già presente', 'error');
|
showToast(t('error.appliance_exists'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
s.appliances.push(name);
|
s.appliances.push(name);
|
||||||
saveSettingsToStorage(s);
|
saveSettingsToStorage(s);
|
||||||
renderAppliances(s.appliances);
|
renderAppliances(s.appliances);
|
||||||
input.value = '';
|
input.value = '';
|
||||||
showToast('Elettrodomestico aggiunto', 'success');
|
showToast(t('toast.appliance_added'), 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
function addApplianceQuick(name) {
|
function addApplianceQuick(name) {
|
||||||
const s = getSettings();
|
const s = getSettings();
|
||||||
if (!s.appliances) s.appliances = [];
|
if (!s.appliances) s.appliances = [];
|
||||||
if (s.appliances.some(a => a.toLowerCase() === name.toLowerCase())) {
|
if (s.appliances.some(a => a.toLowerCase() === name.toLowerCase())) {
|
||||||
showToast('Già presente', 'error');
|
showToast(t('error.already_exists'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
s.appliances.push(name);
|
s.appliances.push(name);
|
||||||
@@ -1364,7 +1481,7 @@ function confirmReviewItem(inventoryId) {
|
|||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
showToast('✓ Quantità confermata', 'success');
|
showToast(t('toast.quantity_confirmed'), 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
function editReviewItem(inventoryId, productId) {
|
function editReviewItem(inventoryId, productId) {
|
||||||
@@ -1747,7 +1864,7 @@ async function quickUse(productId, location) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
console.error('quickUse error:', err);
|
console.error('quickUse error:', err);
|
||||||
showToast('Errore nel caricamento del prodotto', 'error');
|
showToast(t('error.loading'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1755,7 +1872,7 @@ async function deleteInventoryItem(id) {
|
|||||||
if (confirm('Vuoi davvero rimuovere questo prodotto dall\'inventario?')) {
|
if (confirm('Vuoi davvero rimuovere questo prodotto dall\'inventario?')) {
|
||||||
await api('inventory_delete', {}, 'POST', { id });
|
await api('inventory_delete', {}, 'POST', { id });
|
||||||
closeModal();
|
closeModal();
|
||||||
showToast('Prodotto rimosso', 'success');
|
showToast(t('toast.product_removed'), 'success');
|
||||||
refreshCurrentPage();
|
refreshCurrentPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1779,7 +1896,7 @@ function editInventoryItem(id) {
|
|||||||
const item = currentInventory.find(i => i.id === id);
|
const item = currentInventory.find(i => i.id === id);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
closeModal();
|
closeModal();
|
||||||
showToast('Prodotto non trovato', 'error');
|
showToast(t('error.not_found'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2359,7 +2476,7 @@ function submitManualBarcode() {
|
|||||||
const input = document.getElementById('manual-barcode-input');
|
const input = document.getElementById('manual-barcode-input');
|
||||||
const barcode = (input.value || '').trim();
|
const barcode = (input.value || '').trim();
|
||||||
if (!barcode) {
|
if (!barcode) {
|
||||||
showToast('Inserisci un codice a barre', 'error');
|
showToast(t('error.barcode_empty'), 'error');
|
||||||
input.focus();
|
input.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2377,7 +2494,7 @@ async function submitQuickName() {
|
|||||||
const input = document.getElementById('quick-product-name');
|
const input = document.getElementById('quick-product-name');
|
||||||
const name = (input.value || '').trim();
|
const name = (input.value || '').trim();
|
||||||
if (!name || name.length < 2) {
|
if (!name || name.length < 2) {
|
||||||
showToast('Scrivi almeno 2 caratteri', 'error');
|
showToast(t('error.min_chars'), 'error');
|
||||||
input.focus();
|
input.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2402,7 +2519,7 @@ async function submitQuickName() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
console.error('Quick name search error:', err);
|
console.error('Quick name search error:', err);
|
||||||
showToast('Errore nella ricerca', 'error');
|
showToast(t('error.search_short'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2506,7 +2623,7 @@ async function createQuickProduct(name) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
console.error('Quick product creation error:', err);
|
console.error('Quick product creation error:', err);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2794,7 +2911,7 @@ async function submitProduct(e) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3270,7 +3387,7 @@ async function deleteActionInventoryItem(id) {
|
|||||||
if (confirm('Vuoi davvero rimuovere questo prodotto dall\'inventario?')) {
|
if (confirm('Vuoi davvero rimuovere questo prodotto dall\'inventario?')) {
|
||||||
await api('inventory_delete', {}, 'POST', { id });
|
await api('inventory_delete', {}, 'POST', { id });
|
||||||
closeModal();
|
closeModal();
|
||||||
showToast('Prodotto rimosso', 'success');
|
showToast(t('toast.product_removed'), 'success');
|
||||||
showProductAction(); // Refresh the action page
|
showProductAction(); // Refresh the action page
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3371,7 +3488,7 @@ async function throwAll() {
|
|||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3396,7 +3513,7 @@ async function throwPartial() {
|
|||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3412,7 +3529,7 @@ function toggleActionEdit() {
|
|||||||
async function saveEditedProductInfo() {
|
async function saveEditedProductInfo() {
|
||||||
const name = (document.getElementById('edit-action-name')?.value || '').trim();
|
const name = (document.getElementById('edit-action-name')?.value || '').trim();
|
||||||
if (!name) {
|
if (!name) {
|
||||||
showToast('Inserisci il nome del prodotto', 'error');
|
showToast(t('product.name_required'), 'error');
|
||||||
document.getElementById('edit-action-name')?.focus();
|
document.getElementById('edit-action-name')?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3446,7 +3563,7 @@ async function saveEditedProductInfo() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3915,7 +4032,7 @@ async function submitAdd(e) {
|
|||||||
}
|
}
|
||||||
showToast(`✅ ${currentProduct.name} aggiunto!${qtyInfo}`, 'success');
|
showToast(`✅ ${currentProduct.name} aggiunto!${qtyInfo}`, 'success');
|
||||||
if (result.removed_from_bring) {
|
if (result.removed_from_bring) {
|
||||||
setTimeout(() => showToast('🛒 Rimosso dalla lista della spesa', 'info'), 1500);
|
setTimeout(() => showToast(t('toast.removed_from_shopping'), 'info'), 1500);
|
||||||
} else if (shoppingItems.length > 0 && shoppingListUUID) {
|
} else if (shoppingItems.length > 0 && shoppingListUUID) {
|
||||||
// PHP matching may have missed the item (custom name / no catalog match) —
|
// PHP matching may have missed the item (custom name / no catalog match) —
|
||||||
// try a client-side fuzzy remove using the already-loaded shoppingItems
|
// try a client-side fuzzy remove using the already-loaded shoppingItems
|
||||||
@@ -3928,7 +4045,7 @@ async function submitAdd(e) {
|
|||||||
}).then(r => {
|
}).then(r => {
|
||||||
if (r && r.success) {
|
if (r && r.success) {
|
||||||
shoppingItems = shoppingItems.filter(i => i !== match);
|
shoppingItems = shoppingItems.filter(i => i !== match);
|
||||||
setTimeout(() => showToast('🛒 Rimosso dalla lista della spesa', 'info'), 1500);
|
setTimeout(() => showToast(t('toast.removed_from_shopping'), 'info'), 1500);
|
||||||
}
|
}
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -3961,7 +4078,7 @@ async function submitAdd(e) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4384,7 +4501,7 @@ async function addLowStockToBring(productName) {
|
|||||||
if (data.success && data.added > 0) {
|
if (data.success && data.added > 0) {
|
||||||
showToast('🛒 Aggiunto alla lista della spesa!', 'success');
|
showToast('🛒 Aggiunto alla lista della spesa!', 'success');
|
||||||
} else if (data.success && data.skipped > 0) {
|
} else if (data.success && data.skipped > 0) {
|
||||||
showToast('ℹ️ Già nella lista della spesa', 'info');
|
showToast(t('shopping.already_in_list_short'), 'info');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Errore nell\'aggiunta a Bring!', 'error');
|
showToast('Errore nell\'aggiunta a Bring!', 'error');
|
||||||
@@ -4512,7 +4629,7 @@ async function submitUseAll() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4556,7 +4673,7 @@ async function submitUse(e) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4770,12 +4887,12 @@ async function selectAIMatch(idx) {
|
|||||||
showProductAction();
|
showProductAction();
|
||||||
} else {
|
} else {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore nel salvataggio', 'error');
|
showToast(t('error.save'), 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
console.error('AI match select error:', err);
|
console.error('AI match select error:', err);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4804,7 +4921,7 @@ async function saveAIProductDirect() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5062,7 +5179,7 @@ async function selectProductForAction(productId) {
|
|||||||
showProductAction();
|
showProductAction();
|
||||||
} else {
|
} else {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Prodotto non trovato', 'error');
|
showToast(t('error.not_found'), 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
@@ -5663,7 +5780,7 @@ async function addSmartToBring() {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showLoading(false);
|
showLoading(false);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6191,7 +6308,7 @@ async function removeBringItem(idx) {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
shoppingItems.splice(idx, 1);
|
shoppingItems.splice(idx, 1);
|
||||||
renderShoppingItems();
|
renderShoppingItems();
|
||||||
showToast('Rimosso dalla lista', 'success');
|
showToast(t('toast.removed_from_list_short'), 'success');
|
||||||
logOperation('bring_manual_remove', { name: item.name });
|
logOperation('bring_manual_remove', { name: item.name });
|
||||||
// Update dashboard shopping count
|
// Update dashboard shopping count
|
||||||
loadShoppingCount();
|
loadShoppingCount();
|
||||||
@@ -6242,7 +6359,7 @@ async function generateSuggestions() {
|
|||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '🤖 Suggerisci cosa comprare';
|
btn.innerHTML = '🤖 Suggerisci cosa comprare';
|
||||||
console.error('Suggestion error:', err);
|
console.error('Suggestion error:', err);
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6328,7 +6445,7 @@ async function addSelectedSuggestions() {
|
|||||||
showToast(data.error || 'Errore', 'error');
|
showToast(data.error || 'Errore', 'error');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
@@ -7186,7 +7303,7 @@ async function submitRecipeUse(useAll) {
|
|||||||
console.error('Recipe use error:', err);
|
console.error('Recipe use error:', err);
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '📦 Usa';
|
btn.textContent = '📦 Usa';
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
_recipeUseContext = null;
|
_recipeUseContext = null;
|
||||||
}
|
}
|
||||||
@@ -7921,7 +8038,7 @@ async function generateRecipe() {
|
|||||||
console.error('Recipe error:', err);
|
console.error('Recipe error:', err);
|
||||||
document.getElementById('recipe-loading').style.display = 'none';
|
document.getElementById('recipe-loading').style.display = 'none';
|
||||||
document.getElementById('recipe-ask').style.display = '';
|
document.getElementById('recipe-ask').style.display = '';
|
||||||
showToast('Errore di connessione', 'error');
|
showToast(t('error.connection'), 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8694,6 +8811,198 @@ function initInactivityWatcher() {
|
|||||||
|
|
||||||
// ===== INITIALIZATION =====
|
// ===== INITIALIZATION =====
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Load translations first, then initialize the app
|
||||||
|
loadTranslations(_currentLang).then(() => {
|
||||||
|
_initApp();
|
||||||
|
}).catch(() => {
|
||||||
|
_initApp(); // fallback: initialize even if translations fail
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== SETUP WIZARD =====
|
||||||
|
let _setupStep = 0;
|
||||||
|
const _setupData = { lang: _currentLang, gemini_key: '', bring_email: '', bring_password: '' };
|
||||||
|
|
||||||
|
function _isFirstRun() {
|
||||||
|
return !localStorage.getItem('dispensa_setup_done');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setupSteps() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '🌐 ' + t('settings.language.label'),
|
||||||
|
desc: t('settings.language.hint'),
|
||||||
|
render: () => {
|
||||||
|
let html = '<div class="setup-lang-grid">';
|
||||||
|
for (const [code, name] of Object.entries(_SUPPORTED_LANGS)) {
|
||||||
|
const sel = code === _setupData.lang ? ' selected' : '';
|
||||||
|
html += `<button class="setup-lang-btn${sel}" onclick="_setupSelectLang('${code}')">${name}</button>`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '🤖 Google Gemini AI',
|
||||||
|
desc: t('settings.gemini.hint'),
|
||||||
|
render: () => `
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${t('settings.gemini.key_label')}</label>
|
||||||
|
<input type="text" id="setup-gemini-key" class="form-input" placeholder="AIza..." value="${_setupData.gemini_key}">
|
||||||
|
<p style="color:#999;font-size:0.8rem;margin-top:8px">
|
||||||
|
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener">→ Get a free API key from Google AI Studio</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span class="setup-skip-link" onclick="_setupSkipStep()">${t('btn.cancel')} — ${_currentLang === 'it' ? 'configura dopo' : 'configure later'}</span>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '🛒 Bring! Shopping List',
|
||||||
|
desc: t('settings.bring.hint'),
|
||||||
|
render: () => `
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${t('settings.bring.email_label')}</label>
|
||||||
|
<input type="email" id="setup-bring-email" class="form-input" placeholder="email@example.com" value="${_setupData.bring_email}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${t('settings.bring.password_label')}</label>
|
||||||
|
<input type="password" id="setup-bring-password" class="form-input" placeholder="Password" value="${_setupData.bring_password}">
|
||||||
|
</div>
|
||||||
|
<span class="setup-skip-link" onclick="_setupSkipStep()">${t('btn.cancel')} — ${_currentLang === 'it' ? 'configura dopo' : 'configure later'}</span>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '✅ ' + (_currentLang === 'it' ? 'Tutto pronto!' : _currentLang === 'de' ? 'Alles bereit!' : 'All set!'),
|
||||||
|
desc: _currentLang === 'it' ? 'La configurazione è completata. Puoi sempre modificare queste impostazioni dalla pagina Configurazione.'
|
||||||
|
: _currentLang === 'de' ? 'Die Konfiguration ist abgeschlossen. Du kannst diese Einstellungen jederzeit ändern.'
|
||||||
|
: 'Setup is complete. You can always change these settings from the Settings page.',
|
||||||
|
render: () => {
|
||||||
|
let summary = '<div style="text-align:center;font-size:2.5rem;margin:12px 0">🎉</div>';
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSetupWizard() {
|
||||||
|
_setupStep = 0;
|
||||||
|
document.getElementById('setup-wizard').style.display = '';
|
||||||
|
_renderSetupStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderSetupStep() {
|
||||||
|
const steps = _setupSteps();
|
||||||
|
const step = steps[_setupStep];
|
||||||
|
|
||||||
|
// Progress dots
|
||||||
|
const dotsHtml = steps.map((_, i) => {
|
||||||
|
let cls = 'setup-dot';
|
||||||
|
if (i < _setupStep) cls += ' done';
|
||||||
|
if (i === _setupStep) cls += ' active';
|
||||||
|
return `<div class="${cls}"></div>`;
|
||||||
|
}).join('');
|
||||||
|
document.getElementById('setup-progress').innerHTML = dotsHtml;
|
||||||
|
|
||||||
|
// Body
|
||||||
|
document.getElementById('setup-body').innerHTML = `<h3>${step.title}</h3><p>${step.desc}</p>${step.render()}`;
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
const prevBtn = document.getElementById('setup-prev');
|
||||||
|
const nextBtn = document.getElementById('setup-next');
|
||||||
|
prevBtn.style.display = _setupStep > 0 ? '' : 'none';
|
||||||
|
prevBtn.textContent = t('btn.back');
|
||||||
|
|
||||||
|
if (_setupStep === steps.length - 1) {
|
||||||
|
nextBtn.textContent = _currentLang === 'it' ? '🚀 Inizia!' : _currentLang === 'de' ? '🚀 Los geht\'s!' : '🚀 Start!';
|
||||||
|
} else {
|
||||||
|
nextBtn.textContent = _currentLang === 'it' ? 'Avanti →' : _currentLang === 'de' ? 'Weiter →' : 'Next →';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setupSelectLang(lang) {
|
||||||
|
_setupData.lang = lang;
|
||||||
|
document.querySelectorAll('.setup-lang-btn').forEach(b => b.classList.remove('selected'));
|
||||||
|
event.target.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setupSkipStep() {
|
||||||
|
_setupStep++;
|
||||||
|
_renderSetupStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setupCollectCurrent() {
|
||||||
|
if (_setupStep === 1) {
|
||||||
|
const el = document.getElementById('setup-gemini-key');
|
||||||
|
if (el) _setupData.gemini_key = el.value.trim();
|
||||||
|
} else if (_setupStep === 2) {
|
||||||
|
const email = document.getElementById('setup-bring-email');
|
||||||
|
const pass = document.getElementById('setup-bring-password');
|
||||||
|
if (email) _setupData.bring_email = email.value.trim();
|
||||||
|
if (pass) _setupData.bring_password = pass.value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupWizardNav(dir) {
|
||||||
|
_setupCollectCurrent();
|
||||||
|
const steps = _setupSteps();
|
||||||
|
|
||||||
|
if (dir === 1 && _setupStep === steps.length - 1) {
|
||||||
|
// Finish wizard
|
||||||
|
_finishSetup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If language changed, apply it
|
||||||
|
if (_setupStep === 0 && dir === 1 && _setupData.lang !== _currentLang) {
|
||||||
|
localStorage.setItem('dispensa_lang', _setupData.lang);
|
||||||
|
localStorage.setItem('dispensa_setup_step', '1');
|
||||||
|
localStorage.setItem('dispensa_setup_data', JSON.stringify(_setupData));
|
||||||
|
location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupStep = Math.max(0, Math.min(steps.length - 1, _setupStep + dir));
|
||||||
|
_renderSetupStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _finishSetup() {
|
||||||
|
// Save settings
|
||||||
|
const s = getSettings();
|
||||||
|
if (_setupData.gemini_key) s.gemini_key = _setupData.gemini_key;
|
||||||
|
if (_setupData.bring_email) s.bring_email = _setupData.bring_email;
|
||||||
|
if (_setupData.bring_password) s.bring_password = _setupData.bring_password;
|
||||||
|
saveSettingsToStorage(s);
|
||||||
|
|
||||||
|
// Save server-side settings (.env)
|
||||||
|
try {
|
||||||
|
await api('save_settings', {}, 'POST', {
|
||||||
|
gemini_key: _setupData.gemini_key,
|
||||||
|
bring_email: _setupData.bring_email,
|
||||||
|
bring_password: _setupData.bring_password
|
||||||
|
});
|
||||||
|
} catch(e) { /* will work locally */ }
|
||||||
|
|
||||||
|
localStorage.setItem('dispensa_setup_done', '1');
|
||||||
|
localStorage.removeItem('dispensa_setup_step');
|
||||||
|
localStorage.removeItem('dispensa_setup_data');
|
||||||
|
document.getElementById('setup-wizard').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initApp() {
|
||||||
|
// Check for setup wizard resume (after language change)
|
||||||
|
const resumeStep = localStorage.getItem('dispensa_setup_step');
|
||||||
|
const resumeData = localStorage.getItem('dispensa_setup_data');
|
||||||
|
if (resumeStep) {
|
||||||
|
try { Object.assign(_setupData, JSON.parse(resumeData)); } catch(e) {}
|
||||||
|
_setupStep = parseInt(resumeStep) || 0;
|
||||||
|
localStorage.removeItem('dispensa_setup_step');
|
||||||
|
localStorage.removeItem('dispensa_setup_data');
|
||||||
|
document.getElementById('setup-wizard').style.display = '';
|
||||||
|
_renderSetupStep();
|
||||||
|
} else if (_isFirstRun()) {
|
||||||
|
showSetupWizard();
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate old session-based flags to time-based
|
// Migrate old session-based flags to time-based
|
||||||
if (sessionStorage.getItem('_autoAddedCritical')) {
|
if (sessionStorage.getItem('_autoAddedCritical')) {
|
||||||
sessionStorage.removeItem('_autoAddedCritical');
|
sessionStorage.removeItem('_autoAddedCritical');
|
||||||
@@ -8738,7 +9047,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Silent background sync: update urgency specs on Bring and add missing critical items
|
// Silent background sync: update urgency specs on Bring and add missing critical items
|
||||||
// Runs once at startup (time-gated: max every 10 min) without affecting the UI
|
// Runs once at startup (time-gated: max every 10 min) without affecting the UI
|
||||||
_backgroundBringSync();
|
_backgroundBringSync();
|
||||||
});
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Background sync at startup:
|
* Background sync at startup:
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
dispensa:
|
||||||
|
build: .
|
||||||
|
container_name: dispensa
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
# Persist database and runtime data
|
||||||
|
- dispensa_data:/var/www/html/data
|
||||||
|
# Mount your local .env configuration
|
||||||
|
- ./.env:/var/www/html/.env:ro
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Rome
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dispensa_data:
|
||||||
|
driver: local
|
||||||
@@ -0,0 +1,691 @@
|
|||||||
|
openapi: "3.1.0"
|
||||||
|
info:
|
||||||
|
title: Dispensa Manager API
|
||||||
|
description: |
|
||||||
|
REST API for Dispensa Manager — a self-hosted pantry management system.
|
||||||
|
All endpoints use the query parameter `action` to determine the operation.
|
||||||
|
|
||||||
|
**Base URL:** `api/index.php?action={action_name}`
|
||||||
|
|
||||||
|
Rate limits apply:
|
||||||
|
- General: 120 requests/minute
|
||||||
|
- AI endpoints: 15 requests/minute
|
||||||
|
- Login endpoints: 5 requests/minute
|
||||||
|
version: "1.0.0"
|
||||||
|
contact:
|
||||||
|
name: Stimpfl Daniel
|
||||||
|
email: dadaloop82@gmail.com
|
||||||
|
license:
|
||||||
|
name: MIT
|
||||||
|
url: https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: /api
|
||||||
|
description: Local server
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/index.php?action=search_barcode:
|
||||||
|
get:
|
||||||
|
summary: Search product by barcode in local database
|
||||||
|
tags: [Products]
|
||||||
|
parameters:
|
||||||
|
- name: barcode
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Product found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Product"
|
||||||
|
"404":
|
||||||
|
description: Product not found
|
||||||
|
|
||||||
|
/index.php?action=lookup_barcode:
|
||||||
|
get:
|
||||||
|
summary: Lookup barcode on Open Food Facts
|
||||||
|
tags: [Products]
|
||||||
|
parameters:
|
||||||
|
- name: barcode
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Product data from Open Food Facts
|
||||||
|
|
||||||
|
/index.php?action=product_save:
|
||||||
|
post:
|
||||||
|
summary: Create or update a product
|
||||||
|
tags: [Products]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ProductInput"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Product saved
|
||||||
|
|
||||||
|
/index.php?action=product_get:
|
||||||
|
get:
|
||||||
|
summary: Get product by ID
|
||||||
|
tags: [Products]
|
||||||
|
parameters:
|
||||||
|
- name: id
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Product details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/Product"
|
||||||
|
|
||||||
|
/index.php?action=product_delete:
|
||||||
|
post:
|
||||||
|
summary: Delete a product
|
||||||
|
tags: [Products]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Product deleted
|
||||||
|
|
||||||
|
/index.php?action=products_list:
|
||||||
|
get:
|
||||||
|
summary: List all products
|
||||||
|
tags: [Products]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Array of all products
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Product"
|
||||||
|
|
||||||
|
/index.php?action=products_search:
|
||||||
|
get:
|
||||||
|
summary: Search products by name
|
||||||
|
tags: [Products]
|
||||||
|
parameters:
|
||||||
|
- name: q
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Matching products
|
||||||
|
|
||||||
|
/index.php?action=inventory_list:
|
||||||
|
get:
|
||||||
|
summary: List all inventory items
|
||||||
|
tags: [Inventory]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Inventory items grouped by product
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/InventoryItem"
|
||||||
|
|
||||||
|
/index.php?action=inventory_add:
|
||||||
|
post:
|
||||||
|
summary: Add item to inventory
|
||||||
|
tags: [Inventory]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [product_id, quantity, location]
|
||||||
|
properties:
|
||||||
|
product_id:
|
||||||
|
type: integer
|
||||||
|
quantity:
|
||||||
|
type: number
|
||||||
|
location:
|
||||||
|
type: string
|
||||||
|
enum: [dispensa, frigo, freezer, altro]
|
||||||
|
expiry_date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
conf_size:
|
||||||
|
type: number
|
||||||
|
vacuum:
|
||||||
|
type: boolean
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Item added to inventory
|
||||||
|
|
||||||
|
/index.php?action=inventory_use:
|
||||||
|
post:
|
||||||
|
summary: Use/consume items from inventory
|
||||||
|
tags: [Inventory]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [product_id, quantity, location]
|
||||||
|
properties:
|
||||||
|
product_id:
|
||||||
|
type: integer
|
||||||
|
quantity:
|
||||||
|
type: number
|
||||||
|
location:
|
||||||
|
type: string
|
||||||
|
use_all:
|
||||||
|
type: boolean
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Item consumed
|
||||||
|
|
||||||
|
/index.php?action=inventory_update:
|
||||||
|
post:
|
||||||
|
summary: Update an inventory entry
|
||||||
|
tags: [Inventory]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [id]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
quantity:
|
||||||
|
type: number
|
||||||
|
location:
|
||||||
|
type: string
|
||||||
|
expiry_date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Inventory entry updated
|
||||||
|
|
||||||
|
/index.php?action=inventory_delete:
|
||||||
|
post:
|
||||||
|
summary: Remove an inventory entry
|
||||||
|
tags: [Inventory]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [id]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Inventory entry removed
|
||||||
|
|
||||||
|
/index.php?action=inventory_summary:
|
||||||
|
get:
|
||||||
|
summary: Get inventory summary (counts per location)
|
||||||
|
tags: [Inventory]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Summary object with counts
|
||||||
|
|
||||||
|
/index.php?action=transactions_list:
|
||||||
|
get:
|
||||||
|
summary: List operations log
|
||||||
|
tags: [Log]
|
||||||
|
parameters:
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 50
|
||||||
|
- name: offset
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 0
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Array of transactions
|
||||||
|
|
||||||
|
/index.php?action=stats:
|
||||||
|
get:
|
||||||
|
summary: Get waste/consumption statistics
|
||||||
|
tags: [Log]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Statistics for the last 30 days
|
||||||
|
|
||||||
|
/index.php?action=gemini_expiry:
|
||||||
|
post:
|
||||||
|
summary: Use AI to read expiry date from image
|
||||||
|
tags: [AI / Gemini]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
image:
|
||||||
|
type: string
|
||||||
|
description: Base64-encoded image
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Parsed expiry date
|
||||||
|
|
||||||
|
/index.php?action=generate_recipe:
|
||||||
|
post:
|
||||||
|
summary: Generate a recipe based on available ingredients
|
||||||
|
tags: [AI / Gemini]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
persons:
|
||||||
|
type: integer
|
||||||
|
preferences:
|
||||||
|
type: object
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Generated recipe
|
||||||
|
|
||||||
|
/index.php?action=gemini_identify:
|
||||||
|
post:
|
||||||
|
summary: Identify a product from photo using AI
|
||||||
|
tags: [AI / Gemini]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
image:
|
||||||
|
type: string
|
||||||
|
description: Base64-encoded image
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Identified product data
|
||||||
|
|
||||||
|
/index.php?action=gemini_chat:
|
||||||
|
post:
|
||||||
|
summary: Chat with Gemini AI kitchen assistant
|
||||||
|
tags: [AI / Gemini]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
history:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: AI response
|
||||||
|
|
||||||
|
/index.php?action=bring_list:
|
||||||
|
get:
|
||||||
|
summary: Get Bring! shopping list
|
||||||
|
tags: [Bring! Integration]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Shopping list items
|
||||||
|
|
||||||
|
/index.php?action=bring_add:
|
||||||
|
post:
|
||||||
|
summary: Add item to Bring! shopping list
|
||||||
|
tags: [Bring! Integration]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
spec:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Item added
|
||||||
|
|
||||||
|
/index.php?action=bring_remove:
|
||||||
|
post:
|
||||||
|
summary: Remove item from Bring! shopping list
|
||||||
|
tags: [Bring! Integration]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Item removed
|
||||||
|
|
||||||
|
/index.php?action=bring_suggest:
|
||||||
|
post:
|
||||||
|
summary: Get AI shopping suggestions
|
||||||
|
tags: [Bring! Integration]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: AI-generated shopping suggestions
|
||||||
|
|
||||||
|
/index.php?action=smart_shopping:
|
||||||
|
get:
|
||||||
|
summary: Get smart shopping predictions
|
||||||
|
tags: [Bring! Integration]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Predicted shopping needs
|
||||||
|
|
||||||
|
/index.php?action=save_settings:
|
||||||
|
post:
|
||||||
|
summary: Save server-side settings (.env values)
|
||||||
|
tags: [Settings]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Settings saved
|
||||||
|
|
||||||
|
/index.php?action=get_settings:
|
||||||
|
get:
|
||||||
|
summary: Get server settings (masked passwords)
|
||||||
|
tags: [Settings]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Current settings
|
||||||
|
|
||||||
|
/index.php?action=app_settings_get:
|
||||||
|
get:
|
||||||
|
summary: Get application settings from database
|
||||||
|
tags: [Settings]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: App settings object
|
||||||
|
|
||||||
|
/index.php?action=app_settings_save:
|
||||||
|
post:
|
||||||
|
summary: Save application settings to database
|
||||||
|
tags: [Settings]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: App settings saved
|
||||||
|
|
||||||
|
/index.php?action=recipes_list:
|
||||||
|
get:
|
||||||
|
summary: List saved recipes
|
||||||
|
tags: [Recipes]
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Array of saved recipes
|
||||||
|
|
||||||
|
/index.php?action=recipes_save:
|
||||||
|
post:
|
||||||
|
summary: Save a recipe
|
||||||
|
tags: [Recipes]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Recipe saved
|
||||||
|
|
||||||
|
/index.php?action=recipes_delete:
|
||||||
|
post:
|
||||||
|
summary: Delete a recipe
|
||||||
|
tags: [Recipes]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Recipe deleted
|
||||||
|
|
||||||
|
/index.php?action=dupliclick_login:
|
||||||
|
post:
|
||||||
|
summary: Login to DupliClick (online shopping)
|
||||||
|
tags: [DupliClick]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Login successful
|
||||||
|
|
||||||
|
/index.php?action=dupliclick_search:
|
||||||
|
get:
|
||||||
|
summary: Search DupliClick product catalog
|
||||||
|
tags: [DupliClick]
|
||||||
|
parameters:
|
||||||
|
- name: q
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Search results
|
||||||
|
|
||||||
|
/index.php?action=tts_proxy:
|
||||||
|
post:
|
||||||
|
summary: Proxy TTS request to external endpoint
|
||||||
|
tags: [TTS]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
text:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: TTS request sent
|
||||||
|
|
||||||
|
/index.php?action=expiry_history:
|
||||||
|
get:
|
||||||
|
summary: Get expiry scan history for a product
|
||||||
|
tags: [Inventory]
|
||||||
|
parameters:
|
||||||
|
- name: product_id
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Expiry history entries
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
Product:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
brand:
|
||||||
|
type: string
|
||||||
|
barcode:
|
||||||
|
type: string
|
||||||
|
category:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- latticini
|
||||||
|
- carne
|
||||||
|
- pesce
|
||||||
|
- frutta
|
||||||
|
- verdura
|
||||||
|
- pasta
|
||||||
|
- pane
|
||||||
|
- surgelati
|
||||||
|
- bevande
|
||||||
|
- condimenti
|
||||||
|
- snack
|
||||||
|
- conserve
|
||||||
|
- cereali
|
||||||
|
- igiene
|
||||||
|
- pulizia
|
||||||
|
- altro
|
||||||
|
unit:
|
||||||
|
type: string
|
||||||
|
enum: [pz, conf, g, ml]
|
||||||
|
default_quantity:
|
||||||
|
type: number
|
||||||
|
package_size:
|
||||||
|
type: number
|
||||||
|
package_unit:
|
||||||
|
type: string
|
||||||
|
notes:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
ProductInput:
|
||||||
|
type: object
|
||||||
|
required: [name]
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
description: If provided, updates existing product
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
brand:
|
||||||
|
type: string
|
||||||
|
barcode:
|
||||||
|
type: string
|
||||||
|
category:
|
||||||
|
type: string
|
||||||
|
unit:
|
||||||
|
type: string
|
||||||
|
default_quantity:
|
||||||
|
type: number
|
||||||
|
package_size:
|
||||||
|
type: number
|
||||||
|
|
||||||
|
InventoryItem:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
product_id:
|
||||||
|
type: integer
|
||||||
|
product_name:
|
||||||
|
type: string
|
||||||
|
quantity:
|
||||||
|
type: number
|
||||||
|
location:
|
||||||
|
type: string
|
||||||
|
expiry_date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
opened:
|
||||||
|
type: boolean
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
|
responses:
|
||||||
|
TooManyRequests:
|
||||||
|
description: Rate limit exceeded
|
||||||
|
headers:
|
||||||
|
Retry-After:
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Products
|
||||||
|
description: Product catalog management
|
||||||
|
- name: Inventory
|
||||||
|
description: Inventory tracking (stock in/out)
|
||||||
|
- name: Log
|
||||||
|
description: Operations log and statistics
|
||||||
|
- name: AI / Gemini
|
||||||
|
description: Google Gemini AI integration (identification, recipes, chat)
|
||||||
|
- name: Bring! Integration
|
||||||
|
description: Bring! shopping list integration
|
||||||
|
- name: Recipes
|
||||||
|
description: Recipe storage
|
||||||
|
- name: Settings
|
||||||
|
description: Application and server settings
|
||||||
|
- name: DupliClick
|
||||||
|
description: DupliClick online shopping integration
|
||||||
|
- name: TTS
|
||||||
|
description: Text-to-Speech proxy
|
||||||
+94
-65
@@ -20,12 +20,12 @@
|
|||||||
<!-- Top Header -->
|
<!-- Top Header -->
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1 class="header-title" onclick="showPage('dashboard')">🏠 Dispensa</h1>
|
<h1 class="header-title" onclick="showPage('dashboard')" data-i18n="nav.title">🏠 Dispensa</h1>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<button class="header-scan-btn header-gemini-btn" onclick="showPage('chat')" title="Chat con Gemini">
|
<button class="header-scan-btn header-gemini-btn" onclick="showPage('chat')" title="Chat con Gemini" data-i18n-title="chat.title">
|
||||||
<svg class="gemini-icon" viewBox="0 0 24 24" width="28" height="28" fill="white"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
<svg class="gemini-icon" viewBox="0 0 24 24" width="28" height="28" fill="white"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="header-scan-btn" id="btn-header-scan" title="Scansiona prodotto (tieni premuto per modalità spesa)">
|
<button class="header-scan-btn" id="btn-header-scan" title="Scansiona prodotto (tieni premuto per modalità spesa)" data-i18n-title="scan.hint">
|
||||||
📷
|
📷
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,22 +41,22 @@
|
|||||||
<div class="stat-card" onclick="showPage('inventory', 'dispensa')">
|
<div class="stat-card" onclick="showPage('inventory', 'dispensa')">
|
||||||
<span class="stat-icon">🗄️</span>
|
<span class="stat-icon">🗄️</span>
|
||||||
<span class="stat-value" id="stat-dispensa">0</span>
|
<span class="stat-value" id="stat-dispensa">0</span>
|
||||||
<span class="stat-label">Dispensa</span>
|
<span class="stat-label" data-i18n="locations.dispensa">Dispensa</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" onclick="showPage('inventory', 'frigo')">
|
<div class="stat-card" onclick="showPage('inventory', 'frigo')">
|
||||||
<span class="stat-icon">🧊</span>
|
<span class="stat-icon">🧊</span>
|
||||||
<span class="stat-value" id="stat-frigo">0</span>
|
<span class="stat-value" id="stat-frigo">0</span>
|
||||||
<span class="stat-label">Frigo</span>
|
<span class="stat-label" data-i18n="locations.frigo">Frigo</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" onclick="showPage('inventory', 'freezer')">
|
<div class="stat-card" onclick="showPage('inventory', 'freezer')">
|
||||||
<span class="stat-icon">❄️</span>
|
<span class="stat-icon">❄️</span>
|
||||||
<span class="stat-value" id="stat-freezer">0</span>
|
<span class="stat-value" id="stat-freezer">0</span>
|
||||||
<span class="stat-label">Freezer</span>
|
<span class="stat-label" data-i18n="locations.freezer">Freezer</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" onclick="showPage('shopping')">
|
<div class="stat-card" onclick="showPage('shopping')">
|
||||||
<span class="stat-icon">🛒</span>
|
<span class="stat-icon">🛒</span>
|
||||||
<span class="stat-value" id="stat-spesa">-</span>
|
<span class="stat-value" id="stat-spesa">-</span>
|
||||||
<span class="stat-label">Spesa</span>
|
<span class="stat-label" data-i18n="nav.shopping">Spesa</span>
|
||||||
<span class="stat-urgent" id="stat-urgent" style="display:none"></span>
|
<span class="stat-urgent" id="stat-urgent" style="display:none"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,39 +65,39 @@
|
|||||||
<div class="quick-recipe-bar" id="quick-recipe-bar" style="display:none">
|
<div class="quick-recipe-bar" id="quick-recipe-bar" style="display:none">
|
||||||
<button class="btn-quick-recipe" onclick="quickRecipeSuggestion()">
|
<button class="btn-quick-recipe" onclick="quickRecipeSuggestion()">
|
||||||
<span>🍳</span>
|
<span>🍳</span>
|
||||||
<span class="quick-recipe-text">Ricetta veloce con prodotti in scadenza</span>
|
<span class="quick-recipe-text" data-i18n="dashboard.quick_recipe">🍳 Ricetta veloce con prodotti in scadenza</span>
|
||||||
<span>→</span>
|
<span>→</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alert for expired items (on top) -->
|
<!-- Alert for expired items (on top) -->
|
||||||
<div class="alert-section alert-danger" id="alert-expired" style="display:none">
|
<div class="alert-section alert-danger" id="alert-expired" style="display:none">
|
||||||
<h3>🚫 Scaduti</h3>
|
<h3 data-i18n="dashboard.expired_title">🚫 Scaduti</h3>
|
||||||
<div id="expired-list"></div>
|
<div id="expired-list"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Alert for soonest expiring items -->
|
<!-- Alert for soonest expiring items -->
|
||||||
<div class="alert-section" id="alert-expiring" style="display:none">
|
<div class="alert-section" id="alert-expiring" style="display:none">
|
||||||
<h3>⏰ Prossime Scadenze</h3>
|
<h3 data-i18n="dashboard.expiring_title">⏰ Prossime Scadenze</h3>
|
||||||
<div id="expiring-list"></div>
|
<div id="expiring-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Waste vs consumption mini chart -->
|
<!-- Waste vs consumption mini chart -->
|
||||||
<div class="waste-chart-section" id="waste-chart-section" style="display:none">
|
<div class="waste-chart-section" id="waste-chart-section" style="display:none">
|
||||||
<h3>📊 Ultimi 30 giorni</h3>
|
<h3 data-i18n="dashboard.stats_period">📊 Ultimi 30 giorni</h3>
|
||||||
<div class="waste-chart-bar" id="waste-chart-bar"></div>
|
<div class="waste-chart-bar" id="waste-chart-bar"></div>
|
||||||
<div class="waste-chart-legend" id="waste-chart-legend"></div>
|
<div class="waste-chart-legend" id="waste-chart-legend"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Opened (partially used) products -->
|
<!-- Opened (partially used) products -->
|
||||||
<div class="alert-section alert-opened" id="alert-opened" style="display:none">
|
<div class="alert-section alert-opened" id="alert-opened" style="display:none">
|
||||||
<h3>📦 Prodotti Aperti</h3>
|
<h3 data-i18n="dashboard.opened_title">📦 Prodotti Aperti</h3>
|
||||||
<div id="opened-list"></div>
|
<div id="opened-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Review suspicious quantities -->
|
<!-- Review suspicious quantities -->
|
||||||
<div class="alert-section alert-review" id="alert-review" style="display:none">
|
<div class="alert-section alert-review" id="alert-review" style="display:none">
|
||||||
<h3>🔍 Da revisionare</h3>
|
<h3 data-i18n="dashboard.review_title">🔍 Da revisionare</h3>
|
||||||
<p class="review-hint">Quantità che sembrano anomale. Conferma se corrette o modifica.</p>
|
<p class="review-hint" data-i18n="dashboard.review_hint">Quantità che sembrano anomale. Conferma se corrette o modifica.</p>
|
||||||
<div id="review-list"></div>
|
<div id="review-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,18 +106,18 @@
|
|||||||
<!-- ===== INVENTORY LIST ===== -->
|
<!-- ===== INVENTORY LIST ===== -->
|
||||||
<section class="page" id="page-inventory">
|
<section class="page" id="page-inventory">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<button class="back-btn" onclick="showPage('dashboard')">← Indietro</button>
|
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||||||
<h2 id="inventory-title">Dispensa</h2>
|
<h2 id="inventory-title" data-i18n="inventory.title">Dispensa</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="location-tabs" id="location-tabs">
|
<div class="location-tabs" id="location-tabs">
|
||||||
<button class="tab active" onclick="filterLocation('')" data-loc="">Tutti</button>
|
<button class="tab active" onclick="filterLocation('')" data-loc="" data-i18n="inventory.filter_all">Tutti</button>
|
||||||
<button class="tab" onclick="filterLocation('dispensa')" data-loc="dispensa">🗄️ Dispensa</button>
|
<button class="tab" onclick="filterLocation('dispensa')" data-loc="dispensa">🗄️ Dispensa</button>
|
||||||
<button class="tab" onclick="filterLocation('frigo')" data-loc="frigo">🧊 Frigo</button>
|
<button class="tab" onclick="filterLocation('frigo')" data-loc="frigo">🧊 Frigo</button>
|
||||||
<button class="tab" onclick="filterLocation('freezer')" data-loc="freezer">❄️ Freezer</button>
|
<button class="tab" onclick="filterLocation('freezer')" data-loc="freezer">❄️ Freezer</button>
|
||||||
<button class="tab" onclick="filterLocation('altro')" data-loc="altro">📦 Altro</button>
|
<button class="tab" onclick="filterLocation('altro')" data-loc="altro">📦 Altro</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<input type="text" id="inventory-search" placeholder="🔍 Cerca prodotto..." oninput="filterInventory()">
|
<input type="text" id="inventory-search" placeholder="🔍 Cerca prodotto..." oninput="filterInventory()" data-i18n-placeholder="inventory.search_placeholder">
|
||||||
</div>
|
</div>
|
||||||
<div class="inventory-list" id="inventory-list"></div>
|
<div class="inventory-list" id="inventory-list"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -125,15 +125,15 @@
|
|||||||
<!-- ===== SCAN PAGE ===== -->
|
<!-- ===== SCAN PAGE ===== -->
|
||||||
<section class="page" id="page-scan">
|
<section class="page" id="page-scan">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<button class="back-btn" onclick="stopScanner(); showPage('dashboard')">← Indietro</button>
|
<button class="back-btn" onclick="stopScanner(); showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||||||
<h2>Scansiona Prodotto</h2>
|
<h2 data-i18n="scan.title">Scansiona Prodotto</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="spesa-mode-banner" id="spesa-mode-banner" style="display:none">
|
<div class="spesa-mode-banner" id="spesa-mode-banner" style="display:none">
|
||||||
<div class="spesa-banner-left">
|
<div class="spesa-banner-left">
|
||||||
<span>🛒 Modalità Spesa</span>
|
<span data-i18n="scan.mode_shopping">🛒 Modalità Spesa</span>
|
||||||
<span class="spesa-stat"></span>
|
<span class="spesa-stat"></span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-small" onclick="endSpesaMode()">✅ Fine spesa</button>
|
<button class="btn btn-small" onclick="endSpesaMode()" data-i18n="scan.mode_shopping_end">✅ Fine spesa</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="scan-container">
|
<div class="scan-container">
|
||||||
<div class="scanner-viewport" id="scanner-viewport">
|
<div class="scanner-viewport" id="scanner-viewport">
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
<div class="scan-result" id="scan-result" style="display:none"></div>
|
<div class="scan-result" id="scan-result" style="display:none"></div>
|
||||||
<div class="barcode-manual-entry">
|
<div class="barcode-manual-entry">
|
||||||
<div class="barcode-input-row">
|
<div class="barcode-input-row">
|
||||||
<input type="text" id="manual-barcode-input" class="form-input" placeholder="Inserisci codice a barre..." inputmode="numeric" pattern="[0-9]*" onkeydown="if(event.key==='Enter')submitManualBarcode()">
|
<input type="text" id="manual-barcode-input" class="form-input" placeholder="Inserisci codice a barre..." inputmode="numeric" pattern="[0-9]*" onkeydown="if(event.key==='Enter')submitManualBarcode()" data-i18n-placeholder="scan.barcode_placeholder">
|
||||||
<button class="btn btn-primary" onclick="submitManualBarcode()">🔍 Cerca</button>
|
<button class="btn btn-primary" onclick="submitManualBarcode()">🔍 Cerca</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -504,11 +504,11 @@
|
|||||||
<!-- ===== RECIPE PAGE ===== -->
|
<!-- ===== RECIPE PAGE ===== -->
|
||||||
<section class="page" id="page-recipe">
|
<section class="page" id="page-recipe">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<button class="back-btn" onclick="showPage('dashboard')">← Indietro</button>
|
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||||||
<h2>🍳 Ricette</h2>
|
<h2 data-i18n="recipes.title">🍳 Ricette</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="recipe-page-container">
|
<div class="recipe-page-container">
|
||||||
<button class="btn btn-large btn-success full-width" onclick="openRecipeDialog()">
|
<button class="btn btn-large btn-success full-width" onclick="openRecipeDialog()" data-i18n="recipes.generate">
|
||||||
✨ Genera nuova ricetta
|
✨ Genera nuova ricetta
|
||||||
</button>
|
</button>
|
||||||
<div id="recipe-archive" class="recipe-archive"></div>
|
<div id="recipe-archive" class="recipe-archive"></div>
|
||||||
@@ -518,12 +518,12 @@
|
|||||||
<!-- ===== SHOPPING LIST (BRING!) PAGE ===== -->
|
<!-- ===== SHOPPING LIST (BRING!) PAGE ===== -->
|
||||||
<section class="page" id="page-shopping">
|
<section class="page" id="page-shopping">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<button class="back-btn" onclick="showPage('dashboard')">← Indietro</button>
|
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||||||
<h2>🛒 Lista della Spesa</h2>
|
<h2 data-i18n="shopping.title">🛒 Lista della Spesa</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="shopping-container">
|
<div class="shopping-container">
|
||||||
<div class="bring-status" id="bring-status">
|
<div class="bring-status" id="bring-status">
|
||||||
<div class="bring-loading">Connessione a Bring!...</div>
|
<div class="bring-loading" data-i18n="shopping.bring_loading">Connessione a Bring!...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab navigation -->
|
<!-- Tab navigation -->
|
||||||
@@ -541,7 +541,7 @@
|
|||||||
<!-- Price total banner -->
|
<!-- Price total banner -->
|
||||||
<div class="spesa-total-banner" id="spesa-total-banner" style="display:none">
|
<div class="spesa-total-banner" id="spesa-total-banner" style="display:none">
|
||||||
<div class="spesa-total-row">
|
<div class="spesa-total-row">
|
||||||
<span class="spesa-total-label">💰 Totale stimato</span>
|
<span class="spesa-total-label" data-i18n="shopping.total_label">💰 Totale stimato</span>
|
||||||
<span class="spesa-total-value" id="spesa-total-value">€ 0,00</span>
|
<span class="spesa-total-value" id="spesa-total-value">€ 0,00</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="spesa-total-detail" id="spesa-total-detail"></div>
|
<div class="spesa-total-detail" id="spesa-total-detail"></div>
|
||||||
@@ -642,10 +642,10 @@
|
|||||||
<!-- Log Page -->
|
<!-- Log Page -->
|
||||||
<section id="page-log" class="page">
|
<section id="page-log" class="page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>📒 Log Operazioni</h2>
|
<h2 data-i18n="log.title">📒 Log Operazioni</h2>
|
||||||
</div>
|
</div>
|
||||||
<div id="log-list" class="log-list"></div>
|
<div id="log-list" class="log-list"></div>
|
||||||
<button class="btn btn-secondary full-width mt-2" id="log-load-more" style="display:none" onclick="loadLog(true)">
|
<button class="btn btn-secondary full-width mt-2" id="log-load-more" style="display:none" onclick="loadLog(true)" data-i18n="btn.load_more">
|
||||||
Carica altri...
|
Carica altri...
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
@@ -653,8 +653,8 @@
|
|||||||
<!-- ===== SETTINGS PAGE ===== -->
|
<!-- ===== SETTINGS PAGE ===== -->
|
||||||
<section class="page" id="page-settings">
|
<section class="page" id="page-settings">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<button class="back-btn" onclick="showPage('dashboard')">← Indietro</button>
|
<button class="back-btn" onclick="showPage('dashboard')" data-i18n="btn.back">← Indietro</button>
|
||||||
<h2>⚙️ Configurazione</h2>
|
<h2 data-i18n="settings.title">⚙️ Configurazione</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-tabs">
|
<div class="settings-tabs">
|
||||||
<button class="settings-tab active" onclick="switchSettingsTab(this, 'tab-api')" data-tab="tab-api" title="API Keys">🔑</button>
|
<button class="settings-tab active" onclick="switchSettingsTab(this, 'tab-api')" data-tab="tab-api" title="API Keys">🔑</button>
|
||||||
@@ -665,14 +665,15 @@
|
|||||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-spesa')" data-tab="tab-spesa" title="Spesa Online">🛍️</button>
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-spesa')" data-tab="tab-spesa" title="Spesa Online">🛍️</button>
|
||||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</button>
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-camera')" data-tab="tab-camera" title="Fotocamera">📷</button>
|
||||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-security')" data-tab="tab-security" title="Sicurezza">🔒</button>
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-security')" data-tab="tab-security" title="Sicurezza">🔒</button>
|
||||||
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-tts')" data-tab="tab-tts" title="Voce (TTS)">🔊</button>
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-tts')" data-tab="tab-tts" title="Voce (TTS)" data-i18n-title="settings.tab_tts">🔊</button>
|
||||||
|
<button class="settings-tab" onclick="switchSettingsTab(this, 'tab-language')" data-tab="tab-language" title="Lingua" data-i18n-title="settings.tab_language">🌐</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-panels">
|
<div class="settings-panels">
|
||||||
<!-- API Keys Tab -->
|
<!-- API Keys Tab -->
|
||||||
<div class="settings-panel active" id="tab-api">
|
<div class="settings-panel active" id="tab-api">
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4>🤖 Google Gemini AI</h4>
|
<h4 data-i18n="settings.gemini.title">🤖 Google Gemini AI</h4>
|
||||||
<p class="settings-hint">Chiave API per identificazione prodotti, scadenze e ricette.</p>
|
<p class="settings-hint" data-i18n="settings.gemini.hint">Chiave API per identificazione prodotti, scadenze e ricette.</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>API Key Gemini</label>
|
<label>API Key Gemini</label>
|
||||||
<input type="password" id="setting-gemini-key" class="form-input" placeholder="AIza...">
|
<input type="password" id="setting-gemini-key" class="form-input" placeholder="AIza...">
|
||||||
@@ -683,8 +684,8 @@
|
|||||||
<!-- Bring! Tab -->
|
<!-- Bring! Tab -->
|
||||||
<div class="settings-panel" id="tab-bring">
|
<div class="settings-panel" id="tab-bring">
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4>🛒 Bring! Shopping List</h4>
|
<h4 data-i18n="settings.bring.title">🛒 Bring! Shopping List</h4>
|
||||||
<p class="settings-hint">Credenziali per l'integrazione con la lista della spesa Bring!</p>
|
<p class="settings-hint" data-i18n="settings.bring.hint">Credenziali per l'integrazione con la lista della spesa Bring!</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>📧 Email Bring!</label>
|
<label>📧 Email Bring!</label>
|
||||||
<input type="email" id="setting-bring-email" class="form-input" placeholder="email@esempio.com">
|
<input type="email" id="setting-bring-email" class="form-input" placeholder="email@esempio.com">
|
||||||
@@ -699,8 +700,8 @@
|
|||||||
<!-- Recipe Tab -->
|
<!-- Recipe Tab -->
|
||||||
<div class="settings-panel" id="tab-recipe">
|
<div class="settings-panel" id="tab-recipe">
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4>🍳 Preferenze Ricette</h4>
|
<h4 data-i18n="settings.recipe.title">🍳 Preferenze Ricette</h4>
|
||||||
<p class="settings-hint">Configura le opzioni predefinite per la generazione delle ricette.</p>
|
<p class="settings-hint" data-i18n="settings.recipe.hint">Configura le opzioni predefinite per la generazione delle ricette.</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>👥 Persone predefinite</label>
|
<label>👥 Persone predefinite</label>
|
||||||
<div class="qty-control">
|
<div class="qty-control">
|
||||||
@@ -729,8 +730,8 @@
|
|||||||
<!-- Weekly Meal Plan Tab -->
|
<!-- Weekly Meal Plan Tab -->
|
||||||
<div class="settings-panel" id="tab-mealplan">
|
<div class="settings-panel" id="tab-mealplan">
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4>📅 Piano Pasti Settimanale</h4>
|
<h4 data-i18n="settings.mealplan.title">📅 Piano Pasti Settimanale</h4>
|
||||||
<p class="settings-hint">Imposta la tipologia di pasto per ogni giorno. Sarà usata come guida nella generazione delle ricette.</p>
|
<p class="settings-hint" data-i18n="settings.mealplan.hint">Imposta la tipologia di pasto per ogni giorno. Sarà usata come guida nella generazione delle ricette.</p>
|
||||||
<div class="form-group" style="margin-bottom:10px">
|
<div class="form-group" style="margin-bottom:10px">
|
||||||
<label class="toggle-row">
|
<label class="toggle-row">
|
||||||
<span>✅ Attiva piano pasti settimanale</span>
|
<span>✅ Attiva piano pasti settimanale</span>
|
||||||
@@ -759,8 +760,8 @@
|
|||||||
<!-- Appliances Tab -->
|
<!-- Appliances Tab -->
|
||||||
<div class="settings-panel" id="tab-appliances">
|
<div class="settings-panel" id="tab-appliances">
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4>🔌 Elettrodomestici Disponibili</h4>
|
<h4 data-i18n="settings.appliances.title">🔌 Elettrodomestici Disponibili</h4>
|
||||||
<p class="settings-hint">Indica gli elettrodomestici che hai a disposizione. Saranno considerati nella generazione delle ricette.</p>
|
<p class="settings-hint" data-i18n="settings.appliances.hint">Indica gli elettrodomestici che hai a disposizione. Saranno considerati nella generazione delle ricette.</p>
|
||||||
<div class="appliances-list" id="appliances-list"></div>
|
<div class="appliances-list" id="appliances-list"></div>
|
||||||
<div class="form-group mt-2">
|
<div class="form-group mt-2">
|
||||||
<div class="barcode-input-row">
|
<div class="barcode-input-row">
|
||||||
@@ -826,8 +827,8 @@
|
|||||||
<!-- Camera Tab -->
|
<!-- Camera Tab -->
|
||||||
<div class="settings-panel" id="tab-camera">
|
<div class="settings-panel" id="tab-camera">
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4>📷 Fotocamera</h4>
|
<h4 data-i18n="settings.camera.title">📷 Fotocamera</h4>
|
||||||
<p class="settings-hint">Scegli quale fotocamera utilizzare per la scansione barcode e l'identificazione AI.</p>
|
<p class="settings-hint" data-i18n="settings.camera.hint">Scegli quale fotocamera utilizzare per la scansione barcode e l'identificazione AI.</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>📸 Fotocamera predefinita</label>
|
<label>📸 Fotocamera predefinita</label>
|
||||||
<select id="setting-camera-facing" class="form-input">
|
<select id="setting-camera-facing" class="form-input">
|
||||||
@@ -866,8 +867,8 @@
|
|||||||
<!-- TTS Tab -->
|
<!-- TTS Tab -->
|
||||||
<div class="settings-panel" id="tab-tts">
|
<div class="settings-panel" id="tab-tts">
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h4>🔊 Voce & TTS</h4>
|
<h4 data-i18n="settings.tts.title">🔊 Voce & TTS</h4>
|
||||||
<p class="settings-hint">Configura la sintesi vocale tramite qualsiasi API REST esterna. I passi della ricetta e i timer scaduti verranno inviati all'endpoint configurato.</p>
|
<p class="settings-hint" data-i18n="settings.tts.hint">Configura la sintesi vocale tramite qualsiasi API REST esterna. I passi della ricetta e i timer scaduti verranno inviati all'endpoint configurato.</p>
|
||||||
<div class="form-group" style="margin-bottom:10px">
|
<div class="form-group" style="margin-bottom:10px">
|
||||||
<label class="toggle-row">
|
<label class="toggle-row">
|
||||||
<span>✅ Attiva TTS</span>
|
<span>✅ Attiva TTS</span>
|
||||||
@@ -935,8 +936,21 @@
|
|||||||
<div id="tts-test-status" style="display:none;margin-top:8px"></div>
|
<div id="tts-test-status" style="display:none;margin-top:8px"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Language Tab -->
|
||||||
|
<div class="settings-panel" id="tab-language">
|
||||||
|
<div class="settings-card">
|
||||||
|
<h4 data-i18n="settings.language.title">🌐 Lingua / Language</h4>
|
||||||
|
<p class="settings-hint" data-i18n="settings.language.hint">Seleziona la lingua dell'interfaccia. Select the interface language.</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label data-i18n="settings.language.label">🌐 Lingua</label>
|
||||||
|
<select id="setting-language" class="form-input" onchange="changeLanguage(this.value)">
|
||||||
|
</select>
|
||||||
|
<p class="settings-hint mt-2" data-i18n="settings.language.restart_notice">La pagina verrà ricaricata per applicare la nuova lingua.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-large btn-success full-width mt-2" onclick="saveSettings()">💾 Salva Configurazione</button>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-large btn-success full-width mt-2" onclick="saveSettings()" data-i18n="btn.save_config">💾 Salva Configurazione</button>
|
||||||
<div id="settings-status" class="settings-status" style="display:none"></div>
|
<div id="settings-status" class="settings-status" style="display:none"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -946,25 +960,25 @@
|
|||||||
<div class="chat-header-bar">
|
<div class="chat-header-bar">
|
||||||
<div class="chat-header-info">
|
<div class="chat-header-info">
|
||||||
<svg class="gemini-icon-sm" viewBox="0 0 24 24" width="22" height="22" fill="#6366f1"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
<svg class="gemini-icon-sm" viewBox="0 0 24 24" width="22" height="22" fill="#6366f1"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
||||||
<span class="chat-title">Gemini Chef</span>
|
<span class="chat-title" data-i18n="chat.title">Gemini Chef</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-chat-clear" onclick="clearChat()" title="Nuova conversazione">🗑️</button>
|
<button class="btn-chat-clear" onclick="clearChat()" title="Nuova conversazione" data-i18n-title="chat.clear">🗑️</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-messages" id="chat-messages">
|
<div class="chat-messages" id="chat-messages">
|
||||||
<div class="chat-welcome">
|
<div class="chat-welcome">
|
||||||
<svg class="gemini-icon-lg" viewBox="0 0 24 24" width="48" height="48" fill="#6366f1"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
<svg class="gemini-icon-lg" viewBox="0 0 24 24" width="48" height="48" fill="#6366f1"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
|
||||||
<h3>Ciao! Sono il tuo assistente cucina</h3>
|
<h3 data-i18n="chat.welcome">Ciao! Sono il tuo assistente cucina</h3>
|
||||||
<p>Chiedimi di prepararti un succo, uno spuntino, un piatto veloce... Conosco la tua dispensa, i tuoi elettrodomestici e le tue preferenze!</p>
|
<p data-i18n="chat.welcome_desc">Chiedimi di prepararti un succo, uno spuntino, un piatto veloce... Conosco la tua dispensa, i tuoi elettrodomestici e le tue preferenze!</p>
|
||||||
<div class="chat-suggestions">
|
<div class="chat-suggestions">
|
||||||
<button class="chat-suggestion" onclick="sendChatSuggestion('Cosa posso preparare per uno spuntino veloce?')">🍿 Spuntino veloce</button>
|
<button class="chat-suggestion" onclick="sendChatSuggestion('Cosa posso preparare per uno spuntino veloce?')" data-i18n="chat.suggestion_snack">🍿 Spuntino veloce</button>
|
||||||
<button class="chat-suggestion" onclick="sendChatSuggestion('Fammi un succo o frullato con quello che ho')">🥤 Succo/Frullato</button>
|
<button class="chat-suggestion" onclick="sendChatSuggestion('Fammi un succo o frullato con quello che ho')" data-i18n="chat.suggestion_juice">🥤 Succo/Frullato</button>
|
||||||
<button class="chat-suggestion" onclick="sendChatSuggestion('Ho fame ma voglio qualcosa di leggero')">🥗 Qualcosa di leggero</button>
|
<button class="chat-suggestion" onclick="sendChatSuggestion('Ho fame ma voglio qualcosa di leggero')" data-i18n="chat.suggestion_light">🥗 Qualcosa di leggero</button>
|
||||||
<button class="chat-suggestion" onclick="sendChatSuggestion('Cosa sta per scadere e come posso usarlo?')">⏰ Usa le scadenze</button>
|
<button class="chat-suggestion" onclick="sendChatSuggestion('Cosa sta per scadere e come posso usarlo?')" data-i18n="chat.suggestion_expiry">⏰ Usa le scadenze</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input-bar">
|
<div class="chat-input-bar">
|
||||||
<input type="text" id="chat-input" class="chat-input" placeholder="Chiedi qualcosa..." onkeydown="if(event.key==='Enter')sendChatMessage()">
|
<input type="text" id="chat-input" class="chat-input" placeholder="Chiedi qualcosa..." onkeydown="if(event.key==='Enter')sendChatMessage()" data-i18n-placeholder="chat.placeholder">
|
||||||
<button class="btn-chat-send" id="btn-chat-send" onclick="sendChatMessage()">
|
<button class="btn-chat-send" id="btn-chat-send" onclick="sendChatMessage()">
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="white"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="white"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -978,23 +992,23 @@
|
|||||||
<nav class="bottom-nav">
|
<nav class="bottom-nav">
|
||||||
<button class="nav-btn" onclick="showPage('dashboard')" data-page="dashboard">
|
<button class="nav-btn" onclick="showPage('dashboard')" data-page="dashboard">
|
||||||
<span class="nav-icon">🏠</span>
|
<span class="nav-icon">🏠</span>
|
||||||
<span class="nav-label">Home</span>
|
<span class="nav-label" data-i18n="nav.home">Home</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" onclick="showPage('inventory', '')" data-page="inventory">
|
<button class="nav-btn" onclick="showPage('inventory', '')" data-page="inventory">
|
||||||
<span class="nav-icon">📋</span>
|
<span class="nav-icon">📋</span>
|
||||||
<span class="nav-label">Dispensa</span>
|
<span class="nav-label" data-i18n="nav.inventory">Dispensa</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" onclick="showPage('recipe')" data-page="recipe">
|
<button class="nav-btn" onclick="showPage('recipe')" data-page="recipe">
|
||||||
<span class="nav-icon">🍳</span>
|
<span class="nav-icon">🍳</span>
|
||||||
<span class="nav-label">Ricette</span>
|
<span class="nav-label" data-i18n="nav.recipes">Ricette</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" onclick="showPage('shopping')" data-page="shopping">
|
<button class="nav-btn" onclick="showPage('shopping')" data-page="shopping">
|
||||||
<span class="nav-icon">🛒</span>
|
<span class="nav-icon">🛒</span>
|
||||||
<span class="nav-label">Spesa</span>
|
<span class="nav-label" data-i18n="nav.shopping">Spesa</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" onclick="showPage('log')" data-page="log">
|
<button class="nav-btn" onclick="showPage('log')" data-page="log">
|
||||||
<span class="nav-icon">📒</span>
|
<span class="nav-icon">📒</span>
|
||||||
<span class="nav-label">Log</span>
|
<span class="nav-label" data-i18n="nav.log">Log</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" onclick="showPage('settings')" data-page="settings">
|
<button class="nav-btn" onclick="showPage('settings')" data-page="settings">
|
||||||
<span class="nav-icon">⚙️</span>
|
<span class="nav-icon">⚙️</span>
|
||||||
@@ -1060,13 +1074,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Setup Wizard (first-run) -->
|
||||||
|
<div class="modal-overlay" id="setup-wizard" style="display:none">
|
||||||
|
<div class="modal-content setup-wizard-content" onclick="event.stopPropagation()">
|
||||||
|
<div class="setup-header">
|
||||||
|
<h2>🏠 Dispensa Manager</h2>
|
||||||
|
<div class="setup-progress" id="setup-progress"></div>
|
||||||
|
</div>
|
||||||
|
<div class="setup-body" id="setup-body"></div>
|
||||||
|
<div class="setup-footer">
|
||||||
|
<button class="btn btn-secondary" id="setup-prev" onclick="setupWizardNav(-1)" style="display:none">← Indietro</button>
|
||||||
|
<button class="btn btn-accent" id="setup-next" onclick="setupWizardNav(1)">Avanti →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Toast notification -->
|
<!-- Toast notification -->
|
||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<!-- Loading Overlay -->
|
<!-- Loading Overlay -->
|
||||||
<div class="loading-overlay" id="loading" style="display:none">
|
<div class="loading-overlay" id="loading" style="display:none">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
<p>Caricamento...</p>
|
<p data-i18n="app.loading">Caricamento...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal for product details from inventory -->
|
<!-- Modal for product details from inventory -->
|
||||||
|
|||||||
@@ -0,0 +1,431 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "Dispensa Manager",
|
||||||
|
"loading": "Laden..."
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"title": "🏠 Vorratskammer",
|
||||||
|
"home": "Home",
|
||||||
|
"inventory": "Vorrat",
|
||||||
|
"recipes": "Rezepte",
|
||||||
|
"shopping": "Einkauf",
|
||||||
|
"log": "Log"
|
||||||
|
},
|
||||||
|
"btn": {
|
||||||
|
"back": "← Zurück",
|
||||||
|
"save": "💾 Speichern",
|
||||||
|
"cancel": "✕ Abbrechen",
|
||||||
|
"close": "Schließen",
|
||||||
|
"add": "✅ Hinzufügen",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"edit": "✏️ Bearbeiten",
|
||||||
|
"search": "🔍 Suchen",
|
||||||
|
"go": "✅ Los",
|
||||||
|
"toggle_password": "👁️ Anzeigen/Ausblenden",
|
||||||
|
"load_more": "Mehr laden...",
|
||||||
|
"save_config": "💾 Konfiguration speichern",
|
||||||
|
"save_product": "💾 Produkt speichern",
|
||||||
|
"restart": "↺ Neustart",
|
||||||
|
"reset_default": "↺ Standard wiederherstellen"
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"dispensa": "Vorratskammer",
|
||||||
|
"frigo": "Kühlschrank",
|
||||||
|
"freezer": "Gefrierschrank",
|
||||||
|
"altro": "Sonstiges"
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"latticini": "Milchprodukte",
|
||||||
|
"carne": "Fleisch",
|
||||||
|
"pesce": "Fisch",
|
||||||
|
"frutta": "Obst",
|
||||||
|
"verdura": "Gemüse",
|
||||||
|
"pasta": "Pasta & Reis",
|
||||||
|
"pane": "Brot & Backwaren",
|
||||||
|
"surgelati": "Tiefkühl",
|
||||||
|
"bevande": "Getränke",
|
||||||
|
"condimenti": "Gewürze",
|
||||||
|
"snack": "Snacks & Süßes",
|
||||||
|
"conserve": "Konserven",
|
||||||
|
"cereali": "Getreide & Hülsenfrüchte",
|
||||||
|
"igiene": "Hygiene",
|
||||||
|
"pulizia": "Reinigung",
|
||||||
|
"altro": "Sonstiges",
|
||||||
|
"select": "-- Auswählen --"
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"pz": "Stk",
|
||||||
|
"conf": "Pkg",
|
||||||
|
"g": "g",
|
||||||
|
"ml": "ml",
|
||||||
|
"pieces": "Stück",
|
||||||
|
"grams": "Gramm",
|
||||||
|
"box": "Packung",
|
||||||
|
"boxes": "Packungen"
|
||||||
|
},
|
||||||
|
"shopping_sections": {
|
||||||
|
"frutta_verdura": "Obst & Gemüse",
|
||||||
|
"carne_pesce": "Fleisch & Fisch",
|
||||||
|
"latticini": "Milchprodukte & Frisches",
|
||||||
|
"pane_dolci": "Brot & Süßes",
|
||||||
|
"pasta": "Pasta & Getreide",
|
||||||
|
"conserve": "Konserven & Soßen",
|
||||||
|
"surgelati": "Tiefkühl",
|
||||||
|
"bevande": "Getränke",
|
||||||
|
"pulizia_igiene": "Reinigung & Hygiene",
|
||||||
|
"altro": "Sonstiges"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"expired_title": "🚫 Abgelaufen",
|
||||||
|
"expiring_title": "⏰ Bald ablaufend",
|
||||||
|
"stats_period": "📊 Letzte 30 Tage",
|
||||||
|
"opened_title": "📦 Geöffnete Produkte",
|
||||||
|
"review_title": "🔍 Zu prüfen",
|
||||||
|
"review_hint": "Mengen, die ungewöhnlich erscheinen. Bestätigen oder ändern.",
|
||||||
|
"quick_recipe": "🍳 Schnelles Rezept mit ablaufenden Produkten"
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"title": "Vorrat",
|
||||||
|
"filter_all": "Alle",
|
||||||
|
"search_placeholder": "🔍 Produkt suchen...",
|
||||||
|
"empty": "Keine Produkte hier.\nScanne ein Produkt, um es hinzuzufügen!",
|
||||||
|
"no_items_found": "Keine Bestandseinträge gefunden"
|
||||||
|
},
|
||||||
|
"scan": {
|
||||||
|
"title": "Produkt scannen",
|
||||||
|
"mode_shopping": "🛒 Einkaufsmodus",
|
||||||
|
"mode_shopping_end": "✅ Einkauf beenden",
|
||||||
|
"zoom": "Zoom",
|
||||||
|
"barcode_placeholder": "Barcode eingeben...",
|
||||||
|
"quick_name_divider": "oder Name eingeben",
|
||||||
|
"quick_name_placeholder": "z.B.: Äpfel, Zucchini, Brot...",
|
||||||
|
"manual_entry": "✏️ Manuelle Eingabe",
|
||||||
|
"ai_identify": "🤖 Mit KI identifizieren",
|
||||||
|
"hint": "Barcode scannen, Produktname eingeben oder KI zur Identifikation nutzen",
|
||||||
|
"debug_toggle": "🐛 Debug Log",
|
||||||
|
"barcode_acquired": "🔖 Barcode gescannt: {code}",
|
||||||
|
"scan_barcode": "🔖 Barcode scannen"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"title": "Was möchtest du tun?",
|
||||||
|
"add_btn": "📥 HINZUFÜGEN",
|
||||||
|
"add_sub": "in Vorrat/Kühlschrank",
|
||||||
|
"use_btn": "📤 VERWENDEN / VERBRAUCHEN",
|
||||||
|
"use_sub": "aus Vorrat/Kühlschrank"
|
||||||
|
},
|
||||||
|
"add": {
|
||||||
|
"title": "Zum Vorrat hinzufügen",
|
||||||
|
"location_label": "📍 Wohin?",
|
||||||
|
"quantity_label": "📦 Menge",
|
||||||
|
"conf_size_label": "📦 Jede Packung enthält:",
|
||||||
|
"conf_size_placeholder": "z.B. 300",
|
||||||
|
"vacuum_label": "🫙 Vakuumiert",
|
||||||
|
"vacuum_hint": "Ablaufdatum wird automatisch verlängert",
|
||||||
|
"submit": "✅ Hinzufügen"
|
||||||
|
},
|
||||||
|
"use": {
|
||||||
|
"title": "Verwenden / Verbrauchen",
|
||||||
|
"location_label": "📍 Woher?",
|
||||||
|
"quantity_label": "Wie viel hast du benutzt?",
|
||||||
|
"partial_hint": "Oder genaue Menge angeben:",
|
||||||
|
"use_all": "🗑️ ALLES verwendet / Aufgebraucht",
|
||||||
|
"submit": "📤 Diese Menge verwenden",
|
||||||
|
"available": "📦 Verfügbar:",
|
||||||
|
"not_in_inventory": "⚠️ Produkt nicht im Bestand.",
|
||||||
|
"expiry_warning": "⚠️ Verwende zuerst die{loc}, die am {date} abläuft — {when}!"
|
||||||
|
},
|
||||||
|
"product": {
|
||||||
|
"title_new": "Neues Produkt",
|
||||||
|
"title_edit": "Produkt bearbeiten",
|
||||||
|
"ai_fill": "📷 Foto machen und mit KI identifizieren",
|
||||||
|
"ai_fill_hint": "KI füllt die Produktfelder automatisch aus",
|
||||||
|
"name_label": "🏷️ Produktname *",
|
||||||
|
"name_placeholder": "z.B.: Vollmilch, Penne Nudeln...",
|
||||||
|
"brand_label": "🏢 Marke",
|
||||||
|
"brand_placeholder": "z.B.: Barilla, Müller, Knorr...",
|
||||||
|
"category_label": "📂 Kategorie",
|
||||||
|
"unit_label": "📏 Maßeinheit",
|
||||||
|
"default_qty_label": "🔢 Standardmenge",
|
||||||
|
"conf_size_label": "📦 Jede Packung enthält:",
|
||||||
|
"conf_size_placeholder": "z.B. 300",
|
||||||
|
"notes_label": "📝 Notizen",
|
||||||
|
"notes_placeholder": "z.B.: laktosefrei, bio, nach dem Öffnen im Kühlschrank aufbewahren...",
|
||||||
|
"barcode_label": "🔖 Barcode",
|
||||||
|
"barcode_placeholder": "Barcode (falls vorhanden)",
|
||||||
|
"barcode_hint": "⚠️ Barcode hinzufügen, damit du beim nächsten Einkauf nur scannen musst!",
|
||||||
|
"submit": "💾 Produkt speichern",
|
||||||
|
"name_required": "Produktname eingeben",
|
||||||
|
"conf_size_required": "Packungsinhalt angeben",
|
||||||
|
"expiry_estimated": "Geschätztes Ablaufdatum:",
|
||||||
|
"scan_expiry": "Ablaufdatum scannen",
|
||||||
|
"expiry_hint": "📝 Du kannst das Datum ändern oder mit der Kamera scannen",
|
||||||
|
"add_batch": "📦 + Charge mit anderem Ablaufdatum",
|
||||||
|
"package_info": "📦 Packung: {info}",
|
||||||
|
"edit_catalog": "⚙️ Produktinfo bearbeiten (Name, Marke, Kategorie…)",
|
||||||
|
"not_recognized": "⚠️ Produkt nicht erkannt",
|
||||||
|
"edit_info": "✏️ Informationen bearbeiten",
|
||||||
|
"modify_details": "BEARBEITEN\nAblauf, Ort…"
|
||||||
|
},
|
||||||
|
"products": {
|
||||||
|
"title": "📦 Alle Produkte",
|
||||||
|
"search_placeholder": "🔍 Produkt suchen...",
|
||||||
|
"empty": "Keine Produkte in der Datenbank.\nScanne ein Produkt zum Starten!",
|
||||||
|
"no_category": "Keine Produkte in dieser Kategorie"
|
||||||
|
},
|
||||||
|
"recipes": {
|
||||||
|
"title": "🍳 Rezepte",
|
||||||
|
"generate": "✨ Neues Rezept generieren"
|
||||||
|
},
|
||||||
|
"shopping": {
|
||||||
|
"title": "🛒 Einkaufsliste",
|
||||||
|
"bring_loading": "Verbindung zu Bring!...",
|
||||||
|
"tab_to_buy": "🛍️ Zu kaufen",
|
||||||
|
"tab_forecast": "🧠 Vorhersage",
|
||||||
|
"total_label": "💰 Geschätzter Gesamtbetrag",
|
||||||
|
"section_to_buy": "🛍️ Zu kaufen",
|
||||||
|
"suggestions_title": "💡 KI-Vorschläge",
|
||||||
|
"suggestions_add": "✅ Ausgewählte zu Bring! hinzufügen",
|
||||||
|
"search_prices": "🔍 Alle Preise suchen",
|
||||||
|
"suggest_btn": "🤖 Einkaufsvorschläge",
|
||||||
|
"smart_title": "🧠 Intelligente Vorhersagen",
|
||||||
|
"smart_empty": "Keine Vorhersagen verfügbar.<br>Füge Produkte zur Vorratskammer hinzu, um intelligente Vorhersagen zu erhalten.",
|
||||||
|
"smart_filter_all": "Alle",
|
||||||
|
"smart_filter_critical": "🔴 Dringend",
|
||||||
|
"smart_filter_high": "🟠 Bald",
|
||||||
|
"smart_filter_medium": "🟡 Planen",
|
||||||
|
"smart_filter_low": "🟢 Vorhersage",
|
||||||
|
"smart_add": "🛒 Ausgewählte zu Bring! hinzufügen",
|
||||||
|
"empty": "Einkaufsliste leer!\nNutze den Button unten, um Vorschläge zu generieren.",
|
||||||
|
"already_in_list": "🛒 \"{name}\" ist bereits in der Einkaufsliste",
|
||||||
|
"already_in_list_short": "ℹ️ Bereits in der Einkaufsliste",
|
||||||
|
"add_prompt": "Möchtest du es zur Einkaufsliste hinzufügen?",
|
||||||
|
"smart_already": "📊 Intelligenter Einkauf sagt bereits {name} voraus",
|
||||||
|
"all_searched": "Alle Produkte wurden bereits gesucht. Nutze 🔄 für einzelne Suchen.",
|
||||||
|
"search_complete": "Suche abgeschlossen: {count} Produkte",
|
||||||
|
"removed_sufficient": "🧹 {removed} Produkt(e) mit ausreichendem Bestand von der Liste entfernt"
|
||||||
|
},
|
||||||
|
"ai": {
|
||||||
|
"title": "🤖 KI-Identifikation",
|
||||||
|
"capture": "📸 Foto aufnehmen",
|
||||||
|
"retake": "🔄 Neu aufnehmen",
|
||||||
|
"hint": "Mache ein Foto des Produkts und die KI versucht es zu identifizieren",
|
||||||
|
"identifying": "🤖 Identifiziere Produkt...",
|
||||||
|
"no_api_key": "⚠️ Gemini API-Schlüssel nicht konfiguriert.\n<small>Füge GEMINI_API_KEY in der .env Datei auf dem Server hinzu.</small>",
|
||||||
|
"fields_filled": "✅ Felder von KI ausgefüllt"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"title": "📒 Operationslog"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "Gemini Chef",
|
||||||
|
"welcome": "Hallo! Ich bin dein Küchenassistent",
|
||||||
|
"welcome_desc": "Frag mich, dir einen Saft, einen Snack, ein schnelles Gericht zu machen... Ich kenne deinen Vorrat, deine Geräte und deine Vorlieben!",
|
||||||
|
"suggestion_snack": "🍿 Schneller Snack",
|
||||||
|
"suggestion_juice": "🥤 Saft/Smoothie",
|
||||||
|
"suggestion_light": "🥗 Etwas Leichtes",
|
||||||
|
"suggestion_expiry": "⏰ Ablaufende nutzen",
|
||||||
|
"clear": "Neues Gespräch",
|
||||||
|
"placeholder": "Frag etwas..."
|
||||||
|
},
|
||||||
|
"cooking": {
|
||||||
|
"close": "Schließen",
|
||||||
|
"tts_btn": "Vorlesen",
|
||||||
|
"restart": "↺ Neustart",
|
||||||
|
"replay": "🔊 Nochmal",
|
||||||
|
"timer": "⏱️ {time} · Timer",
|
||||||
|
"prev": "◀ Zurück",
|
||||||
|
"next": "Weiter ▶"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "⚙️ Einstellungen",
|
||||||
|
"tab_api": "API Keys",
|
||||||
|
"tab_bring": "Bring!",
|
||||||
|
"tab_recipe": "Rezepte",
|
||||||
|
"tab_mealplan": "Wochenplan",
|
||||||
|
"tab_appliances": "Geräte",
|
||||||
|
"tab_spesa": "Online-Einkauf",
|
||||||
|
"tab_camera": "Kamera",
|
||||||
|
"tab_security": "Sicherheit",
|
||||||
|
"tab_tts": "Sprache (TTS)",
|
||||||
|
"tab_language": "Sprache",
|
||||||
|
"gemini": {
|
||||||
|
"title": "🤖 Google Gemini AI",
|
||||||
|
"hint": "API-Schlüssel für Produkterkennung, Ablaufdaten und Rezepte.",
|
||||||
|
"key_label": "Gemini API Key"
|
||||||
|
},
|
||||||
|
"bring": {
|
||||||
|
"title": "🛒 Bring! Einkaufsliste",
|
||||||
|
"hint": "Zugangsdaten für die Bring! Einkaufslisten-Integration.",
|
||||||
|
"email_label": "📧 Bring! E-Mail",
|
||||||
|
"password_label": "🔒 Bring! Passwort"
|
||||||
|
},
|
||||||
|
"recipe": {
|
||||||
|
"title": "🍳 Rezept-Einstellungen",
|
||||||
|
"hint": "Konfiguriere die Standardoptionen für die Rezeptgenerierung.",
|
||||||
|
"persons_label": "👥 Standard-Portionen",
|
||||||
|
"options_label": "🎯 Standard-Rezeptoptionen",
|
||||||
|
"fast": "⚡ Schnelles Gericht",
|
||||||
|
"light": "🥗 Leichte Mahlzeit",
|
||||||
|
"expiry": "⏰ Ablauf-Priorität",
|
||||||
|
"healthy": "💚 Extra Gesund",
|
||||||
|
"opened": "📦 Offene Produkte zuerst",
|
||||||
|
"zerowaste": "♻️ Keine Verschwendung",
|
||||||
|
"dietary_label": "🚫 Unverträglichkeiten / Einschränkungen",
|
||||||
|
"dietary_placeholder": "z.B.: glutenfrei, laktosefrei, vegetarisch..."
|
||||||
|
},
|
||||||
|
"mealplan": {
|
||||||
|
"title": "📅 Wöchentlicher Essensplan",
|
||||||
|
"hint": "Lege die Mahlzeitenart für jeden Tag fest. Wird als Leitfaden bei der Rezeptgenerierung verwendet.",
|
||||||
|
"enabled": "✅ Wöchentlichen Essensplan aktivieren",
|
||||||
|
"legend": "🌤️ = Mittagessen · 🌙 = Abendessen · Tippe auf ein Badge, um es zu ändern.",
|
||||||
|
"types_title": "📋 Verfügbare Typen"
|
||||||
|
},
|
||||||
|
"appliances": {
|
||||||
|
"title": "🔌 Verfügbare Geräte",
|
||||||
|
"hint": "Gib an, welche Geräte du hast. Sie werden bei der Rezeptgenerierung berücksichtigt.",
|
||||||
|
"new_placeholder": "z.B.: Brotbackmaschine, Thermomix, Heißluftfritteuse...",
|
||||||
|
"quick_title": "Schnell hinzufügen:",
|
||||||
|
"oven": "🔥 Backofen",
|
||||||
|
"microwave": "📡 Mikrowelle",
|
||||||
|
"air_fryer": "🍟 Heißluftfritteuse",
|
||||||
|
"bread_maker": "🍞 Brotbackmaschine",
|
||||||
|
"bimby": "🤖 Thermomix/Cookeo",
|
||||||
|
"mixer": "🌀 Küchenmaschine",
|
||||||
|
"steamer": "♨️ Dampfgarer",
|
||||||
|
"pressure_cooker": "🫕 Schnellkochtopf",
|
||||||
|
"toaster": "🍞 Toaster",
|
||||||
|
"blender": "🍹 Mixer",
|
||||||
|
"empty": "Keine Geräte hinzugefügt"
|
||||||
|
},
|
||||||
|
"spesa": {
|
||||||
|
"title": "🛍️ Online-Einkauf",
|
||||||
|
"hint": "Online-Einkaufsanbieter konfigurieren.",
|
||||||
|
"provider_label": "🏪 Anbieter",
|
||||||
|
"email_label": "📧 E-Mail",
|
||||||
|
"password_label": "🔒 Passwort",
|
||||||
|
"login_btn": "🔐 Anmelden",
|
||||||
|
"ai_prompt_label": "🤖 KI-Produktauswahl Prompt",
|
||||||
|
"ai_prompt_placeholder": "Anweisungen für die KI bei der Auswahl zwischen mehreren Produkten...",
|
||||||
|
"ai_prompt_hint": "Die KI verwendet diesen Prompt zur Auswahl des passendsten Produkts. Leer lassen für Standardverhalten.",
|
||||||
|
"configure_first": "Konfiguriere zuerst den Online-Einkauf in den Einstellungen"
|
||||||
|
},
|
||||||
|
"camera": {
|
||||||
|
"title": "📷 Kamera",
|
||||||
|
"hint": "Wähle die Kamera für Barcode-Scanning und KI-Identifikation.",
|
||||||
|
"device_label": "📸 Standardkamera",
|
||||||
|
"back": "📱 Rückkamera (Standard)",
|
||||||
|
"front": "🤳 Frontkamera",
|
||||||
|
"devices_hint": "Bei mehreren Kameras kannst du nach Freigabe der Berechtigungen eine bestimmte aus der Liste oben wählen.",
|
||||||
|
"detect_btn": "🔄 Kameras erkennen"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "🔒 HTTPS-Zertifikat",
|
||||||
|
"hint": "Wenn der Browser den Fehler \"Verbindung nicht sicher\" (ERR_CERT_AUTHORITY_INVALID) zeigt, installiere das CA-Zertifikat auf dem Gerät.",
|
||||||
|
"download_btn": "📥 CA-Zertifikat herunterladen"
|
||||||
|
},
|
||||||
|
"tts": {
|
||||||
|
"title": "🔊 Sprache & TTS",
|
||||||
|
"hint": "Sprachsynthese über externe REST-API konfigurieren. Rezeptschritte und abgelaufene Timer werden an den Endpunkt gesendet.",
|
||||||
|
"enabled": "✅ TTS aktivieren",
|
||||||
|
"url_label": "🌐 Endpunkt-URL",
|
||||||
|
"method_label": "📡 HTTP-Methode",
|
||||||
|
"auth_label": "🔐 Authentifizierung",
|
||||||
|
"auth_bearer": "Bearer Token",
|
||||||
|
"auth_custom": "Benutzerdefinierter Header",
|
||||||
|
"auth_none": "Keine",
|
||||||
|
"token_label": "🔑 Bearer Token",
|
||||||
|
"custom_header_name": "📋 Header-Name",
|
||||||
|
"custom_header_value": "📋 Header-Wert",
|
||||||
|
"content_type_label": "📄 Content-Type",
|
||||||
|
"payload_key_label": "🗝️ Textfeld im Payload",
|
||||||
|
"payload_key_hint": "Name des JSON-Feldes für den zu lesenden Text (z.B.: message, text).",
|
||||||
|
"extra_fields_label": "➕ Zusätzliche Felder (JSON)",
|
||||||
|
"extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}",
|
||||||
|
"extra_fields_hint": "Zusätzliche Felder im Payload, im JSON-Format. Leer lassen wenn nicht benötigt.",
|
||||||
|
"test_btn": "🔊 Testansage senden"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"title": "🌐 Sprache",
|
||||||
|
"hint": "Wähle die Sprache der Benutzeroberfläche.",
|
||||||
|
"label": "🌐 Sprache",
|
||||||
|
"restart_notice": "Die Seite wird neu geladen, um die neue Sprache anzuwenden."
|
||||||
|
},
|
||||||
|
"saved": "✅ Konfiguration gespeichert!",
|
||||||
|
"saved_local": "✅ Konfiguration lokal gespeichert",
|
||||||
|
"saved_local_error": "⚠️ Lokal gespeichert, Serverfehler: {error}"
|
||||||
|
},
|
||||||
|
"expiry": {
|
||||||
|
"today": "HEUTE",
|
||||||
|
"tomorrow": "Morgen",
|
||||||
|
"days": "{days} Tage",
|
||||||
|
"expired_days": "Seit {days}T",
|
||||||
|
"expired_yesterday": "Seit gestern",
|
||||||
|
"expired_today": "Heute"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"ok": "OK",
|
||||||
|
"check": "Prüfen",
|
||||||
|
"discard": "Entsorgen"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"product_saved": "Produkt gespeichert!",
|
||||||
|
"product_created": "Produkt erstellt!",
|
||||||
|
"product_updated": "✅ Produkt aktualisiert!",
|
||||||
|
"product_removed": "Produkt entfernt",
|
||||||
|
"updated": "Aktualisiert!",
|
||||||
|
"quantity_confirmed": "✓ Menge bestätigt",
|
||||||
|
"added_to_inventory": "✅ {name} hinzugefügt!",
|
||||||
|
"removed_from_list": "✅ {name} von der Liste entfernt!",
|
||||||
|
"removed_from_list_short": "Von der Liste entfernt",
|
||||||
|
"added_to_shopping": "🛒 Zur Einkaufsliste hinzugefügt!",
|
||||||
|
"removed_from_shopping": "🛒 Von der Einkaufsliste entfernt",
|
||||||
|
"finished_to_bring": "🛒 Produkt aufgebraucht → zu Bring! hinzugefügt",
|
||||||
|
"thrown_away": "🗑️ {name} weggeworfen!",
|
||||||
|
"thrown_away_partial": "🗑️ {qty} {unit} von {name} weggeworfen",
|
||||||
|
"appliance_added": "Gerät hinzugefügt",
|
||||||
|
"item_added": "{name} hinzugefügt"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"generic": "Fehler",
|
||||||
|
"loading": "Fehler beim Laden des Produkts",
|
||||||
|
"not_found": "Produkt nicht gefunden",
|
||||||
|
"not_found_manual": "Produkt nicht gefunden. Manuell eingeben.",
|
||||||
|
"search": "Suchfehler. Nochmal versuchen.",
|
||||||
|
"search_short": "Suchfehler",
|
||||||
|
"save": "Fehler beim Speichern",
|
||||||
|
"connection": "Verbindungsfehler",
|
||||||
|
"camera": "Kamera nicht verfügbar",
|
||||||
|
"bring_add": "Fehler beim Hinzufügen zu Bring!",
|
||||||
|
"bring_connection": "Bring! Verbindungsfehler",
|
||||||
|
"identification": "Identifikationsfehler",
|
||||||
|
"barcode_empty": "Barcode eingeben",
|
||||||
|
"barcode_format": "Barcode darf nur Zahlen enthalten (4-14 Ziffern)",
|
||||||
|
"min_chars": "Mindestens 2 Zeichen eingeben",
|
||||||
|
"not_in_inventory": "Produkt nicht im Bestand",
|
||||||
|
"appliance_exists": "Gerät bereits vorhanden",
|
||||||
|
"already_exists": "Bereits vorhanden"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"remove_item": "Möchtest du dieses Produkt wirklich aus dem Bestand entfernen?"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "{name} bearbeiten"
|
||||||
|
},
|
||||||
|
"screensaver": {
|
||||||
|
"recipe_btn": "Rezepte",
|
||||||
|
"scan_btn": "Produkt scannen"
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"mon": "Montag",
|
||||||
|
"tue": "Dienstag",
|
||||||
|
"wed": "Mittwoch",
|
||||||
|
"thu": "Donnerstag",
|
||||||
|
"fri": "Freitag",
|
||||||
|
"sat": "Samstag",
|
||||||
|
"sun": "Sonntag"
|
||||||
|
},
|
||||||
|
"meal_types": {
|
||||||
|
"lunch": "Mittagessen",
|
||||||
|
"dinner": "Abendessen"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,431 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "Dispensa Manager",
|
||||||
|
"loading": "Loading..."
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"title": "🏠 Pantry",
|
||||||
|
"home": "Home",
|
||||||
|
"inventory": "Pantry",
|
||||||
|
"recipes": "Recipes",
|
||||||
|
"shopping": "Shopping",
|
||||||
|
"log": "Log"
|
||||||
|
},
|
||||||
|
"btn": {
|
||||||
|
"back": "← Back",
|
||||||
|
"save": "💾 Save",
|
||||||
|
"cancel": "✕ Cancel",
|
||||||
|
"close": "Close",
|
||||||
|
"add": "✅ Add",
|
||||||
|
"delete": "Delete",
|
||||||
|
"edit": "✏️ Edit",
|
||||||
|
"search": "🔍 Search",
|
||||||
|
"go": "✅ Go",
|
||||||
|
"toggle_password": "👁️ Show/Hide",
|
||||||
|
"load_more": "Load more...",
|
||||||
|
"save_config": "💾 Save Configuration",
|
||||||
|
"save_product": "💾 Save Product",
|
||||||
|
"restart": "↺ Restart",
|
||||||
|
"reset_default": "↺ Reset to default"
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"dispensa": "Pantry",
|
||||||
|
"frigo": "Fridge",
|
||||||
|
"freezer": "Freezer",
|
||||||
|
"altro": "Other"
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"latticini": "Dairy",
|
||||||
|
"carne": "Meat",
|
||||||
|
"pesce": "Fish",
|
||||||
|
"frutta": "Fruit",
|
||||||
|
"verdura": "Vegetables",
|
||||||
|
"pasta": "Pasta & Rice",
|
||||||
|
"pane": "Bread & Bakery",
|
||||||
|
"surgelati": "Frozen",
|
||||||
|
"bevande": "Beverages",
|
||||||
|
"condimenti": "Condiments",
|
||||||
|
"snack": "Snacks & Sweets",
|
||||||
|
"conserve": "Canned Goods",
|
||||||
|
"cereali": "Cereals & Legumes",
|
||||||
|
"igiene": "Hygiene",
|
||||||
|
"pulizia": "Household",
|
||||||
|
"altro": "Other",
|
||||||
|
"select": "-- Select --"
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"pz": "pcs",
|
||||||
|
"conf": "pkg",
|
||||||
|
"g": "g",
|
||||||
|
"ml": "ml",
|
||||||
|
"pieces": "Pieces",
|
||||||
|
"grams": "Grams",
|
||||||
|
"box": "Package",
|
||||||
|
"boxes": "Packages"
|
||||||
|
},
|
||||||
|
"shopping_sections": {
|
||||||
|
"frutta_verdura": "Fruits & Vegetables",
|
||||||
|
"carne_pesce": "Meat & Fish",
|
||||||
|
"latticini": "Dairy & Fresh",
|
||||||
|
"pane_dolci": "Bread & Sweets",
|
||||||
|
"pasta": "Pasta & Cereals",
|
||||||
|
"conserve": "Canned & Sauces",
|
||||||
|
"surgelati": "Frozen",
|
||||||
|
"bevande": "Beverages",
|
||||||
|
"pulizia_igiene": "Cleaning & Hygiene",
|
||||||
|
"altro": "Other"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"expired_title": "🚫 Expired",
|
||||||
|
"expiring_title": "⏰ Expiring Soon",
|
||||||
|
"stats_period": "📊 Last 30 days",
|
||||||
|
"opened_title": "📦 Opened Products",
|
||||||
|
"review_title": "🔍 To Review",
|
||||||
|
"review_hint": "Quantities that seem unusual. Confirm if correct or modify.",
|
||||||
|
"quick_recipe": "🍳 Quick recipe with expiring products"
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"title": "Pantry",
|
||||||
|
"filter_all": "All",
|
||||||
|
"search_placeholder": "🔍 Search product...",
|
||||||
|
"empty": "No products here.\nScan a product to add it!",
|
||||||
|
"no_items_found": "No inventory items found"
|
||||||
|
},
|
||||||
|
"scan": {
|
||||||
|
"title": "Scan Product",
|
||||||
|
"mode_shopping": "🛒 Shopping Mode",
|
||||||
|
"mode_shopping_end": "✅ End shopping",
|
||||||
|
"zoom": "Zoom",
|
||||||
|
"barcode_placeholder": "Enter barcode...",
|
||||||
|
"quick_name_divider": "or type the name",
|
||||||
|
"quick_name_placeholder": "E.g.: Apples, Zucchini, Bread...",
|
||||||
|
"manual_entry": "✏️ Manual Entry",
|
||||||
|
"ai_identify": "🤖 Identify with AI",
|
||||||
|
"hint": "Scan the barcode, type the product name, or use AI to identify it",
|
||||||
|
"debug_toggle": "🐛 Debug Log",
|
||||||
|
"barcode_acquired": "🔖 Barcode scanned: {code}",
|
||||||
|
"scan_barcode": "🔖 Scan Barcode"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"title": "What do you want to do?",
|
||||||
|
"add_btn": "📥 ADD",
|
||||||
|
"add_sub": "to pantry/fridge",
|
||||||
|
"use_btn": "📤 USE / CONSUME",
|
||||||
|
"use_sub": "from pantry/fridge"
|
||||||
|
},
|
||||||
|
"add": {
|
||||||
|
"title": "Add to Pantry",
|
||||||
|
"location_label": "📍 Where do you put it?",
|
||||||
|
"quantity_label": "📦 Quantity",
|
||||||
|
"conf_size_label": "📦 Each package contains:",
|
||||||
|
"conf_size_placeholder": "e.g. 300",
|
||||||
|
"vacuum_label": "🫙 Vacuum sealed",
|
||||||
|
"vacuum_hint": "Expiry date will be extended automatically",
|
||||||
|
"submit": "✅ Add"
|
||||||
|
},
|
||||||
|
"use": {
|
||||||
|
"title": "Use / Consume",
|
||||||
|
"location_label": "📍 From where?",
|
||||||
|
"quantity_label": "How much did you use?",
|
||||||
|
"partial_hint": "Or specify the quantity used:",
|
||||||
|
"use_all": "🗑️ Used ALL / Finished",
|
||||||
|
"submit": "📤 Use this quantity",
|
||||||
|
"available": "📦 Available:",
|
||||||
|
"not_in_inventory": "⚠️ Product not in inventory.",
|
||||||
|
"expiry_warning": "⚠️ Use first the one{loc} that expires on {date} — {when}!"
|
||||||
|
},
|
||||||
|
"product": {
|
||||||
|
"title_new": "New Product",
|
||||||
|
"title_edit": "Edit Product",
|
||||||
|
"ai_fill": "📷 Take photo and identify with AI",
|
||||||
|
"ai_fill_hint": "AI will automatically fill in the product fields",
|
||||||
|
"name_label": "🏷️ Product Name *",
|
||||||
|
"name_placeholder": "E.g.: Whole milk, Penne pasta...",
|
||||||
|
"brand_label": "🏢 Brand",
|
||||||
|
"brand_placeholder": "E.g.: Barilla, Granarolo, Mutti...",
|
||||||
|
"category_label": "📂 Category",
|
||||||
|
"unit_label": "📏 Unit of measure",
|
||||||
|
"default_qty_label": "🔢 Default quantity",
|
||||||
|
"conf_size_label": "📦 Each package contains:",
|
||||||
|
"conf_size_placeholder": "e.g. 300",
|
||||||
|
"notes_label": "📝 Notes",
|
||||||
|
"notes_placeholder": "E.g.: lactose free, organic, store in fridge after opening...",
|
||||||
|
"barcode_label": "🔖 Barcode",
|
||||||
|
"barcode_placeholder": "Barcode (if available)",
|
||||||
|
"barcode_hint": "⚠️ Add the barcode so next time you just need to scan it!",
|
||||||
|
"submit": "💾 Save Product",
|
||||||
|
"name_required": "Enter the product name",
|
||||||
|
"conf_size_required": "Specify the package content",
|
||||||
|
"expiry_estimated": "Estimated expiry:",
|
||||||
|
"scan_expiry": "Scan expiry date",
|
||||||
|
"expiry_hint": "📝 You can edit the date or scan it with the camera",
|
||||||
|
"add_batch": "📦 + Batch with different expiry",
|
||||||
|
"package_info": "📦 Package: {info}",
|
||||||
|
"edit_catalog": "⚙️ Edit product info (name, brand, category…)",
|
||||||
|
"not_recognized": "⚠️ Product not recognized",
|
||||||
|
"edit_info": "✏️ Edit information",
|
||||||
|
"modify_details": "EDIT\nexpiry, location…"
|
||||||
|
},
|
||||||
|
"products": {
|
||||||
|
"title": "📦 All Products",
|
||||||
|
"search_placeholder": "🔍 Search product...",
|
||||||
|
"empty": "No products in database.\nScan a product to get started!",
|
||||||
|
"no_category": "No products in this category"
|
||||||
|
},
|
||||||
|
"recipes": {
|
||||||
|
"title": "🍳 Recipes",
|
||||||
|
"generate": "✨ Generate new recipe"
|
||||||
|
},
|
||||||
|
"shopping": {
|
||||||
|
"title": "🛒 Shopping List",
|
||||||
|
"bring_loading": "Connecting to Bring!...",
|
||||||
|
"tab_to_buy": "🛍️ To buy",
|
||||||
|
"tab_forecast": "🧠 Forecast",
|
||||||
|
"total_label": "💰 Estimated total",
|
||||||
|
"section_to_buy": "🛍️ To buy",
|
||||||
|
"suggestions_title": "💡 AI Suggestions",
|
||||||
|
"suggestions_add": "✅ Add selected to Bring!",
|
||||||
|
"search_prices": "🔍 Search all prices",
|
||||||
|
"suggest_btn": "🤖 Suggest what to buy",
|
||||||
|
"smart_title": "🧠 Smart Predictions",
|
||||||
|
"smart_empty": "No predictions available.<br>Add products to your pantry to receive smart predictions.",
|
||||||
|
"smart_filter_all": "All",
|
||||||
|
"smart_filter_critical": "🔴 Urgent",
|
||||||
|
"smart_filter_high": "🟠 Soon",
|
||||||
|
"smart_filter_medium": "🟡 Plan",
|
||||||
|
"smart_filter_low": "🟢 Forecast",
|
||||||
|
"smart_add": "🛒 Add selected to Bring!",
|
||||||
|
"empty": "Shopping list empty!\nUse the button below to generate suggestions.",
|
||||||
|
"already_in_list": "🛒 \"{name}\" is already in the shopping list",
|
||||||
|
"already_in_list_short": "ℹ️ Already in the shopping list",
|
||||||
|
"add_prompt": "Do you want to add it to the shopping list?",
|
||||||
|
"smart_already": "📊 Smart shopping already predicts {name}",
|
||||||
|
"all_searched": "All products have already been searched. Use 🔄 to search individual ones.",
|
||||||
|
"search_complete": "Search complete: {count} products",
|
||||||
|
"removed_sufficient": "🧹 {removed} product(s) with sufficient stock removed from the list"
|
||||||
|
},
|
||||||
|
"ai": {
|
||||||
|
"title": "🤖 AI Identification",
|
||||||
|
"capture": "📸 Take Photo",
|
||||||
|
"retake": "🔄 Retake",
|
||||||
|
"hint": "Take a photo of the product and AI will try to identify it",
|
||||||
|
"identifying": "🤖 Identifying product...",
|
||||||
|
"no_api_key": "⚠️ Gemini API key not configured.\n<small>Add GEMINI_API_KEY to the .env file on the server.</small>",
|
||||||
|
"fields_filled": "✅ Fields filled by AI"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"title": "📒 Operations Log"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "Gemini Chef",
|
||||||
|
"welcome": "Hi! I'm your kitchen assistant",
|
||||||
|
"welcome_desc": "Ask me to make you a juice, a snack, a quick dish... I know your pantry, your appliances and your preferences!",
|
||||||
|
"suggestion_snack": "🍿 Quick snack",
|
||||||
|
"suggestion_juice": "🥤 Juice/Smoothie",
|
||||||
|
"suggestion_light": "🥗 Something light",
|
||||||
|
"suggestion_expiry": "⏰ Use expiring items",
|
||||||
|
"clear": "New conversation",
|
||||||
|
"placeholder": "Ask something..."
|
||||||
|
},
|
||||||
|
"cooking": {
|
||||||
|
"close": "Close",
|
||||||
|
"tts_btn": "Read aloud",
|
||||||
|
"restart": "↺ Restart",
|
||||||
|
"replay": "🔊 Replay",
|
||||||
|
"timer": "⏱️ {time} · Timer",
|
||||||
|
"prev": "◀ Previous",
|
||||||
|
"next": "Next ▶"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "⚙️ Settings",
|
||||||
|
"tab_api": "API Keys",
|
||||||
|
"tab_bring": "Bring!",
|
||||||
|
"tab_recipe": "Recipes",
|
||||||
|
"tab_mealplan": "Weekly Plan",
|
||||||
|
"tab_appliances": "Appliances",
|
||||||
|
"tab_spesa": "Online Shopping",
|
||||||
|
"tab_camera": "Camera",
|
||||||
|
"tab_security": "Security",
|
||||||
|
"tab_tts": "Voice (TTS)",
|
||||||
|
"tab_language": "Language",
|
||||||
|
"gemini": {
|
||||||
|
"title": "🤖 Google Gemini AI",
|
||||||
|
"hint": "API key for product identification, expiry dates and recipes.",
|
||||||
|
"key_label": "Gemini API Key"
|
||||||
|
},
|
||||||
|
"bring": {
|
||||||
|
"title": "🛒 Bring! Shopping List",
|
||||||
|
"hint": "Credentials for the Bring! shopping list integration.",
|
||||||
|
"email_label": "📧 Bring! Email",
|
||||||
|
"password_label": "🔒 Bring! Password"
|
||||||
|
},
|
||||||
|
"recipe": {
|
||||||
|
"title": "🍳 Recipe Preferences",
|
||||||
|
"hint": "Configure the default options for recipe generation.",
|
||||||
|
"persons_label": "👥 Default servings",
|
||||||
|
"options_label": "🎯 Default recipe options",
|
||||||
|
"fast": "⚡ Quick Meal",
|
||||||
|
"light": "🥗 Light Meal",
|
||||||
|
"expiry": "⏰ Expiry Priority",
|
||||||
|
"healthy": "💚 Extra Healthy",
|
||||||
|
"opened": "📦 Open Items Priority",
|
||||||
|
"zerowaste": "♻️ Zero Waste",
|
||||||
|
"dietary_label": "🚫 Intolerances / Restrictions",
|
||||||
|
"dietary_placeholder": "E.g.: gluten free, lactose free, vegetarian..."
|
||||||
|
},
|
||||||
|
"mealplan": {
|
||||||
|
"title": "📅 Weekly Meal Plan",
|
||||||
|
"hint": "Set the meal type for each day. It will be used as a guide in recipe generation.",
|
||||||
|
"enabled": "✅ Enable weekly meal plan",
|
||||||
|
"legend": "🌤️ = Lunch · 🌙 = Dinner · Tap a badge to change it.",
|
||||||
|
"types_title": "📋 Available types"
|
||||||
|
},
|
||||||
|
"appliances": {
|
||||||
|
"title": "🔌 Available Appliances",
|
||||||
|
"hint": "Indicate the appliances you have. They will be considered in recipe generation.",
|
||||||
|
"new_placeholder": "E.g.: Bread machine, Thermomix, Air fryer...",
|
||||||
|
"quick_title": "Quick add:",
|
||||||
|
"oven": "🔥 Oven",
|
||||||
|
"microwave": "📡 Microwave",
|
||||||
|
"air_fryer": "🍟 Air fryer",
|
||||||
|
"bread_maker": "🍞 Bread maker",
|
||||||
|
"bimby": "🤖 Thermomix/Cookeo",
|
||||||
|
"mixer": "🌀 Stand mixer",
|
||||||
|
"steamer": "♨️ Steamer",
|
||||||
|
"pressure_cooker": "🫕 Pressure cooker",
|
||||||
|
"toaster": "🍞 Toaster",
|
||||||
|
"blender": "🍹 Blender",
|
||||||
|
"empty": "No appliances added"
|
||||||
|
},
|
||||||
|
"spesa": {
|
||||||
|
"title": "🛍️ Online Shopping",
|
||||||
|
"hint": "Configure the online shopping provider.",
|
||||||
|
"provider_label": "🏪 Provider",
|
||||||
|
"email_label": "📧 Email",
|
||||||
|
"password_label": "🔒 Password",
|
||||||
|
"login_btn": "🔐 Login",
|
||||||
|
"ai_prompt_label": "🤖 AI product selection prompt",
|
||||||
|
"ai_prompt_placeholder": "Instructions for AI when choosing between multiple products...",
|
||||||
|
"ai_prompt_hint": "AI uses this prompt to choose the most appropriate product from results. Leave empty for default behavior.",
|
||||||
|
"configure_first": "Configure Online Shopping in settings first"
|
||||||
|
},
|
||||||
|
"camera": {
|
||||||
|
"title": "📷 Camera",
|
||||||
|
"hint": "Choose which camera to use for barcode scanning and AI identification.",
|
||||||
|
"device_label": "📸 Default camera",
|
||||||
|
"back": "📱 Rear (default)",
|
||||||
|
"front": "🤳 Front",
|
||||||
|
"devices_hint": "If you have multiple cameras, you can select a specific one from the list above after granting permissions.",
|
||||||
|
"detect_btn": "🔄 Detect cameras"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "🔒 HTTPS Certificate",
|
||||||
|
"hint": "If the browser shows the error \"Your connection is not private\" (ERR_CERT_AUTHORITY_INVALID), you need to install the CA certificate on the device.",
|
||||||
|
"download_btn": "📥 Download CA Certificate"
|
||||||
|
},
|
||||||
|
"tts": {
|
||||||
|
"title": "🔊 Voice & TTS",
|
||||||
|
"hint": "Configure text-to-speech via any external REST API. Recipe steps and expired timers will be sent to the configured endpoint.",
|
||||||
|
"enabled": "✅ Enable TTS",
|
||||||
|
"url_label": "🌐 Endpoint URL",
|
||||||
|
"method_label": "📡 HTTP Method",
|
||||||
|
"auth_label": "🔐 Authentication",
|
||||||
|
"auth_bearer": "Bearer Token",
|
||||||
|
"auth_custom": "Custom Header",
|
||||||
|
"auth_none": "None",
|
||||||
|
"token_label": "🔑 Bearer Token",
|
||||||
|
"custom_header_name": "📋 Header name",
|
||||||
|
"custom_header_value": "📋 Header value",
|
||||||
|
"content_type_label": "📄 Content-Type",
|
||||||
|
"payload_key_label": "🗝️ Text field in payload",
|
||||||
|
"payload_key_hint": "Name of the JSON field that will contain the text to read (e.g.: message, text).",
|
||||||
|
"extra_fields_label": "➕ Extra fields (JSON)",
|
||||||
|
"extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}",
|
||||||
|
"extra_fields_hint": "Additional fields to include in the payload, in JSON format. Leave empty if not needed.",
|
||||||
|
"test_btn": "🔊 Send Test Voice"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"title": "🌐 Language",
|
||||||
|
"hint": "Select the interface language.",
|
||||||
|
"label": "🌐 Language",
|
||||||
|
"restart_notice": "The page will reload to apply the new language."
|
||||||
|
},
|
||||||
|
"saved": "✅ Configuration saved!",
|
||||||
|
"saved_local": "✅ Configuration saved locally",
|
||||||
|
"saved_local_error": "⚠️ Saved locally, server error: {error}"
|
||||||
|
},
|
||||||
|
"expiry": {
|
||||||
|
"today": "TODAY",
|
||||||
|
"tomorrow": "Tomorrow",
|
||||||
|
"days": "{days} days",
|
||||||
|
"expired_days": "{days}d ago",
|
||||||
|
"expired_yesterday": "Yesterday",
|
||||||
|
"expired_today": "Today"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"ok": "OK",
|
||||||
|
"check": "Check",
|
||||||
|
"discard": "Discard"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"product_saved": "Product saved!",
|
||||||
|
"product_created": "Product created!",
|
||||||
|
"product_updated": "✅ Product updated!",
|
||||||
|
"product_removed": "Product removed",
|
||||||
|
"updated": "Updated!",
|
||||||
|
"quantity_confirmed": "✓ Quantity confirmed",
|
||||||
|
"added_to_inventory": "✅ {name} added!",
|
||||||
|
"removed_from_list": "✅ {name} removed from the list!",
|
||||||
|
"removed_from_list_short": "Removed from the list",
|
||||||
|
"added_to_shopping": "🛒 Added to the shopping list!",
|
||||||
|
"removed_from_shopping": "🛒 Removed from the shopping list",
|
||||||
|
"finished_to_bring": "🛒 Product finished → added to Bring!",
|
||||||
|
"thrown_away": "🗑️ {name} thrown away!",
|
||||||
|
"thrown_away_partial": "🗑️ Thrown away {qty} {unit} of {name}",
|
||||||
|
"appliance_added": "Appliance added",
|
||||||
|
"item_added": "{name} added"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"generic": "Error",
|
||||||
|
"loading": "Error loading product",
|
||||||
|
"not_found": "Product not found",
|
||||||
|
"not_found_manual": "Product not found. Enter it manually.",
|
||||||
|
"search": "Search error. Try again.",
|
||||||
|
"search_short": "Search error",
|
||||||
|
"save": "Error saving",
|
||||||
|
"connection": "Connection error",
|
||||||
|
"camera": "Cannot access camera",
|
||||||
|
"bring_add": "Error adding to Bring!",
|
||||||
|
"bring_connection": "Bring! connection error",
|
||||||
|
"identification": "Identification error",
|
||||||
|
"barcode_empty": "Enter a barcode",
|
||||||
|
"barcode_format": "Barcode must contain only numbers (4-14 digits)",
|
||||||
|
"min_chars": "Type at least 2 characters",
|
||||||
|
"not_in_inventory": "Product not in inventory",
|
||||||
|
"appliance_exists": "Appliance already exists",
|
||||||
|
"already_exists": "Already exists"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"remove_item": "Do you really want to remove this product from inventory?"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "Edit {name}"
|
||||||
|
},
|
||||||
|
"screensaver": {
|
||||||
|
"recipe_btn": "Recipes",
|
||||||
|
"scan_btn": "Scan product"
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"mon": "Monday",
|
||||||
|
"tue": "Tuesday",
|
||||||
|
"wed": "Wednesday",
|
||||||
|
"thu": "Thursday",
|
||||||
|
"fri": "Friday",
|
||||||
|
"sat": "Saturday",
|
||||||
|
"sun": "Sunday"
|
||||||
|
},
|
||||||
|
"meal_types": {
|
||||||
|
"lunch": "Lunch",
|
||||||
|
"dinner": "Dinner"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,431 @@
|
|||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"name": "Dispensa Manager",
|
||||||
|
"loading": "Caricamento..."
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"title": "🏠 Dispensa",
|
||||||
|
"home": "Home",
|
||||||
|
"inventory": "Dispensa",
|
||||||
|
"recipes": "Ricette",
|
||||||
|
"shopping": "Spesa",
|
||||||
|
"log": "Log"
|
||||||
|
},
|
||||||
|
"btn": {
|
||||||
|
"back": "← Indietro",
|
||||||
|
"save": "💾 Salva",
|
||||||
|
"cancel": "✕ Annulla",
|
||||||
|
"close": "Chiudi",
|
||||||
|
"add": "✅ Aggiungi",
|
||||||
|
"delete": "Elimina",
|
||||||
|
"edit": "✏️ Modifica",
|
||||||
|
"search": "🔍 Cerca",
|
||||||
|
"go": "✅ Vai",
|
||||||
|
"toggle_password": "👁️ Mostra/Nascondi",
|
||||||
|
"load_more": "Carica altri...",
|
||||||
|
"save_config": "💾 Salva Configurazione",
|
||||||
|
"save_product": "💾 Salva Prodotto",
|
||||||
|
"restart": "↺ Ricomincia",
|
||||||
|
"reset_default": "↺ Ripristina default"
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"dispensa": "Dispensa",
|
||||||
|
"frigo": "Frigo",
|
||||||
|
"freezer": "Freezer",
|
||||||
|
"altro": "Altro"
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"latticini": "Latticini",
|
||||||
|
"carne": "Carne",
|
||||||
|
"pesce": "Pesce",
|
||||||
|
"frutta": "Frutta",
|
||||||
|
"verdura": "Verdura",
|
||||||
|
"pasta": "Pasta & Riso",
|
||||||
|
"pane": "Pane & Forno",
|
||||||
|
"surgelati": "Surgelati",
|
||||||
|
"bevande": "Bevande",
|
||||||
|
"condimenti": "Condimenti",
|
||||||
|
"snack": "Snack & Dolci",
|
||||||
|
"conserve": "Conserve",
|
||||||
|
"cereali": "Cereali & Legumi",
|
||||||
|
"igiene": "Igiene",
|
||||||
|
"pulizia": "Pulizia Casa",
|
||||||
|
"altro": "Altro",
|
||||||
|
"select": "-- Seleziona --"
|
||||||
|
},
|
||||||
|
"units": {
|
||||||
|
"pz": "pz",
|
||||||
|
"conf": "conf",
|
||||||
|
"g": "g",
|
||||||
|
"ml": "ml",
|
||||||
|
"pieces": "Pezzi",
|
||||||
|
"grams": "Grammi",
|
||||||
|
"box": "Confezione",
|
||||||
|
"boxes": "Confezioni"
|
||||||
|
},
|
||||||
|
"shopping_sections": {
|
||||||
|
"frutta_verdura": "Frutta & Verdura",
|
||||||
|
"carne_pesce": "Carne & Pesce",
|
||||||
|
"latticini": "Latticini & Fresco",
|
||||||
|
"pane_dolci": "Pane & Dolci",
|
||||||
|
"pasta": "Pasta & Cereali",
|
||||||
|
"conserve": "Conserve & Salse",
|
||||||
|
"surgelati": "Surgelati",
|
||||||
|
"bevande": "Bevande",
|
||||||
|
"pulizia_igiene": "Pulizia & Igiene",
|
||||||
|
"altro": "Altro"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"expired_title": "🚫 Scaduti",
|
||||||
|
"expiring_title": "⏰ Prossime Scadenze",
|
||||||
|
"stats_period": "📊 Ultimi 30 giorni",
|
||||||
|
"opened_title": "📦 Prodotti Aperti",
|
||||||
|
"review_title": "🔍 Da revisionare",
|
||||||
|
"review_hint": "Quantità che sembrano anomale. Conferma se corrette o modifica.",
|
||||||
|
"quick_recipe": "🍳 Ricetta veloce con prodotti in scadenza"
|
||||||
|
},
|
||||||
|
"inventory": {
|
||||||
|
"title": "Dispensa",
|
||||||
|
"filter_all": "Tutti",
|
||||||
|
"search_placeholder": "🔍 Cerca prodotto...",
|
||||||
|
"empty": "Nessun prodotto qui.\nScansiona un prodotto per aggiungerlo!",
|
||||||
|
"no_items_found": "Nessuna voce di inventario trovata"
|
||||||
|
},
|
||||||
|
"scan": {
|
||||||
|
"title": "Scansiona Prodotto",
|
||||||
|
"mode_shopping": "🛒 Modalità Spesa",
|
||||||
|
"mode_shopping_end": "✅ Fine spesa",
|
||||||
|
"zoom": "Zoom",
|
||||||
|
"barcode_placeholder": "Inserisci codice a barre...",
|
||||||
|
"quick_name_divider": "oppure scrivi il nome",
|
||||||
|
"quick_name_placeholder": "Es: Mele, Zucchine, Pane...",
|
||||||
|
"manual_entry": "✏️ Inserimento Manuale",
|
||||||
|
"ai_identify": "🤖 Identifica con AI",
|
||||||
|
"hint": "Scansiona il barcode, scrivi il nome del prodotto, oppure usa l'AI per identificarlo",
|
||||||
|
"debug_toggle": "🐛 Debug Log",
|
||||||
|
"barcode_acquired": "🔖 Barcode acquisito: {code}",
|
||||||
|
"scan_barcode": "🔖 Scansiona Barcode"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"title": "Cosa vuoi fare?",
|
||||||
|
"add_btn": "📥 AGGIUNGI",
|
||||||
|
"add_sub": "in dispensa/frigo",
|
||||||
|
"use_btn": "📤 USA / CONSUMA",
|
||||||
|
"use_sub": "dalla dispensa/frigo"
|
||||||
|
},
|
||||||
|
"add": {
|
||||||
|
"title": "Aggiungi alla Dispensa",
|
||||||
|
"location_label": "📍 Dove lo metti?",
|
||||||
|
"quantity_label": "📦 Quantità",
|
||||||
|
"conf_size_label": "📦 Ogni confezione contiene:",
|
||||||
|
"conf_size_placeholder": "es. 300",
|
||||||
|
"vacuum_label": "🫙 Sotto vuoto",
|
||||||
|
"vacuum_hint": "La scadenza verrà estesa automaticamente",
|
||||||
|
"submit": "✅ Aggiungi"
|
||||||
|
},
|
||||||
|
"use": {
|
||||||
|
"title": "Usa / Consuma",
|
||||||
|
"location_label": "📍 Da dove?",
|
||||||
|
"quantity_label": "Quanto hai usato?",
|
||||||
|
"partial_hint": "Oppure specifica la quantità usata:",
|
||||||
|
"use_all": "🗑️ Usato TUTTO / Finito",
|
||||||
|
"submit": "📤 Usa questa quantità",
|
||||||
|
"available": "📦 Disponibile:",
|
||||||
|
"not_in_inventory": "⚠️ Prodotto non presente nell'inventario.",
|
||||||
|
"expiry_warning": "⚠️ Usa prima quella{loc} che scade il {date} — {when}!"
|
||||||
|
},
|
||||||
|
"product": {
|
||||||
|
"title_new": "Nuovo Prodotto",
|
||||||
|
"title_edit": "Modifica Prodotto",
|
||||||
|
"ai_fill": "📷 Scatta foto e identifica con AI",
|
||||||
|
"ai_fill_hint": "L'AI compilerà automaticamente i campi del prodotto",
|
||||||
|
"name_label": "🏷️ Nome Prodotto *",
|
||||||
|
"name_placeholder": "Es: Latte intero, Pasta penne rigate...",
|
||||||
|
"brand_label": "🏢 Marca",
|
||||||
|
"brand_placeholder": "Es: Barilla, Granarolo, Mutti...",
|
||||||
|
"category_label": "📂 Categoria",
|
||||||
|
"unit_label": "📏 Unità di misura",
|
||||||
|
"default_qty_label": "🔢 Quantità default",
|
||||||
|
"conf_size_label": "📦 Ogni confezione contiene:",
|
||||||
|
"conf_size_placeholder": "es. 300",
|
||||||
|
"notes_label": "📝 Note",
|
||||||
|
"notes_placeholder": "Es: senza lattosio, bio, conservare in frigo dopo apertura...",
|
||||||
|
"barcode_label": "🔖 Barcode",
|
||||||
|
"barcode_placeholder": "Codice a barre (se disponibile)",
|
||||||
|
"barcode_hint": "⚠️ Aggiungi il barcode così al prossimo acquisto basta scansionarlo!",
|
||||||
|
"submit": "💾 Salva Prodotto",
|
||||||
|
"name_required": "Inserisci il nome del prodotto",
|
||||||
|
"conf_size_required": "Specifica il contenuto di ogni confezione",
|
||||||
|
"expiry_estimated": "Scadenza stimata:",
|
||||||
|
"scan_expiry": "Scansiona data scadenza",
|
||||||
|
"expiry_hint": "📝 Puoi modificare la data o scansionarla con la fotocamera",
|
||||||
|
"add_batch": "📦 + Lotto con scadenza diversa",
|
||||||
|
"package_info": "📦 Confezione: {info}",
|
||||||
|
"edit_catalog": "⚙️ Modifica scheda prodotto (nome, marca, categoria…)",
|
||||||
|
"not_recognized": "⚠️ Prodotto non riconosciuto",
|
||||||
|
"edit_info": "✏️ Modifica informazioni",
|
||||||
|
"modify_details": "MODIFICA\nscadenza, luogo…"
|
||||||
|
},
|
||||||
|
"products": {
|
||||||
|
"title": "📦 Tutti i Prodotti",
|
||||||
|
"search_placeholder": "🔍 Cerca prodotto...",
|
||||||
|
"empty": "Nessun prodotto nel database.\nScansiona un prodotto per iniziare!",
|
||||||
|
"no_category": "Nessun prodotto in questa categoria"
|
||||||
|
},
|
||||||
|
"recipes": {
|
||||||
|
"title": "🍳 Ricette",
|
||||||
|
"generate": "✨ Genera nuova ricetta"
|
||||||
|
},
|
||||||
|
"shopping": {
|
||||||
|
"title": "🛒 Lista della Spesa",
|
||||||
|
"bring_loading": "Connessione a Bring!...",
|
||||||
|
"tab_to_buy": "🛍️ Da comprare",
|
||||||
|
"tab_forecast": "🧠 In previsione",
|
||||||
|
"total_label": "💰 Totale stimato",
|
||||||
|
"section_to_buy": "🛍️ Da comprare",
|
||||||
|
"suggestions_title": "💡 Suggerimenti AI",
|
||||||
|
"suggestions_add": "✅ Aggiungi selezionati a Bring!",
|
||||||
|
"search_prices": "🔍 Cerca tutti i prezzi",
|
||||||
|
"suggest_btn": "🤖 Suggerisci cosa comprare",
|
||||||
|
"smart_title": "🧠 Previsioni intelligenti",
|
||||||
|
"smart_empty": "Nessuna previsione disponibile.<br>Aggiungi prodotti alla dispensa per ricevere previsioni intelligenti.",
|
||||||
|
"smart_filter_all": "Tutti",
|
||||||
|
"smart_filter_critical": "🔴 Urgenti",
|
||||||
|
"smart_filter_high": "🟠 Presto",
|
||||||
|
"smart_filter_medium": "🟡 Pianifica",
|
||||||
|
"smart_filter_low": "🟢 Previsione",
|
||||||
|
"smart_add": "🛒 Aggiungi selezionati a Bring!",
|
||||||
|
"empty": "Lista della spesa vuota!\nUsa il pulsante sotto per generare suggerimenti.",
|
||||||
|
"already_in_list": "🛒 \"{name}\" già nella lista della spesa",
|
||||||
|
"already_in_list_short": "ℹ️ Già nella lista della spesa",
|
||||||
|
"add_prompt": "Vuoi aggiungerlo alla lista della spesa?",
|
||||||
|
"smart_already": "📊 La spesa intelligente prevede già {name}",
|
||||||
|
"all_searched": "Tutti i prodotti sono già stati cercati. Usa 🔄 per ricercare singoli.",
|
||||||
|
"search_complete": "Ricerca completata: {count} prodotti",
|
||||||
|
"removed_sufficient": "🧹 {removed} prodotto/i con scorte sufficienti rimosso/i dalla lista"
|
||||||
|
},
|
||||||
|
"ai": {
|
||||||
|
"title": "🤖 Identificazione AI",
|
||||||
|
"capture": "📸 Scatta Foto",
|
||||||
|
"retake": "🔄 Riscatta",
|
||||||
|
"hint": "Scatta una foto del prodotto e l'AI cercherà di identificarlo",
|
||||||
|
"identifying": "🤖 Identifico il prodotto...",
|
||||||
|
"no_api_key": "⚠️ Chiave API Gemini non configurata.\n<small>Aggiungi GEMINI_API_KEY nel file .env sul server.</small>",
|
||||||
|
"fields_filled": "✅ Campi compilati dall'AI"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"title": "📒 Log Operazioni"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "Gemini Chef",
|
||||||
|
"welcome": "Ciao! Sono il tuo assistente cucina",
|
||||||
|
"welcome_desc": "Chiedimi di prepararti un succo, uno spuntino, un piatto veloce... Conosco la tua dispensa, i tuoi elettrodomestici e le tue preferenze!",
|
||||||
|
"suggestion_snack": "🍿 Spuntino veloce",
|
||||||
|
"suggestion_juice": "🥤 Succo/Frullato",
|
||||||
|
"suggestion_light": "🥗 Qualcosa di leggero",
|
||||||
|
"suggestion_expiry": "⏰ Usa le scadenze",
|
||||||
|
"clear": "Nuova conversazione",
|
||||||
|
"placeholder": "Chiedi qualcosa..."
|
||||||
|
},
|
||||||
|
"cooking": {
|
||||||
|
"close": "Chiudi",
|
||||||
|
"tts_btn": "Leggi ad alta voce",
|
||||||
|
"restart": "↺ Ricomincia",
|
||||||
|
"replay": "🔊 Rileggi",
|
||||||
|
"timer": "⏱️ {time} · Timer",
|
||||||
|
"prev": "◀ Precedente",
|
||||||
|
"next": "Successivo ▶"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "⚙️ Configurazione",
|
||||||
|
"tab_api": "API Keys",
|
||||||
|
"tab_bring": "Bring!",
|
||||||
|
"tab_recipe": "Ricette",
|
||||||
|
"tab_mealplan": "Piano Settimanale",
|
||||||
|
"tab_appliances": "Elettrodomestici",
|
||||||
|
"tab_spesa": "Spesa Online",
|
||||||
|
"tab_camera": "Fotocamera",
|
||||||
|
"tab_security": "Sicurezza",
|
||||||
|
"tab_tts": "Voce (TTS)",
|
||||||
|
"tab_language": "Lingua",
|
||||||
|
"gemini": {
|
||||||
|
"title": "🤖 Google Gemini AI",
|
||||||
|
"hint": "Chiave API per identificazione prodotti, scadenze e ricette.",
|
||||||
|
"key_label": "API Key Gemini"
|
||||||
|
},
|
||||||
|
"bring": {
|
||||||
|
"title": "🛒 Bring! Shopping List",
|
||||||
|
"hint": "Credenziali per l'integrazione con la lista della spesa Bring!",
|
||||||
|
"email_label": "📧 Email Bring!",
|
||||||
|
"password_label": "🔒 Password Bring!"
|
||||||
|
},
|
||||||
|
"recipe": {
|
||||||
|
"title": "🍳 Preferenze Ricette",
|
||||||
|
"hint": "Configura le opzioni predefinite per la generazione delle ricette.",
|
||||||
|
"persons_label": "👥 Persone predefinite",
|
||||||
|
"options_label": "🎯 Opzioni ricetta predefinite",
|
||||||
|
"fast": "⚡ Pasto Veloce",
|
||||||
|
"light": "🥗 Poca Fame",
|
||||||
|
"expiry": "⏰ Priorità Scadenze",
|
||||||
|
"healthy": "💚 Extra Salutare",
|
||||||
|
"opened": "📦 Priorità Cose Aperte",
|
||||||
|
"zerowaste": "♻️ Zero Sprechi",
|
||||||
|
"dietary_label": "🚫 Intolleranze / Restrizioni",
|
||||||
|
"dietary_placeholder": "Es: senza glutine, senza lattosio, vegetariano..."
|
||||||
|
},
|
||||||
|
"mealplan": {
|
||||||
|
"title": "📅 Piano Pasti Settimanale",
|
||||||
|
"hint": "Imposta la tipologia di pasto per ogni giorno. Sarà usata come guida nella generazione delle ricette.",
|
||||||
|
"enabled": "✅ Attiva piano pasti settimanale",
|
||||||
|
"legend": "🌤️ = Pranzo · 🌙 = Cena · Tocca un badge per cambiarlo.",
|
||||||
|
"types_title": "📋 Tipologie disponibili"
|
||||||
|
},
|
||||||
|
"appliances": {
|
||||||
|
"title": "🔌 Elettrodomestici Disponibili",
|
||||||
|
"hint": "Indica gli elettrodomestici che hai a disposizione. Saranno considerati nella generazione delle ricette.",
|
||||||
|
"new_placeholder": "Es: Macchina del pane, Bimby, Friggitrice ad aria...",
|
||||||
|
"quick_title": "Aggiungi velocemente:",
|
||||||
|
"oven": "🔥 Forno",
|
||||||
|
"microwave": "📡 Microonde",
|
||||||
|
"air_fryer": "🍟 Friggitrice ad aria",
|
||||||
|
"bread_maker": "🍞 Macchina pane",
|
||||||
|
"bimby": "🤖 Bimby/Cookeo",
|
||||||
|
"mixer": "🌀 Planetaria",
|
||||||
|
"steamer": "♨️ Vaporiera",
|
||||||
|
"pressure_cooker": "🫕 Pentola pressione",
|
||||||
|
"toaster": "🍞 Tostapane",
|
||||||
|
"blender": "🍹 Frullatore",
|
||||||
|
"empty": "Nessun elettrodomestico aggiunto"
|
||||||
|
},
|
||||||
|
"spesa": {
|
||||||
|
"title": "🛍️ Spesa Online",
|
||||||
|
"hint": "Configura il provider per la spesa online.",
|
||||||
|
"provider_label": "🏪 Provider",
|
||||||
|
"email_label": "📧 Email",
|
||||||
|
"password_label": "🔒 Password",
|
||||||
|
"login_btn": "🔐 Accedi",
|
||||||
|
"ai_prompt_label": "🤖 Prompt AI selezione prodotto",
|
||||||
|
"ai_prompt_placeholder": "Istruzioni per l'AI quando deve scegliere tra più prodotti...",
|
||||||
|
"ai_prompt_hint": "L'AI usa questo prompt per scegliere il prodotto più appropriato tra i risultati. Lascia vuoto per il comportamento predefinito.",
|
||||||
|
"configure_first": "Configura prima la Spesa Online nelle impostazioni"
|
||||||
|
},
|
||||||
|
"camera": {
|
||||||
|
"title": "📷 Fotocamera",
|
||||||
|
"hint": "Scegli quale fotocamera utilizzare per la scansione barcode e l'identificazione AI.",
|
||||||
|
"device_label": "📸 Fotocamera predefinita",
|
||||||
|
"back": "📱 Posteriore (default)",
|
||||||
|
"front": "🤳 Anteriore",
|
||||||
|
"devices_hint": "Se hai più fotocamere, puoi selezionarne una specifica dall'elenco sopra dopo aver concesso i permessi.",
|
||||||
|
"detect_btn": "🔄 Rileva fotocamere"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"title": "🔒 Certificato HTTPS",
|
||||||
|
"hint": "Se il browser mostra l'errore \"La connessione non è privata\" (ERR_CERT_AUTHORITY_INVALID), devi installare il certificato CA nel dispositivo.",
|
||||||
|
"download_btn": "📥 Scarica Certificato CA"
|
||||||
|
},
|
||||||
|
"tts": {
|
||||||
|
"title": "🔊 Voce & TTS",
|
||||||
|
"hint": "Configura la sintesi vocale tramite qualsiasi API REST esterna. I passi della ricetta e i timer scaduti verranno inviati all'endpoint configurato.",
|
||||||
|
"enabled": "✅ Attiva TTS",
|
||||||
|
"url_label": "🌐 URL Endpoint",
|
||||||
|
"method_label": "📡 Metodo HTTP",
|
||||||
|
"auth_label": "🔐 Autenticazione",
|
||||||
|
"auth_bearer": "Bearer Token",
|
||||||
|
"auth_custom": "Header personalizzato",
|
||||||
|
"auth_none": "Nessuna",
|
||||||
|
"token_label": "🔑 Bearer Token",
|
||||||
|
"custom_header_name": "📋 Nome header",
|
||||||
|
"custom_header_value": "📋 Valore header",
|
||||||
|
"content_type_label": "📄 Content-Type",
|
||||||
|
"payload_key_label": "🗝️ Campo testo nel payload",
|
||||||
|
"payload_key_hint": "Nome del campo JSON che conterrà il testo da leggere (es: message, text).",
|
||||||
|
"extra_fields_label": "➕ Campi extra (JSON)",
|
||||||
|
"extra_fields_placeholder": "{\"entity_id\": \"media_player.living_room\"}",
|
||||||
|
"extra_fields_hint": "Campi aggiuntivi da includere nel payload, in formato JSON. Lascia vuoto se non necessario.",
|
||||||
|
"test_btn": "🔊 Invia Test Vocale"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"title": "🌐 Lingua / Language",
|
||||||
|
"hint": "Seleziona la lingua dell'interfaccia. Select the interface language.",
|
||||||
|
"label": "🌐 Lingua",
|
||||||
|
"restart_notice": "La pagina verrà ricaricata per applicare la nuova lingua."
|
||||||
|
},
|
||||||
|
"saved": "✅ Configurazione salvata!",
|
||||||
|
"saved_local": "✅ Configurazione salvata localmente",
|
||||||
|
"saved_local_error": "⚠️ Salvato localmente, errore server: {error}"
|
||||||
|
},
|
||||||
|
"expiry": {
|
||||||
|
"today": "OGGI",
|
||||||
|
"tomorrow": "Domani",
|
||||||
|
"days": "{days} giorni",
|
||||||
|
"expired_days": "Da {days}g",
|
||||||
|
"expired_yesterday": "Da ieri",
|
||||||
|
"expired_today": "Oggi"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"ok": "OK",
|
||||||
|
"check": "Controlla",
|
||||||
|
"discard": "Buttare"
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
"product_saved": "Prodotto salvato!",
|
||||||
|
"product_created": "Prodotto creato!",
|
||||||
|
"product_updated": "✅ Prodotto aggiornato!",
|
||||||
|
"product_removed": "Prodotto rimosso",
|
||||||
|
"updated": "Aggiornato!",
|
||||||
|
"quantity_confirmed": "✓ Quantità confermata",
|
||||||
|
"added_to_inventory": "✅ {name} aggiunto!",
|
||||||
|
"removed_from_list": "✅ {name} rimosso dalla lista!",
|
||||||
|
"removed_from_list_short": "Rimosso dalla lista",
|
||||||
|
"added_to_shopping": "🛒 Aggiunto alla lista della spesa!",
|
||||||
|
"removed_from_shopping": "🛒 Rimosso dalla lista della spesa",
|
||||||
|
"finished_to_bring": "🛒 Prodotto finito → aggiunto a Bring!",
|
||||||
|
"thrown_away": "🗑️ {name} buttato!",
|
||||||
|
"thrown_away_partial": "🗑️ Buttato {qty} {unit} di {name}",
|
||||||
|
"appliance_added": "Elettrodomestico aggiunto",
|
||||||
|
"item_added": "{name} aggiunto"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"generic": "Errore",
|
||||||
|
"loading": "Errore nel caricamento del prodotto",
|
||||||
|
"not_found": "Prodotto non trovato",
|
||||||
|
"not_found_manual": "Prodotto non trovato. Inseriscilo manualmente.",
|
||||||
|
"search": "Errore nella ricerca. Riprova.",
|
||||||
|
"search_short": "Errore nella ricerca",
|
||||||
|
"save": "Errore nel salvataggio",
|
||||||
|
"connection": "Errore di connessione",
|
||||||
|
"camera": "Impossibile accedere alla fotocamera",
|
||||||
|
"bring_add": "Errore nell'aggiunta a Bring!",
|
||||||
|
"bring_connection": "Errore connessione Bring!",
|
||||||
|
"identification": "Errore nell'identificazione",
|
||||||
|
"barcode_empty": "Inserisci un codice a barre",
|
||||||
|
"barcode_format": "Il codice a barre deve contenere solo numeri (4-14 cifre)",
|
||||||
|
"min_chars": "Scrivi almeno 2 caratteri",
|
||||||
|
"not_in_inventory": "Prodotto non nell'inventario",
|
||||||
|
"appliance_exists": "Elettrodomestico già presente",
|
||||||
|
"already_exists": "Già presente"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"remove_item": "Vuoi davvero rimuovere questo prodotto dall'inventario?"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "Modifica {name}"
|
||||||
|
},
|
||||||
|
"screensaver": {
|
||||||
|
"recipe_btn": "Ricette",
|
||||||
|
"scan_btn": "Scansiona prodotto"
|
||||||
|
},
|
||||||
|
"days": {
|
||||||
|
"mon": "Lunedì",
|
||||||
|
"tue": "Martedì",
|
||||||
|
"wed": "Mercoledì",
|
||||||
|
"thu": "Giovedì",
|
||||||
|
"fri": "Venerdì",
|
||||||
|
"sat": "Sabato",
|
||||||
|
"sun": "Domenica"
|
||||||
|
},
|
||||||
|
"meal_types": {
|
||||||
|
"lunch": "Pranzo",
|
||||||
|
"dinner": "Cena"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user