feat: add localisation

This commit is contained in:
2026-07-31 21:46:05 +02:00
parent b58c9a8722
commit f5b2041612
30 changed files with 4099 additions and 326 deletions
+48 -43
View File
@@ -10,6 +10,8 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/product.dart';
import '../viewmodels/product_list_view_model.dart';
import '../l10n/app_localizations.dart';
import '../l10n/app_localizations_helpers.dart';
class ProductFormView extends StatefulWidget {
final String? productId;
@@ -48,6 +50,7 @@ class _ProductFormViewState extends State<ProductFormView> {
}
Future<void> _loadProductIfNeeded() async {
final l10n = AppLocalizations.of(context);
if (!widget.isEditing) {
setState(() => _isLoading = false);
return;
@@ -62,7 +65,7 @@ class _ProductFormViewState extends State<ProductFormView> {
if (product == null) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Product not found.')));
).showSnackBar(SnackBar(content: Text(l10n.productNotFound)));
context.go('/products');
return;
@@ -83,7 +86,7 @@ class _ProductFormViewState extends State<ProductFormView> {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Could not load product: $e')));
).showSnackBar(SnackBar(content: Text(l10n.couldNotLoadProduct)));
context.go('/products');
}
}
@@ -120,11 +123,11 @@ class _ProductFormViewState extends State<ProductFormView> {
Future<void> _pickImage() async {
final pickedFile = await _imagePicker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
maxWidth: 1000,
maxHeight: 1000,
);
source: ImageSource.gallery,
imageQuality: 80,
maxWidth: 1000,
maxHeight: 1000,
);
if (pickedFile == null) return;
@@ -146,19 +149,20 @@ class _ProductFormViewState extends State<ProductFormView> {
}
Future<void> _save() async {
final l10n = AppLocalizations.of(context);
if (!_formKey.currentState!.validate()) return;
if (_imagePath == null || _imagePath!.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Choose a product image.')));
).showSnackBar(SnackBar(content: Text(l10n.chooseImage)));
return;
}
if (_selectedCategory == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select a category.')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.selectCategory)));
return;
}
@@ -198,9 +202,9 @@ class _ProductFormViewState extends State<ProductFormView> {
Sentry.captureException(e, stackTrace: stack);
if (!mounted) return;
setState(() => _isSaving = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not save product: $e')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.couldNotSaveProduct)));
return;
}
@@ -212,23 +216,22 @@ class _ProductFormViewState extends State<ProductFormView> {
Future<void> _delete() async {
if (!widget.isEditing) return;
final l10n = AppLocalizations.of(context);
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Delete product?'),
content: const Text(
'This will remove the product from the product list.',
),
title: Text(l10n.deleteProduct),
content: Text(l10n.deleteProductDescription),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Cancel'),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Delete'),
child: Text(l10n.delete),
),
],
);
@@ -244,9 +247,9 @@ class _ProductFormViewState extends State<ProductFormView> {
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not delete product: $e')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.couldNotDeleteProduct)));
return;
}
@@ -256,6 +259,7 @@ class _ProductFormViewState extends State<ProductFormView> {
}
Widget _buildImagePicker(BuildContext context) {
final l10n = AppLocalizations.of(context);
return InkWell(
onTap: _pickImage,
@@ -289,7 +293,7 @@ class _ProductFormViewState extends State<ProductFormView> {
child: FilledButton.icon(
onPressed: _pickImage,
icon: const Icon(Icons.image),
label: const Text('Change image'),
label: Text(l10n.changeImage),
),
),
],
@@ -301,7 +305,7 @@ class _ProductFormViewState extends State<ProductFormView> {
const Icon(Icons.add_photo_alternate_outlined, size: 48),
const SizedBox(height: 12),
Text(
'Choose product image',
l10n.chooseProductImage,
style: Theme.of(context).textTheme.titleMedium,
),
],
@@ -313,7 +317,8 @@ class _ProductFormViewState extends State<ProductFormView> {
@override
Widget build(BuildContext context) {
final title = widget.isEditing ? 'Edit product' : 'Add product';
final l10n = AppLocalizations.of(context);
final title = widget.isEditing ? l10n.editProduct : l10n.addProduct;
return Scaffold(
appBar: AppBar(
@@ -345,14 +350,14 @@ class _ProductFormViewState extends State<ProductFormView> {
const SizedBox(height: 24),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Product name',
decoration: InputDecoration(
labelText: l10n.productName,
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Enter a product name.';
return l10n.enterProductName;
}
return null;
@@ -370,13 +375,13 @@ class _ProductFormViewState extends State<ProductFormView> {
enableSearch: true,
controller: _categoryController,
requestFocusOnTap: true,
label: const Text('Category'),
hintText: 'Select a category',
label: Text(l10n.category),
hintText: l10n.selectCategory,
dropdownMenuEntries: viewModel.categories
.map(
(category) => DropdownMenuEntry<String>(
value: category,
label: category,
label: l10n.categoryLabel(category),
),
)
.toList(),
@@ -393,8 +398,8 @@ class _ProductFormViewState extends State<ProductFormView> {
const SizedBox(height: 16),
TextFormField(
controller: _priceController,
decoration: const InputDecoration(
labelText: 'Price',
decoration: InputDecoration(
labelText: l10n.price,
prefixText: '',
border: OutlineInputBorder(),
),
@@ -404,14 +409,14 @@ class _ProductFormViewState extends State<ProductFormView> {
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Enter a price.';
return l10n.enterPrice;
}
final normalized = value.replaceAll(',', '.');
final price = double.tryParse(normalized);
if (price == null || price < 0) {
return 'Enter a valid price.';
return l10n.enterValidPrice;
}
return null;
@@ -420,8 +425,8 @@ class _ProductFormViewState extends State<ProductFormView> {
const SizedBox(height: 16),
TextFormField(
controller: _stockController,
decoration: const InputDecoration(
labelText: 'Current stock',
decoration: InputDecoration(
labelText: l10n.stock,
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
@@ -430,7 +435,7 @@ class _ProductFormViewState extends State<ProductFormView> {
final number = int.tryParse(value ?? '');
if (number == null || number < 0) {
return 'Enter a valid stock amount.';
return l10n.enterValidStock;
}
return null;
@@ -439,8 +444,8 @@ class _ProductFormViewState extends State<ProductFormView> {
const SizedBox(height: 16),
TextFormField(
controller: _lowStockController,
decoration: const InputDecoration(
labelText: 'Low stock warning threshold',
decoration: InputDecoration(
labelText: l10n.lowStockThreshold,
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
@@ -448,7 +453,7 @@ class _ProductFormViewState extends State<ProductFormView> {
final number = int.tryParse(value ?? '');
if (number == null || number < 0) {
return 'Enter a valid threshold.';
return l10n.enterValidThreshold;
}
return null;
@@ -467,7 +472,7 @@ class _ProductFormViewState extends State<ProductFormView> {
)
: const Icon(Icons.save),
label: Text(
widget.isEditing ? 'Save changes' : 'Add product',
widget.isEditing ? l10n.confirm : l10n.addProduct,
),
),
],