feat: add localisation
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user