From f5b2041612286854f7d921421c5ee700baf5fcc7 Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Fri, 31 Jul 2026 21:46:05 +0200 Subject: [PATCH] feat: add localisation --- l10n.yaml | 5 + lib/app/app.dart | 20 +- lib/database/app_database.dart | 8 +- lib/database/app_database.g.dart | 86 +- lib/l10n/app_en.arb | 239 ++++ lib/l10n/app_localizations.dart | 1454 +++++++++++++++++++++++ lib/l10n/app_localizations_en.dart | 723 +++++++++++ lib/l10n/app_localizations_helpers.dart | 157 +++ lib/l10n/app_localizations_nl.dart | 734 ++++++++++++ lib/l10n/app_nl.arb | 239 ++++ lib/models/settings.dart | 20 +- lib/services/settings_service.dart | 5 + lib/utils/app_updater.dart | 11 +- lib/viewmodels/settings_view_model.dart | 3 + lib/views/bar_screen_view.dart | 85 +- lib/views/dev_menu_view.dart | 117 +- lib/views/dialogs/close_tab_dialog.dart | 38 +- lib/views/dialogs/new_tab_dialog.dart | 16 +- lib/views/error_screen_view.dart | 23 +- lib/views/history_screen_view.dart | 50 +- lib/views/pin_lock_view.dart | 31 +- lib/views/product_form_view.dart | 91 +- lib/views/product_list_view.dart | 29 +- lib/views/settings_view.dart | 128 +- lib/views/update_progress_view.dart | 61 +- lib/views/widgets/closed_tab_card.dart | 28 +- lib/views/widgets/product_tile.dart | 5 +- lib/views/widgets/slide_confirm.dart | 5 +- pubspec.lock | 9 +- pubspec.yaml | 5 +- 30 files changed, 4099 insertions(+), 326 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_helpers.dart create mode 100644 lib/l10n/app_localizations_nl.dart create mode 100644 lib/l10n/app_nl.arb diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..a4d323c --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,5 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations +nullable-getter: false diff --git a/lib/app/app.dart b/lib/app/app.dart index 607b6f7..a6300e0 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; import '../models/settings.dart'; import '../viewmodels/settings_view_model.dart'; import '../theme.dart'; +import '../l10n/app_localizations.dart'; class KoolTabApp extends StatefulWidget { const KoolTabApp({super.key, required this.router}); @@ -30,9 +31,12 @@ class _KoolTabAppState extends State { final settings = context.watch().settings; return MaterialApp.router( - title: 'KoolTab', + onGenerateTitle: (context) => AppLocalizations.of(context).appTitle, debugShowCheckedModeBanner: false, routerConfig: widget.router, + locale: _localeFor(settings.language), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, theme: lightTheme, darkTheme: _themeFor(settings.themeMode), themeMode: _mapThemeMode(settings.themeMode, context), @@ -46,16 +50,22 @@ class _KoolTabAppState extends State { AppThemeMode.dark => ThemeMode.dark, AppThemeMode.light => ThemeMode.light, AppThemeMode.ugly => ThemeMode.dark, - AppThemeMode.system => - hasLoaded ? ThemeMode.system : ThemeMode.dark, + AppThemeMode.system => hasLoaded ? ThemeMode.system : ThemeMode.dark, }; } - ThemeData _themeFor(AppThemeMode mode) { return switch (mode) { AppThemeMode.ugly => uglyTheme, _ => darkTheme, }; } -} \ No newline at end of file + + Locale? _localeFor(AppLanguage language) { + return switch (language) { + AppLanguage.system => null, + AppLanguage.english => const Locale('en'), + AppLanguage.dutch => const Locale('nl'), + }; + } +} diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index f6b0666..66a8ff8 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -109,6 +109,8 @@ class AppSettingsTable extends Table { TextColumn get themeMode => text().withDefault(const Constant('system'))(); + TextColumn get language => text().withDefault(const Constant('system'))(); + @override Set get primaryKey => {id}; } @@ -129,7 +131,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); @override - int get schemaVersion => 6; + int get schemaVersion => 7; @override MigrationStrategy get migration { @@ -159,6 +161,10 @@ class AppDatabase extends _$AppDatabase { if (from < 6) { await migrator.addColumn(closedTabs, closedTabs.paymentMethod); } + + if (from < 7) { + await migrator.addColumn(appSettingsTable, appSettingsTable.language); + } }, ); } diff --git a/lib/database/app_database.g.dart b/lib/database/app_database.g.dart index 7c62f3e..de3a94b 100644 --- a/lib/database/app_database.g.dart +++ b/lib/database/app_database.g.dart @@ -2219,8 +2219,20 @@ class $AppSettingsTableTable extends AppSettingsTable requiredDuringInsert: false, defaultValue: const Constant('system'), ); + static const VerificationMeta _languageMeta = const VerificationMeta( + 'language', + ); @override - List get $columns => [id, pinRequired, themeMode]; + late final GeneratedColumn language = GeneratedColumn( + 'language', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('system'), + ); + @override + List get $columns => [id, pinRequired, themeMode, language]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -2253,6 +2265,12 @@ class $AppSettingsTableTable extends AppSettingsTable themeMode.isAcceptableOrUnknown(data['theme_mode']!, _themeModeMeta), ); } + if (data.containsKey('language')) { + context.handle( + _languageMeta, + language.isAcceptableOrUnknown(data['language']!, _languageMeta), + ); + } return context; } @@ -2274,6 +2292,10 @@ class $AppSettingsTableTable extends AppSettingsTable DriftSqlType.string, data['${effectivePrefix}theme_mode'], )!, + language: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}language'], + )!, ); } @@ -2287,10 +2309,12 @@ class AppSettingsRow extends DataClass implements Insertable { final String id; final bool pinRequired; final String themeMode; + final String language; const AppSettingsRow({ required this.id, required this.pinRequired, required this.themeMode, + required this.language, }); @override Map toColumns(bool nullToAbsent) { @@ -2298,6 +2322,7 @@ class AppSettingsRow extends DataClass implements Insertable { map['id'] = Variable(id); map['pin_required'] = Variable(pinRequired); map['theme_mode'] = Variable(themeMode); + map['language'] = Variable(language); return map; } @@ -2306,6 +2331,7 @@ class AppSettingsRow extends DataClass implements Insertable { id: Value(id), pinRequired: Value(pinRequired), themeMode: Value(themeMode), + language: Value(language), ); } @@ -2318,6 +2344,7 @@ class AppSettingsRow extends DataClass implements Insertable { id: serializer.fromJson(json['id']), pinRequired: serializer.fromJson(json['pinRequired']), themeMode: serializer.fromJson(json['themeMode']), + language: serializer.fromJson(json['language']), ); } @override @@ -2327,15 +2354,21 @@ class AppSettingsRow extends DataClass implements Insertable { 'id': serializer.toJson(id), 'pinRequired': serializer.toJson(pinRequired), 'themeMode': serializer.toJson(themeMode), + 'language': serializer.toJson(language), }; } - AppSettingsRow copyWith({String? id, bool? pinRequired, String? themeMode}) => - AppSettingsRow( - id: id ?? this.id, - pinRequired: pinRequired ?? this.pinRequired, - themeMode: themeMode ?? this.themeMode, - ); + AppSettingsRow copyWith({ + String? id, + bool? pinRequired, + String? themeMode, + String? language, + }) => AppSettingsRow( + id: id ?? this.id, + pinRequired: pinRequired ?? this.pinRequired, + themeMode: themeMode ?? this.themeMode, + language: language ?? this.language, + ); AppSettingsRow copyWithCompanion(AppSettingsTableCompanion data) { return AppSettingsRow( id: data.id.present ? data.id.value : this.id, @@ -2343,6 +2376,7 @@ class AppSettingsRow extends DataClass implements Insertable { ? data.pinRequired.value : this.pinRequired, themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, + language: data.language.present ? data.language.value : this.language, ); } @@ -2351,49 +2385,56 @@ class AppSettingsRow extends DataClass implements Insertable { return (StringBuffer('AppSettingsRow(') ..write('id: $id, ') ..write('pinRequired: $pinRequired, ') - ..write('themeMode: $themeMode') + ..write('themeMode: $themeMode, ') + ..write('language: $language') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, pinRequired, themeMode); + int get hashCode => Object.hash(id, pinRequired, themeMode, language); @override bool operator ==(Object other) => identical(this, other) || (other is AppSettingsRow && other.id == this.id && other.pinRequired == this.pinRequired && - other.themeMode == this.themeMode); + other.themeMode == this.themeMode && + other.language == this.language); } class AppSettingsTableCompanion extends UpdateCompanion { final Value id; final Value pinRequired; final Value themeMode; + final Value language; final Value rowid; const AppSettingsTableCompanion({ this.id = const Value.absent(), this.pinRequired = const Value.absent(), this.themeMode = const Value.absent(), + this.language = const Value.absent(), this.rowid = const Value.absent(), }); AppSettingsTableCompanion.insert({ required String id, this.pinRequired = const Value.absent(), this.themeMode = const Value.absent(), + this.language = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id); static Insertable custom({ Expression? id, Expression? pinRequired, Expression? themeMode, + Expression? language, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (pinRequired != null) 'pin_required': pinRequired, if (themeMode != null) 'theme_mode': themeMode, + if (language != null) 'language': language, if (rowid != null) 'rowid': rowid, }); } @@ -2402,12 +2443,14 @@ class AppSettingsTableCompanion extends UpdateCompanion { Value? id, Value? pinRequired, Value? themeMode, + Value? language, Value? rowid, }) { return AppSettingsTableCompanion( id: id ?? this.id, pinRequired: pinRequired ?? this.pinRequired, themeMode: themeMode ?? this.themeMode, + language: language ?? this.language, rowid: rowid ?? this.rowid, ); } @@ -2424,6 +2467,9 @@ class AppSettingsTableCompanion extends UpdateCompanion { if (themeMode.present) { map['theme_mode'] = Variable(themeMode.value); } + if (language.present) { + map['language'] = Variable(language.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -2436,6 +2482,7 @@ class AppSettingsTableCompanion extends UpdateCompanion { ..write('id: $id, ') ..write('pinRequired: $pinRequired, ') ..write('themeMode: $themeMode, ') + ..write('language: $language, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -4023,6 +4070,7 @@ typedef $$AppSettingsTableTableCreateCompanionBuilder = required String id, Value pinRequired, Value themeMode, + Value language, Value rowid, }); typedef $$AppSettingsTableTableUpdateCompanionBuilder = @@ -4030,6 +4078,7 @@ typedef $$AppSettingsTableTableUpdateCompanionBuilder = Value id, Value pinRequired, Value themeMode, + Value language, Value rowid, }); @@ -4056,6 +4105,11 @@ class $$AppSettingsTableTableFilterComposer column: $table.themeMode, builder: (column) => ColumnFilters(column), ); + + ColumnFilters get language => $composableBuilder( + column: $table.language, + builder: (column) => ColumnFilters(column), + ); } class $$AppSettingsTableTableOrderingComposer @@ -4081,6 +4135,11 @@ class $$AppSettingsTableTableOrderingComposer column: $table.themeMode, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get language => $composableBuilder( + column: $table.language, + builder: (column) => ColumnOrderings(column), + ); } class $$AppSettingsTableTableAnnotationComposer @@ -4102,6 +4161,9 @@ class $$AppSettingsTableTableAnnotationComposer GeneratedColumn get themeMode => $composableBuilder(column: $table.themeMode, builder: (column) => column); + + GeneratedColumn get language => + $composableBuilder(column: $table.language, builder: (column) => column); } class $$AppSettingsTableTableTableManager @@ -4144,11 +4206,13 @@ class $$AppSettingsTableTableTableManager Value id = const Value.absent(), Value pinRequired = const Value.absent(), Value themeMode = const Value.absent(), + Value language = const Value.absent(), Value rowid = const Value.absent(), }) => AppSettingsTableCompanion( id: id, pinRequired: pinRequired, themeMode: themeMode, + language: language, rowid: rowid, ), createCompanionCallback: @@ -4156,11 +4220,13 @@ class $$AppSettingsTableTableTableManager required String id, Value pinRequired = const Value.absent(), Value themeMode = const Value.absent(), + Value language = const Value.absent(), Value rowid = const Value.absent(), }) => AppSettingsTableCompanion.insert( id: id, pinRequired: pinRequired, themeMode: themeMode, + language: language, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..e8fbd5d --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,239 @@ +{ + "@@locale": "en", + "appTitle": "KoolTab", + "barTabs": "Bar Tabs", + "products": "Products", + "addProduct": "Add product", + "noProductsYet": "No products yet.", + "addFirstProduct": "Add your first product to start selling.", + "manageProducts": "Manage products", + "tabHistory": "Tab history", + "refresh": "Refresh", + "settings": "Settings", + "logout": "Logout", + "openOrSelectTab": "Open or select a tab first.", + "openNewTab": "Open new tab", + "customerGroupName": "Customer / group name", + "cancel": "Cancel", + "openTab": "Open tab", + "couldNotCreateTab": "Could not create tab: {error}", + "@couldNotCreateTab": {"placeholders": {"error": {"type": "String"}}}, + "searchByName": "Search by name…", + "allCustomers": "All customers", + "customer": "Customer", + "noClosedTabs": "No closed tabs yet", + "settingsTitle": "Settings", + "security": "Security", + "pinRequired": "PIN Required", + "changePin": "Change PIN", + "appearance": "Appearance", + "language": "Language", + "languageSystem": "System default", + "languageEnglish": "English", + "languageDutch": "Dutch", + "theme": "Theme", + "system": "System", + "light": "Light", + "dark": "Dark", + "ugly": "Ugly", + "setPinCode": "Set PIN Code", + "enterPin": "Enter PIN", + "enterCurrentPin": "Enter Current PIN", + "enterNewPin": "Enter New PIN", + "fourDigits": "4 digits", + "enterPinFourDigits": "Enter PIN (4 digits)", + "confirm": "Confirm", + "confirmPin": "Confirm PIN", + "pinExactlyFour": "PIN must be exactly 4 digits", + "couldNotSavePin": "Could not save PIN setting.", + "editProduct": "Edit product", + "productName": "Product name", + "category": "Category", + "price": "Price", + "stock": "Stock", + "lowStockThreshold": "Low stock threshold", + "chooseProductImage": "Choose product image", + "changeImage": "Change image", + "deleteProduct": "Delete product?", + "deleteProductDescription": "This will remove the product from the product list.", + "delete": "Delete", + "productNotFound": "Product not found.", + "chooseImage": "Choose a product image.", + "selectCategory": "Please select a category.", + "enterProductName": "Enter a product name.", + "version": "Version {version}", + "@version": {"placeholders": {"version": {"type": "String"}}}, + "selectTabToAddItems": "Select a tab to add items", + "openTabs": "Open tabs", + "selectOrOpenTab": "Select or open a tab", + "noOpenTabs": "No open tabs", + "edit": "Edit", + "close": "Close", + "tabItemSummary": "{count, plural, =1 {1 item - {total}} other {{count} items - {total}}}", + "@tabItemSummary": {"placeholders": {"count": {"type": "int"}, "total": {"type": "String"}}}, + "tabItemCount": "{count, plural, =1 {1 item} other {{count} items}}", + "@tabItemCount": {"placeholders": {"count": {"type": "int"}}}, + "tapProductsToAdd": "Tap products to add them", + "total": "Total", + "closeTab": "Close tab", + "productStockSummary": "{category} • {price} • Stock: {stock}", + "@productStockSummary": {"placeholders": {"category": {"type": "String"}, "price": {"type": "String"}, "stock": {"type": "int"}}}, + "outOfStock": "OUT OF STOCK", + "tabsYouCloseAppearHere": "Tabs you close will show up here.", + "enterPinSubtitle": "You’ll use this to unlock the app", + "confirmPinSubtitle": "Enter the same PIN again", + "createPin": "Create a PIN", + "pinsDidNotMatch": "PINs didn’t match. Try again.", + "somethingWentWrong": "Something went wrong.", + "incorrectPin": "Incorrect PIN.", + "enterPrice": "Enter a price.", + "enterValidPrice": "Enter a valid price.", + "enterValidStock": "Enter a valid stock amount.", + "enterValidThreshold": "Enter a valid threshold.", + "couldNotLoadProduct": "Could not load product.", + "couldNotSaveProduct": "Could not save product.", + "couldNotDeleteProduct": "Could not delete product.", + "paymentMethod": "Payment method", + "cash": "Cash", + "payconiq": "Payconiq", + "closeTabForCustomer": "Close tab for {customerName}?", + "@closeTabForCustomer": {"placeholders": {"customerName": {"type": "String"}}}, + "currentTotal": "Current total: {total}", + "@currentTotal": {"placeholders": {"total": {"type": "String"}}}, + "couldNotCloseTab": "Could not close tab.", + "closingTab": "Closing tab…", + "slideToConfirmClosing": "Slide to confirm closing", + "errorScreen": "Error Screen", + "unexpectedError": "An unexpected error occurred.\nPlease try restarting the app.", + "goToBarScreen": "Go to bar screen", + "updates": "Updates", + "checkingForUpdates": "Checking for updates…", + "checkForUpdates": "Check for updates", + "latestVersion": "You are already on the latest version.", + "updateCheckFailed": "Update check failed.", + "updateAvailable": "Update available", + "versionAvailable": "Version {version} is available.", + "@versionAvailable": {"placeholders": {"version": {"type": "String"}}}, + "later": "Later", + "update": "Update", + "updateReady": "Update ready", + "updateReadyDescription": "The update has been downloaded and installed. Restart the app now to apply the changes.", + "restartApp": "Restart app", + "updateFailed": "Update failed", + "retry": "Retry", + "updating": "Updating", + "installing": "Installing", + "doNotCloseDuringUpdate": "Do not close the app during the update", + "cancelUpdate": "Cancel update?", + "cancelUpdateDescription": "The update is in progress. If you leave now, the app may become unstable.", + "continueUpdate": "Continue update", + "cancelUpdateAction": "Cancel update", + "preparingDownload": "Preparing download…", + "simulatedDownloadError": "Simulated download error (network timeout)", + "downloadFailed": "Download failed", + "downloading": "Downloading {percent}%", + "@downloading": {"placeholders": {"percent": {"type": "String"}}}, + "installingUpdate": "Installing update…", + "updateInstalledRestarting": "Update installed! Restarting…", + "installationFailed": "Installation failed", + "updateAlreadyInProgress": "Update already in progress", + "updateAlreadyRunning": "Update already running", + "genericError": "An unexpected error occurred.", + "couldNotLoadBar": "Could not load bar screen.", + "couldNotCreateTabMessage": "Could not create tab.", + "productOutOfStock": "Product is out of stock.", + "couldNotAddProductToTab": "Could not add product to tab.", + "couldNotUpdateQuantity": "Could not update item quantity.", + "couldNotReloadTabs": "Could not reload tabs.", + "couldNotLoadHistory": "Could not load tab history.", + "couldNotLoadMoreTabs": "Could not load more tabs.", + "couldNotLoadProducts": "Could not load products.", + "couldNotAddProduct": "Could not add product.", + "couldNotUpdateProduct": "Could not update product.", + "couldNotDeleteProductMessage": "Could not delete product.", + "couldNotCheckPinStatus": "Could not check PIN status.", + "couldNotVerifyPin": "Could not verify PIN.", + "currentPinIncorrect": "Current PIN is incorrect.", + "couldNotLoadSettings": "Could not load settings.", + "couldNotSaveSettings": "Could not save settings.", + "downloadFailedWithDetail": "Download failed.", + "installationFailedWithDetail": "Installation failed.", + "devMenu": "Dev Menu", + "dataManagement": "Data Management", + "clearOpenTabs": "Clear all open tabs", + "clearClosedHistory": "Clear closed tab history", + "seedDefaultProducts": "Seed default products", + "onlySeedsWhenEmpty": "Only seeds if table is empty", + "clearAndReseedProducts": "Clear & reseed products", + "wipesAndReseeds": "Wipes all products, then seeds defaults", + "clearAllProducts": "Clear all products", + "clearProductImages": "Clear product images", + "deletesProductImages": "Deletes images from product_images folder", + "debugging": "Debugging", + "resetPin": "Reset PIN", + "removePinLock": "Remove PIN lock", + "toggleDebugOverlay": "Toggle debug overlay", + "showFpsMemoryWidgets": "Show FPS, memory, widget count", + "errorScreenMenu": "Error screen", + "viewErrorScreen": "View the error screen UI", + "featureToggles": "Feature Toggles", + "simulateUpdateDownload": "Simulate update download", + "openDownloadProgress": "Open the download progress screen", + "testingHelpers": "Testing Helpers", + "createTestTab": "Create test tab", + "addRandomItems": "Add tab with random items", + "addTestProducts": "Add 100 test products", + "stressTestGrid": "Stress test product grid", + "simulateLowStock": "Simulate low stock", + "setProductsBelowThreshold": "Set all products below threshold", + "generateMockOrders": "Generate 100 mock orders", + "randomCustomersItemsAmounts": "Random customers, items, and amounts", + "performance": "Performance", + "clearImageCache": "Clear image cache", + "reloadProductImages": "Reload product images", + "debugOverlay": "Debug Overlay", + "debugOverlayDescription": "The debug overlay shows FPS, memory usage, and widget counts. Enable it via Flutter DevTools in debug mode.", + "ok": "OK", + "simulateUpdate": "Simulate update", + "chooseSimulationMode": "Choose a simulation mode:", + "successfulDownload": "Successful download", + "progressThenInstall": "Progress 0→100%, then install, then done", + "downloadError": "Download error", + "failsWithNetworkTimeout": "Fails at 50% with a network timeout", + "realDownloadWillFail": "Real download (will fail)", + "attemptsFakeUrl": "Attempts real OTA with fake URL", + "categoryBeer": "Beer", + "categoryWine": "Wine", + "categorySoftDrinks": "Soft drinks", + "categoryCocktails": "Cocktails", + "categorySnacks": "Snacks", + "categoryCoffee": "Coffee", + "categoryTea": "Tea", + "categoryOther": "Other", + "devActionClearedOpenTabs": "Cleared all open tabs", + "devActionClearedHistory": "Cleared closed tab history", + "devActionClearedProducts": "Cleared all products", + "devActionSeededProducts": "Seeded {count} products", + "@devActionSeededProducts": {"placeholders": {"count": {"type": "int"}}}, + "devActionReseededProducts": "Cleared and reseeded {count} products", + "@devActionReseededProducts": {"placeholders": {"count": {"type": "int"}}}, + "devActionImagesCleared": "Product images cleared", + "devActionResetPin": "PIN reset (no PIN required)", + "devActionNoProducts": "No products available", + "devActionImagesReloaded": "Product images reloaded", + "devActionCacheCleared": "Image cache cleared (no-op in debug mode)", + "devActionLowStock": "Simulated low stock for {count} products", + "@devActionLowStock": {"placeholders": {"count": {"type": "int"}}}, + "devActionTestTab": "Created test tab with {count} items", + "@devActionTestTab": {"placeholders": {"count": {"type": "int"}}}, + "devActionTestProducts": "Added {count} test products", + "@devActionTestProducts": {"placeholders": {"count": {"type": "int"}}}, + "devActionMockOrders": "Generated {orders} mock orders across {customers} customers", + "@devActionMockOrders": {"placeholders": {"orders": {"type": "int"}, "customers": {"type": "int"}}}, + "devActionFailedImages": "Failed to clear images.", + "devActionResetStock": "Reset stock for {count} products", + "@devActionResetStock": {"placeholders": {"count": {"type": "int"}}}, + "paymentCash": "Cash", + "paymentPayconiq": "Payconiq", + "simulatedUpdateNotes": "Bug fixes and performance improvements." +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..6b4d8a5 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,1454 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_nl.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('nl'), + ]; + + /// No description provided for @appTitle. + /// + /// In en, this message translates to: + /// **'KoolTab'** + String get appTitle; + + /// No description provided for @barTabs. + /// + /// In en, this message translates to: + /// **'Bar Tabs'** + String get barTabs; + + /// No description provided for @products. + /// + /// In en, this message translates to: + /// **'Products'** + String get products; + + /// No description provided for @addProduct. + /// + /// In en, this message translates to: + /// **'Add product'** + String get addProduct; + + /// No description provided for @noProductsYet. + /// + /// In en, this message translates to: + /// **'No products yet.'** + String get noProductsYet; + + /// No description provided for @addFirstProduct. + /// + /// In en, this message translates to: + /// **'Add your first product to start selling.'** + String get addFirstProduct; + + /// No description provided for @manageProducts. + /// + /// In en, this message translates to: + /// **'Manage products'** + String get manageProducts; + + /// No description provided for @tabHistory. + /// + /// In en, this message translates to: + /// **'Tab history'** + String get tabHistory; + + /// No description provided for @refresh. + /// + /// In en, this message translates to: + /// **'Refresh'** + String get refresh; + + /// No description provided for @settings. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// No description provided for @logout. + /// + /// In en, this message translates to: + /// **'Logout'** + String get logout; + + /// No description provided for @openOrSelectTab. + /// + /// In en, this message translates to: + /// **'Open or select a tab first.'** + String get openOrSelectTab; + + /// No description provided for @openNewTab. + /// + /// In en, this message translates to: + /// **'Open new tab'** + String get openNewTab; + + /// No description provided for @customerGroupName. + /// + /// In en, this message translates to: + /// **'Customer / group name'** + String get customerGroupName; + + /// No description provided for @cancel. + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// No description provided for @openTab. + /// + /// In en, this message translates to: + /// **'Open tab'** + String get openTab; + + /// No description provided for @couldNotCreateTab. + /// + /// In en, this message translates to: + /// **'Could not create tab: {error}'** + String couldNotCreateTab(String error); + + /// No description provided for @searchByName. + /// + /// In en, this message translates to: + /// **'Search by name…'** + String get searchByName; + + /// No description provided for @allCustomers. + /// + /// In en, this message translates to: + /// **'All customers'** + String get allCustomers; + + /// No description provided for @customer. + /// + /// In en, this message translates to: + /// **'Customer'** + String get customer; + + /// No description provided for @noClosedTabs. + /// + /// In en, this message translates to: + /// **'No closed tabs yet'** + String get noClosedTabs; + + /// No description provided for @settingsTitle. + /// + /// In en, this message translates to: + /// **'Settings'** + String get settingsTitle; + + /// No description provided for @security. + /// + /// In en, this message translates to: + /// **'Security'** + String get security; + + /// No description provided for @pinRequired. + /// + /// In en, this message translates to: + /// **'PIN Required'** + String get pinRequired; + + /// No description provided for @changePin. + /// + /// In en, this message translates to: + /// **'Change PIN'** + String get changePin; + + /// No description provided for @appearance. + /// + /// In en, this message translates to: + /// **'Appearance'** + String get appearance; + + /// No description provided for @language. + /// + /// In en, this message translates to: + /// **'Language'** + String get language; + + /// No description provided for @languageSystem. + /// + /// In en, this message translates to: + /// **'System default'** + String get languageSystem; + + /// No description provided for @languageEnglish. + /// + /// In en, this message translates to: + /// **'English'** + String get languageEnglish; + + /// No description provided for @languageDutch. + /// + /// In en, this message translates to: + /// **'Dutch'** + String get languageDutch; + + /// No description provided for @theme. + /// + /// In en, this message translates to: + /// **'Theme'** + String get theme; + + /// No description provided for @system. + /// + /// In en, this message translates to: + /// **'System'** + String get system; + + /// No description provided for @light. + /// + /// In en, this message translates to: + /// **'Light'** + String get light; + + /// No description provided for @dark. + /// + /// In en, this message translates to: + /// **'Dark'** + String get dark; + + /// No description provided for @ugly. + /// + /// In en, this message translates to: + /// **'Ugly'** + String get ugly; + + /// No description provided for @setPinCode. + /// + /// In en, this message translates to: + /// **'Set PIN Code'** + String get setPinCode; + + /// No description provided for @enterPin. + /// + /// In en, this message translates to: + /// **'Enter PIN'** + String get enterPin; + + /// No description provided for @enterCurrentPin. + /// + /// In en, this message translates to: + /// **'Enter Current PIN'** + String get enterCurrentPin; + + /// No description provided for @enterNewPin. + /// + /// In en, this message translates to: + /// **'Enter New PIN'** + String get enterNewPin; + + /// No description provided for @fourDigits. + /// + /// In en, this message translates to: + /// **'4 digits'** + String get fourDigits; + + /// No description provided for @enterPinFourDigits. + /// + /// In en, this message translates to: + /// **'Enter PIN (4 digits)'** + String get enterPinFourDigits; + + /// No description provided for @confirm. + /// + /// In en, this message translates to: + /// **'Confirm'** + String get confirm; + + /// No description provided for @confirmPin. + /// + /// In en, this message translates to: + /// **'Confirm PIN'** + String get confirmPin; + + /// No description provided for @pinExactlyFour. + /// + /// In en, this message translates to: + /// **'PIN must be exactly 4 digits'** + String get pinExactlyFour; + + /// No description provided for @couldNotSavePin. + /// + /// In en, this message translates to: + /// **'Could not save PIN setting.'** + String get couldNotSavePin; + + /// No description provided for @editProduct. + /// + /// In en, this message translates to: + /// **'Edit product'** + String get editProduct; + + /// No description provided for @productName. + /// + /// In en, this message translates to: + /// **'Product name'** + String get productName; + + /// No description provided for @category. + /// + /// In en, this message translates to: + /// **'Category'** + String get category; + + /// No description provided for @price. + /// + /// In en, this message translates to: + /// **'Price'** + String get price; + + /// No description provided for @stock. + /// + /// In en, this message translates to: + /// **'Stock'** + String get stock; + + /// No description provided for @lowStockThreshold. + /// + /// In en, this message translates to: + /// **'Low stock threshold'** + String get lowStockThreshold; + + /// No description provided for @chooseProductImage. + /// + /// In en, this message translates to: + /// **'Choose product image'** + String get chooseProductImage; + + /// No description provided for @changeImage. + /// + /// In en, this message translates to: + /// **'Change image'** + String get changeImage; + + /// No description provided for @deleteProduct. + /// + /// In en, this message translates to: + /// **'Delete product?'** + String get deleteProduct; + + /// No description provided for @deleteProductDescription. + /// + /// In en, this message translates to: + /// **'This will remove the product from the product list.'** + String get deleteProductDescription; + + /// No description provided for @delete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get delete; + + /// No description provided for @productNotFound. + /// + /// In en, this message translates to: + /// **'Product not found.'** + String get productNotFound; + + /// No description provided for @chooseImage. + /// + /// In en, this message translates to: + /// **'Choose a product image.'** + String get chooseImage; + + /// No description provided for @selectCategory. + /// + /// In en, this message translates to: + /// **'Please select a category.'** + String get selectCategory; + + /// No description provided for @enterProductName. + /// + /// In en, this message translates to: + /// **'Enter a product name.'** + String get enterProductName; + + /// No description provided for @version. + /// + /// In en, this message translates to: + /// **'Version {version}'** + String version(String version); + + /// No description provided for @selectTabToAddItems. + /// + /// In en, this message translates to: + /// **'Select a tab to add items'** + String get selectTabToAddItems; + + /// No description provided for @openTabs. + /// + /// In en, this message translates to: + /// **'Open tabs'** + String get openTabs; + + /// No description provided for @selectOrOpenTab. + /// + /// In en, this message translates to: + /// **'Select or open a tab'** + String get selectOrOpenTab; + + /// No description provided for @noOpenTabs. + /// + /// In en, this message translates to: + /// **'No open tabs'** + String get noOpenTabs; + + /// No description provided for @edit. + /// + /// In en, this message translates to: + /// **'Edit'** + String get edit; + + /// No description provided for @close. + /// + /// In en, this message translates to: + /// **'Close'** + String get close; + + /// No description provided for @tabItemSummary. + /// + /// In en, this message translates to: + /// **'{count, plural, =1 {1 item - {total}} other {{count} items - {total}}}'** + String tabItemSummary(int count, String total); + + /// No description provided for @tabItemCount. + /// + /// In en, this message translates to: + /// **'{count, plural, =1 {1 item} other {{count} items}}'** + String tabItemCount(int count); + + /// No description provided for @tapProductsToAdd. + /// + /// In en, this message translates to: + /// **'Tap products to add them'** + String get tapProductsToAdd; + + /// No description provided for @total. + /// + /// In en, this message translates to: + /// **'Total'** + String get total; + + /// No description provided for @closeTab. + /// + /// In en, this message translates to: + /// **'Close tab'** + String get closeTab; + + /// No description provided for @productStockSummary. + /// + /// In en, this message translates to: + /// **'{category} • {price} • Stock: {stock}'** + String productStockSummary(String category, String price, int stock); + + /// No description provided for @outOfStock. + /// + /// In en, this message translates to: + /// **'OUT OF STOCK'** + String get outOfStock; + + /// No description provided for @tabsYouCloseAppearHere. + /// + /// In en, this message translates to: + /// **'Tabs you close will show up here.'** + String get tabsYouCloseAppearHere; + + /// No description provided for @enterPinSubtitle. + /// + /// In en, this message translates to: + /// **'You’ll use this to unlock the app'** + String get enterPinSubtitle; + + /// No description provided for @confirmPinSubtitle. + /// + /// In en, this message translates to: + /// **'Enter the same PIN again'** + String get confirmPinSubtitle; + + /// No description provided for @createPin. + /// + /// In en, this message translates to: + /// **'Create a PIN'** + String get createPin; + + /// No description provided for @pinsDidNotMatch. + /// + /// In en, this message translates to: + /// **'PINs didn’t match. Try again.'** + String get pinsDidNotMatch; + + /// No description provided for @somethingWentWrong. + /// + /// In en, this message translates to: + /// **'Something went wrong.'** + String get somethingWentWrong; + + /// No description provided for @incorrectPin. + /// + /// In en, this message translates to: + /// **'Incorrect PIN.'** + String get incorrectPin; + + /// No description provided for @enterPrice. + /// + /// In en, this message translates to: + /// **'Enter a price.'** + String get enterPrice; + + /// No description provided for @enterValidPrice. + /// + /// In en, this message translates to: + /// **'Enter a valid price.'** + String get enterValidPrice; + + /// No description provided for @enterValidStock. + /// + /// In en, this message translates to: + /// **'Enter a valid stock amount.'** + String get enterValidStock; + + /// No description provided for @enterValidThreshold. + /// + /// In en, this message translates to: + /// **'Enter a valid threshold.'** + String get enterValidThreshold; + + /// No description provided for @couldNotLoadProduct. + /// + /// In en, this message translates to: + /// **'Could not load product.'** + String get couldNotLoadProduct; + + /// No description provided for @couldNotSaveProduct. + /// + /// In en, this message translates to: + /// **'Could not save product.'** + String get couldNotSaveProduct; + + /// No description provided for @couldNotDeleteProduct. + /// + /// In en, this message translates to: + /// **'Could not delete product.'** + String get couldNotDeleteProduct; + + /// No description provided for @paymentMethod. + /// + /// In en, this message translates to: + /// **'Payment method'** + String get paymentMethod; + + /// No description provided for @cash. + /// + /// In en, this message translates to: + /// **'Cash'** + String get cash; + + /// No description provided for @payconiq. + /// + /// In en, this message translates to: + /// **'Payconiq'** + String get payconiq; + + /// No description provided for @closeTabForCustomer. + /// + /// In en, this message translates to: + /// **'Close tab for {customerName}?'** + String closeTabForCustomer(String customerName); + + /// No description provided for @currentTotal. + /// + /// In en, this message translates to: + /// **'Current total: {total}'** + String currentTotal(String total); + + /// No description provided for @couldNotCloseTab. + /// + /// In en, this message translates to: + /// **'Could not close tab.'** + String get couldNotCloseTab; + + /// No description provided for @closingTab. + /// + /// In en, this message translates to: + /// **'Closing tab…'** + String get closingTab; + + /// No description provided for @slideToConfirmClosing. + /// + /// In en, this message translates to: + /// **'Slide to confirm closing'** + String get slideToConfirmClosing; + + /// No description provided for @errorScreen. + /// + /// In en, this message translates to: + /// **'Error Screen'** + String get errorScreen; + + /// No description provided for @unexpectedError. + /// + /// In en, this message translates to: + /// **'An unexpected error occurred.\nPlease try restarting the app.'** + String get unexpectedError; + + /// No description provided for @goToBarScreen. + /// + /// In en, this message translates to: + /// **'Go to bar screen'** + String get goToBarScreen; + + /// No description provided for @updates. + /// + /// In en, this message translates to: + /// **'Updates'** + String get updates; + + /// No description provided for @checkingForUpdates. + /// + /// In en, this message translates to: + /// **'Checking for updates…'** + String get checkingForUpdates; + + /// No description provided for @checkForUpdates. + /// + /// In en, this message translates to: + /// **'Check for updates'** + String get checkForUpdates; + + /// No description provided for @latestVersion. + /// + /// In en, this message translates to: + /// **'You are already on the latest version.'** + String get latestVersion; + + /// No description provided for @updateCheckFailed. + /// + /// In en, this message translates to: + /// **'Update check failed.'** + String get updateCheckFailed; + + /// No description provided for @updateAvailable. + /// + /// In en, this message translates to: + /// **'Update available'** + String get updateAvailable; + + /// No description provided for @versionAvailable. + /// + /// In en, this message translates to: + /// **'Version {version} is available.'** + String versionAvailable(String version); + + /// No description provided for @later. + /// + /// In en, this message translates to: + /// **'Later'** + String get later; + + /// No description provided for @update. + /// + /// In en, this message translates to: + /// **'Update'** + String get update; + + /// No description provided for @updateReady. + /// + /// In en, this message translates to: + /// **'Update ready'** + String get updateReady; + + /// No description provided for @updateReadyDescription. + /// + /// In en, this message translates to: + /// **'The update has been downloaded and installed. Restart the app now to apply the changes.'** + String get updateReadyDescription; + + /// No description provided for @restartApp. + /// + /// In en, this message translates to: + /// **'Restart app'** + String get restartApp; + + /// No description provided for @updateFailed. + /// + /// In en, this message translates to: + /// **'Update failed'** + String get updateFailed; + + /// No description provided for @retry. + /// + /// In en, this message translates to: + /// **'Retry'** + String get retry; + + /// No description provided for @updating. + /// + /// In en, this message translates to: + /// **'Updating'** + String get updating; + + /// No description provided for @installing. + /// + /// In en, this message translates to: + /// **'Installing'** + String get installing; + + /// No description provided for @doNotCloseDuringUpdate. + /// + /// In en, this message translates to: + /// **'Do not close the app during the update'** + String get doNotCloseDuringUpdate; + + /// No description provided for @cancelUpdate. + /// + /// In en, this message translates to: + /// **'Cancel update?'** + String get cancelUpdate; + + /// No description provided for @cancelUpdateDescription. + /// + /// In en, this message translates to: + /// **'The update is in progress. If you leave now, the app may become unstable.'** + String get cancelUpdateDescription; + + /// No description provided for @continueUpdate. + /// + /// In en, this message translates to: + /// **'Continue update'** + String get continueUpdate; + + /// No description provided for @cancelUpdateAction. + /// + /// In en, this message translates to: + /// **'Cancel update'** + String get cancelUpdateAction; + + /// No description provided for @preparingDownload. + /// + /// In en, this message translates to: + /// **'Preparing download…'** + String get preparingDownload; + + /// No description provided for @simulatedDownloadError. + /// + /// In en, this message translates to: + /// **'Simulated download error (network timeout)'** + String get simulatedDownloadError; + + /// No description provided for @downloadFailed. + /// + /// In en, this message translates to: + /// **'Download failed'** + String get downloadFailed; + + /// No description provided for @downloading. + /// + /// In en, this message translates to: + /// **'Downloading {percent}%'** + String downloading(String percent); + + /// No description provided for @installingUpdate. + /// + /// In en, this message translates to: + /// **'Installing update…'** + String get installingUpdate; + + /// No description provided for @updateInstalledRestarting. + /// + /// In en, this message translates to: + /// **'Update installed! Restarting…'** + String get updateInstalledRestarting; + + /// No description provided for @installationFailed. + /// + /// In en, this message translates to: + /// **'Installation failed'** + String get installationFailed; + + /// No description provided for @updateAlreadyInProgress. + /// + /// In en, this message translates to: + /// **'Update already in progress'** + String get updateAlreadyInProgress; + + /// No description provided for @updateAlreadyRunning. + /// + /// In en, this message translates to: + /// **'Update already running'** + String get updateAlreadyRunning; + + /// No description provided for @genericError. + /// + /// In en, this message translates to: + /// **'An unexpected error occurred.'** + String get genericError; + + /// No description provided for @couldNotLoadBar. + /// + /// In en, this message translates to: + /// **'Could not load bar screen.'** + String get couldNotLoadBar; + + /// No description provided for @couldNotCreateTabMessage. + /// + /// In en, this message translates to: + /// **'Could not create tab.'** + String get couldNotCreateTabMessage; + + /// No description provided for @productOutOfStock. + /// + /// In en, this message translates to: + /// **'Product is out of stock.'** + String get productOutOfStock; + + /// No description provided for @couldNotAddProductToTab. + /// + /// In en, this message translates to: + /// **'Could not add product to tab.'** + String get couldNotAddProductToTab; + + /// No description provided for @couldNotUpdateQuantity. + /// + /// In en, this message translates to: + /// **'Could not update item quantity.'** + String get couldNotUpdateQuantity; + + /// No description provided for @couldNotReloadTabs. + /// + /// In en, this message translates to: + /// **'Could not reload tabs.'** + String get couldNotReloadTabs; + + /// No description provided for @couldNotLoadHistory. + /// + /// In en, this message translates to: + /// **'Could not load tab history.'** + String get couldNotLoadHistory; + + /// No description provided for @couldNotLoadMoreTabs. + /// + /// In en, this message translates to: + /// **'Could not load more tabs.'** + String get couldNotLoadMoreTabs; + + /// No description provided for @couldNotLoadProducts. + /// + /// In en, this message translates to: + /// **'Could not load products.'** + String get couldNotLoadProducts; + + /// No description provided for @couldNotAddProduct. + /// + /// In en, this message translates to: + /// **'Could not add product.'** + String get couldNotAddProduct; + + /// No description provided for @couldNotUpdateProduct. + /// + /// In en, this message translates to: + /// **'Could not update product.'** + String get couldNotUpdateProduct; + + /// No description provided for @couldNotDeleteProductMessage. + /// + /// In en, this message translates to: + /// **'Could not delete product.'** + String get couldNotDeleteProductMessage; + + /// No description provided for @couldNotCheckPinStatus. + /// + /// In en, this message translates to: + /// **'Could not check PIN status.'** + String get couldNotCheckPinStatus; + + /// No description provided for @couldNotVerifyPin. + /// + /// In en, this message translates to: + /// **'Could not verify PIN.'** + String get couldNotVerifyPin; + + /// No description provided for @currentPinIncorrect. + /// + /// In en, this message translates to: + /// **'Current PIN is incorrect.'** + String get currentPinIncorrect; + + /// No description provided for @couldNotLoadSettings. + /// + /// In en, this message translates to: + /// **'Could not load settings.'** + String get couldNotLoadSettings; + + /// No description provided for @couldNotSaveSettings. + /// + /// In en, this message translates to: + /// **'Could not save settings.'** + String get couldNotSaveSettings; + + /// No description provided for @downloadFailedWithDetail. + /// + /// In en, this message translates to: + /// **'Download failed.'** + String get downloadFailedWithDetail; + + /// No description provided for @installationFailedWithDetail. + /// + /// In en, this message translates to: + /// **'Installation failed.'** + String get installationFailedWithDetail; + + /// No description provided for @devMenu. + /// + /// In en, this message translates to: + /// **'Dev Menu'** + String get devMenu; + + /// No description provided for @dataManagement. + /// + /// In en, this message translates to: + /// **'Data Management'** + String get dataManagement; + + /// No description provided for @clearOpenTabs. + /// + /// In en, this message translates to: + /// **'Clear all open tabs'** + String get clearOpenTabs; + + /// No description provided for @clearClosedHistory. + /// + /// In en, this message translates to: + /// **'Clear closed tab history'** + String get clearClosedHistory; + + /// No description provided for @seedDefaultProducts. + /// + /// In en, this message translates to: + /// **'Seed default products'** + String get seedDefaultProducts; + + /// No description provided for @onlySeedsWhenEmpty. + /// + /// In en, this message translates to: + /// **'Only seeds if table is empty'** + String get onlySeedsWhenEmpty; + + /// No description provided for @clearAndReseedProducts. + /// + /// In en, this message translates to: + /// **'Clear & reseed products'** + String get clearAndReseedProducts; + + /// No description provided for @wipesAndReseeds. + /// + /// In en, this message translates to: + /// **'Wipes all products, then seeds defaults'** + String get wipesAndReseeds; + + /// No description provided for @clearAllProducts. + /// + /// In en, this message translates to: + /// **'Clear all products'** + String get clearAllProducts; + + /// No description provided for @clearProductImages. + /// + /// In en, this message translates to: + /// **'Clear product images'** + String get clearProductImages; + + /// No description provided for @deletesProductImages. + /// + /// In en, this message translates to: + /// **'Deletes images from product_images folder'** + String get deletesProductImages; + + /// No description provided for @debugging. + /// + /// In en, this message translates to: + /// **'Debugging'** + String get debugging; + + /// No description provided for @resetPin. + /// + /// In en, this message translates to: + /// **'Reset PIN'** + String get resetPin; + + /// No description provided for @removePinLock. + /// + /// In en, this message translates to: + /// **'Remove PIN lock'** + String get removePinLock; + + /// No description provided for @toggleDebugOverlay. + /// + /// In en, this message translates to: + /// **'Toggle debug overlay'** + String get toggleDebugOverlay; + + /// No description provided for @showFpsMemoryWidgets. + /// + /// In en, this message translates to: + /// **'Show FPS, memory, widget count'** + String get showFpsMemoryWidgets; + + /// No description provided for @errorScreenMenu. + /// + /// In en, this message translates to: + /// **'Error screen'** + String get errorScreenMenu; + + /// No description provided for @viewErrorScreen. + /// + /// In en, this message translates to: + /// **'View the error screen UI'** + String get viewErrorScreen; + + /// No description provided for @featureToggles. + /// + /// In en, this message translates to: + /// **'Feature Toggles'** + String get featureToggles; + + /// No description provided for @simulateUpdateDownload. + /// + /// In en, this message translates to: + /// **'Simulate update download'** + String get simulateUpdateDownload; + + /// No description provided for @openDownloadProgress. + /// + /// In en, this message translates to: + /// **'Open the download progress screen'** + String get openDownloadProgress; + + /// No description provided for @testingHelpers. + /// + /// In en, this message translates to: + /// **'Testing Helpers'** + String get testingHelpers; + + /// No description provided for @createTestTab. + /// + /// In en, this message translates to: + /// **'Create test tab'** + String get createTestTab; + + /// No description provided for @addRandomItems. + /// + /// In en, this message translates to: + /// **'Add tab with random items'** + String get addRandomItems; + + /// No description provided for @addTestProducts. + /// + /// In en, this message translates to: + /// **'Add 100 test products'** + String get addTestProducts; + + /// No description provided for @stressTestGrid. + /// + /// In en, this message translates to: + /// **'Stress test product grid'** + String get stressTestGrid; + + /// No description provided for @simulateLowStock. + /// + /// In en, this message translates to: + /// **'Simulate low stock'** + String get simulateLowStock; + + /// No description provided for @setProductsBelowThreshold. + /// + /// In en, this message translates to: + /// **'Set all products below threshold'** + String get setProductsBelowThreshold; + + /// No description provided for @generateMockOrders. + /// + /// In en, this message translates to: + /// **'Generate 100 mock orders'** + String get generateMockOrders; + + /// No description provided for @randomCustomersItemsAmounts. + /// + /// In en, this message translates to: + /// **'Random customers, items, and amounts'** + String get randomCustomersItemsAmounts; + + /// No description provided for @performance. + /// + /// In en, this message translates to: + /// **'Performance'** + String get performance; + + /// No description provided for @clearImageCache. + /// + /// In en, this message translates to: + /// **'Clear image cache'** + String get clearImageCache; + + /// No description provided for @reloadProductImages. + /// + /// In en, this message translates to: + /// **'Reload product images'** + String get reloadProductImages; + + /// No description provided for @debugOverlay. + /// + /// In en, this message translates to: + /// **'Debug Overlay'** + String get debugOverlay; + + /// No description provided for @debugOverlayDescription. + /// + /// In en, this message translates to: + /// **'The debug overlay shows FPS, memory usage, and widget counts. Enable it via Flutter DevTools in debug mode.'** + String get debugOverlayDescription; + + /// No description provided for @ok. + /// + /// In en, this message translates to: + /// **'OK'** + String get ok; + + /// No description provided for @simulateUpdate. + /// + /// In en, this message translates to: + /// **'Simulate update'** + String get simulateUpdate; + + /// No description provided for @chooseSimulationMode. + /// + /// In en, this message translates to: + /// **'Choose a simulation mode:'** + String get chooseSimulationMode; + + /// No description provided for @successfulDownload. + /// + /// In en, this message translates to: + /// **'Successful download'** + String get successfulDownload; + + /// No description provided for @progressThenInstall. + /// + /// In en, this message translates to: + /// **'Progress 0→100%, then install, then done'** + String get progressThenInstall; + + /// No description provided for @downloadError. + /// + /// In en, this message translates to: + /// **'Download error'** + String get downloadError; + + /// No description provided for @failsWithNetworkTimeout. + /// + /// In en, this message translates to: + /// **'Fails at 50% with a network timeout'** + String get failsWithNetworkTimeout; + + /// No description provided for @realDownloadWillFail. + /// + /// In en, this message translates to: + /// **'Real download (will fail)'** + String get realDownloadWillFail; + + /// No description provided for @attemptsFakeUrl. + /// + /// In en, this message translates to: + /// **'Attempts real OTA with fake URL'** + String get attemptsFakeUrl; + + /// No description provided for @categoryBeer. + /// + /// In en, this message translates to: + /// **'Beer'** + String get categoryBeer; + + /// No description provided for @categoryWine. + /// + /// In en, this message translates to: + /// **'Wine'** + String get categoryWine; + + /// No description provided for @categorySoftDrinks. + /// + /// In en, this message translates to: + /// **'Soft drinks'** + String get categorySoftDrinks; + + /// No description provided for @categoryCocktails. + /// + /// In en, this message translates to: + /// **'Cocktails'** + String get categoryCocktails; + + /// No description provided for @categorySnacks. + /// + /// In en, this message translates to: + /// **'Snacks'** + String get categorySnacks; + + /// No description provided for @categoryCoffee. + /// + /// In en, this message translates to: + /// **'Coffee'** + String get categoryCoffee; + + /// No description provided for @categoryTea. + /// + /// In en, this message translates to: + /// **'Tea'** + String get categoryTea; + + /// No description provided for @categoryOther. + /// + /// In en, this message translates to: + /// **'Other'** + String get categoryOther; + + /// No description provided for @devActionClearedOpenTabs. + /// + /// In en, this message translates to: + /// **'Cleared all open tabs'** + String get devActionClearedOpenTabs; + + /// No description provided for @devActionClearedHistory. + /// + /// In en, this message translates to: + /// **'Cleared closed tab history'** + String get devActionClearedHistory; + + /// No description provided for @devActionClearedProducts. + /// + /// In en, this message translates to: + /// **'Cleared all products'** + String get devActionClearedProducts; + + /// No description provided for @devActionSeededProducts. + /// + /// In en, this message translates to: + /// **'Seeded {count} products'** + String devActionSeededProducts(int count); + + /// No description provided for @devActionReseededProducts. + /// + /// In en, this message translates to: + /// **'Cleared and reseeded {count} products'** + String devActionReseededProducts(int count); + + /// No description provided for @devActionImagesCleared. + /// + /// In en, this message translates to: + /// **'Product images cleared'** + String get devActionImagesCleared; + + /// No description provided for @devActionResetPin. + /// + /// In en, this message translates to: + /// **'PIN reset (no PIN required)'** + String get devActionResetPin; + + /// No description provided for @devActionNoProducts. + /// + /// In en, this message translates to: + /// **'No products available'** + String get devActionNoProducts; + + /// No description provided for @devActionImagesReloaded. + /// + /// In en, this message translates to: + /// **'Product images reloaded'** + String get devActionImagesReloaded; + + /// No description provided for @devActionCacheCleared. + /// + /// In en, this message translates to: + /// **'Image cache cleared (no-op in debug mode)'** + String get devActionCacheCleared; + + /// No description provided for @devActionLowStock. + /// + /// In en, this message translates to: + /// **'Simulated low stock for {count} products'** + String devActionLowStock(int count); + + /// No description provided for @devActionTestTab. + /// + /// In en, this message translates to: + /// **'Created test tab with {count} items'** + String devActionTestTab(int count); + + /// No description provided for @devActionTestProducts. + /// + /// In en, this message translates to: + /// **'Added {count} test products'** + String devActionTestProducts(int count); + + /// No description provided for @devActionMockOrders. + /// + /// In en, this message translates to: + /// **'Generated {orders} mock orders across {customers} customers'** + String devActionMockOrders(int orders, int customers); + + /// No description provided for @devActionFailedImages. + /// + /// In en, this message translates to: + /// **'Failed to clear images.'** + String get devActionFailedImages; + + /// No description provided for @devActionResetStock. + /// + /// In en, this message translates to: + /// **'Reset stock for {count} products'** + String devActionResetStock(int count); + + /// No description provided for @paymentCash. + /// + /// In en, this message translates to: + /// **'Cash'** + String get paymentCash; + + /// No description provided for @paymentPayconiq. + /// + /// In en, this message translates to: + /// **'Payconiq'** + String get paymentPayconiq; + + /// No description provided for @simulatedUpdateNotes. + /// + /// In en, this message translates to: + /// **'Bug fixes and performance improvements.'** + String get simulatedUpdateNotes; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'nl'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'nl': + return AppLocalizationsNl(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..34ab41f --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,723 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'KoolTab'; + + @override + String get barTabs => 'Bar Tabs'; + + @override + String get products => 'Products'; + + @override + String get addProduct => 'Add product'; + + @override + String get noProductsYet => 'No products yet.'; + + @override + String get addFirstProduct => 'Add your first product to start selling.'; + + @override + String get manageProducts => 'Manage products'; + + @override + String get tabHistory => 'Tab history'; + + @override + String get refresh => 'Refresh'; + + @override + String get settings => 'Settings'; + + @override + String get logout => 'Logout'; + + @override + String get openOrSelectTab => 'Open or select a tab first.'; + + @override + String get openNewTab => 'Open new tab'; + + @override + String get customerGroupName => 'Customer / group name'; + + @override + String get cancel => 'Cancel'; + + @override + String get openTab => 'Open tab'; + + @override + String couldNotCreateTab(String error) { + return 'Could not create tab: $error'; + } + + @override + String get searchByName => 'Search by name…'; + + @override + String get allCustomers => 'All customers'; + + @override + String get customer => 'Customer'; + + @override + String get noClosedTabs => 'No closed tabs yet'; + + @override + String get settingsTitle => 'Settings'; + + @override + String get security => 'Security'; + + @override + String get pinRequired => 'PIN Required'; + + @override + String get changePin => 'Change PIN'; + + @override + String get appearance => 'Appearance'; + + @override + String get language => 'Language'; + + @override + String get languageSystem => 'System default'; + + @override + String get languageEnglish => 'English'; + + @override + String get languageDutch => 'Dutch'; + + @override + String get theme => 'Theme'; + + @override + String get system => 'System'; + + @override + String get light => 'Light'; + + @override + String get dark => 'Dark'; + + @override + String get ugly => 'Ugly'; + + @override + String get setPinCode => 'Set PIN Code'; + + @override + String get enterPin => 'Enter PIN'; + + @override + String get enterCurrentPin => 'Enter Current PIN'; + + @override + String get enterNewPin => 'Enter New PIN'; + + @override + String get fourDigits => '4 digits'; + + @override + String get enterPinFourDigits => 'Enter PIN (4 digits)'; + + @override + String get confirm => 'Confirm'; + + @override + String get confirmPin => 'Confirm PIN'; + + @override + String get pinExactlyFour => 'PIN must be exactly 4 digits'; + + @override + String get couldNotSavePin => 'Could not save PIN setting.'; + + @override + String get editProduct => 'Edit product'; + + @override + String get productName => 'Product name'; + + @override + String get category => 'Category'; + + @override + String get price => 'Price'; + + @override + String get stock => 'Stock'; + + @override + String get lowStockThreshold => 'Low stock threshold'; + + @override + String get chooseProductImage => 'Choose product image'; + + @override + String get changeImage => 'Change image'; + + @override + String get deleteProduct => 'Delete product?'; + + @override + String get deleteProductDescription => + 'This will remove the product from the product list.'; + + @override + String get delete => 'Delete'; + + @override + String get productNotFound => 'Product not found.'; + + @override + String get chooseImage => 'Choose a product image.'; + + @override + String get selectCategory => 'Please select a category.'; + + @override + String get enterProductName => 'Enter a product name.'; + + @override + String version(String version) { + return 'Version $version'; + } + + @override + String get selectTabToAddItems => 'Select a tab to add items'; + + @override + String get openTabs => 'Open tabs'; + + @override + String get selectOrOpenTab => 'Select or open a tab'; + + @override + String get noOpenTabs => 'No open tabs'; + + @override + String get edit => 'Edit'; + + @override + String get close => 'Close'; + + @override + String tabItemSummary(int count, String total) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items - $total', + one: '1 item - $total', + ); + return '$_temp0'; + } + + @override + String tabItemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get tapProductsToAdd => 'Tap products to add them'; + + @override + String get total => 'Total'; + + @override + String get closeTab => 'Close tab'; + + @override + String productStockSummary(String category, String price, int stock) { + return '$category • $price • Stock: $stock'; + } + + @override + String get outOfStock => 'OUT OF STOCK'; + + @override + String get tabsYouCloseAppearHere => 'Tabs you close will show up here.'; + + @override + String get enterPinSubtitle => 'You’ll use this to unlock the app'; + + @override + String get confirmPinSubtitle => 'Enter the same PIN again'; + + @override + String get createPin => 'Create a PIN'; + + @override + String get pinsDidNotMatch => 'PINs didn’t match. Try again.'; + + @override + String get somethingWentWrong => 'Something went wrong.'; + + @override + String get incorrectPin => 'Incorrect PIN.'; + + @override + String get enterPrice => 'Enter a price.'; + + @override + String get enterValidPrice => 'Enter a valid price.'; + + @override + String get enterValidStock => 'Enter a valid stock amount.'; + + @override + String get enterValidThreshold => 'Enter a valid threshold.'; + + @override + String get couldNotLoadProduct => 'Could not load product.'; + + @override + String get couldNotSaveProduct => 'Could not save product.'; + + @override + String get couldNotDeleteProduct => 'Could not delete product.'; + + @override + String get paymentMethod => 'Payment method'; + + @override + String get cash => 'Cash'; + + @override + String get payconiq => 'Payconiq'; + + @override + String closeTabForCustomer(String customerName) { + return 'Close tab for $customerName?'; + } + + @override + String currentTotal(String total) { + return 'Current total: $total'; + } + + @override + String get couldNotCloseTab => 'Could not close tab.'; + + @override + String get closingTab => 'Closing tab…'; + + @override + String get slideToConfirmClosing => 'Slide to confirm closing'; + + @override + String get errorScreen => 'Error Screen'; + + @override + String get unexpectedError => + 'An unexpected error occurred.\nPlease try restarting the app.'; + + @override + String get goToBarScreen => 'Go to bar screen'; + + @override + String get updates => 'Updates'; + + @override + String get checkingForUpdates => 'Checking for updates…'; + + @override + String get checkForUpdates => 'Check for updates'; + + @override + String get latestVersion => 'You are already on the latest version.'; + + @override + String get updateCheckFailed => 'Update check failed.'; + + @override + String get updateAvailable => 'Update available'; + + @override + String versionAvailable(String version) { + return 'Version $version is available.'; + } + + @override + String get later => 'Later'; + + @override + String get update => 'Update'; + + @override + String get updateReady => 'Update ready'; + + @override + String get updateReadyDescription => + 'The update has been downloaded and installed. Restart the app now to apply the changes.'; + + @override + String get restartApp => 'Restart app'; + + @override + String get updateFailed => 'Update failed'; + + @override + String get retry => 'Retry'; + + @override + String get updating => 'Updating'; + + @override + String get installing => 'Installing'; + + @override + String get doNotCloseDuringUpdate => 'Do not close the app during the update'; + + @override + String get cancelUpdate => 'Cancel update?'; + + @override + String get cancelUpdateDescription => + 'The update is in progress. If you leave now, the app may become unstable.'; + + @override + String get continueUpdate => 'Continue update'; + + @override + String get cancelUpdateAction => 'Cancel update'; + + @override + String get preparingDownload => 'Preparing download…'; + + @override + String get simulatedDownloadError => + 'Simulated download error (network timeout)'; + + @override + String get downloadFailed => 'Download failed'; + + @override + String downloading(String percent) { + return 'Downloading $percent%'; + } + + @override + String get installingUpdate => 'Installing update…'; + + @override + String get updateInstalledRestarting => 'Update installed! Restarting…'; + + @override + String get installationFailed => 'Installation failed'; + + @override + String get updateAlreadyInProgress => 'Update already in progress'; + + @override + String get updateAlreadyRunning => 'Update already running'; + + @override + String get genericError => 'An unexpected error occurred.'; + + @override + String get couldNotLoadBar => 'Could not load bar screen.'; + + @override + String get couldNotCreateTabMessage => 'Could not create tab.'; + + @override + String get productOutOfStock => 'Product is out of stock.'; + + @override + String get couldNotAddProductToTab => 'Could not add product to tab.'; + + @override + String get couldNotUpdateQuantity => 'Could not update item quantity.'; + + @override + String get couldNotReloadTabs => 'Could not reload tabs.'; + + @override + String get couldNotLoadHistory => 'Could not load tab history.'; + + @override + String get couldNotLoadMoreTabs => 'Could not load more tabs.'; + + @override + String get couldNotLoadProducts => 'Could not load products.'; + + @override + String get couldNotAddProduct => 'Could not add product.'; + + @override + String get couldNotUpdateProduct => 'Could not update product.'; + + @override + String get couldNotDeleteProductMessage => 'Could not delete product.'; + + @override + String get couldNotCheckPinStatus => 'Could not check PIN status.'; + + @override + String get couldNotVerifyPin => 'Could not verify PIN.'; + + @override + String get currentPinIncorrect => 'Current PIN is incorrect.'; + + @override + String get couldNotLoadSettings => 'Could not load settings.'; + + @override + String get couldNotSaveSettings => 'Could not save settings.'; + + @override + String get downloadFailedWithDetail => 'Download failed.'; + + @override + String get installationFailedWithDetail => 'Installation failed.'; + + @override + String get devMenu => 'Dev Menu'; + + @override + String get dataManagement => 'Data Management'; + + @override + String get clearOpenTabs => 'Clear all open tabs'; + + @override + String get clearClosedHistory => 'Clear closed tab history'; + + @override + String get seedDefaultProducts => 'Seed default products'; + + @override + String get onlySeedsWhenEmpty => 'Only seeds if table is empty'; + + @override + String get clearAndReseedProducts => 'Clear & reseed products'; + + @override + String get wipesAndReseeds => 'Wipes all products, then seeds defaults'; + + @override + String get clearAllProducts => 'Clear all products'; + + @override + String get clearProductImages => 'Clear product images'; + + @override + String get deletesProductImages => + 'Deletes images from product_images folder'; + + @override + String get debugging => 'Debugging'; + + @override + String get resetPin => 'Reset PIN'; + + @override + String get removePinLock => 'Remove PIN lock'; + + @override + String get toggleDebugOverlay => 'Toggle debug overlay'; + + @override + String get showFpsMemoryWidgets => 'Show FPS, memory, widget count'; + + @override + String get errorScreenMenu => 'Error screen'; + + @override + String get viewErrorScreen => 'View the error screen UI'; + + @override + String get featureToggles => 'Feature Toggles'; + + @override + String get simulateUpdateDownload => 'Simulate update download'; + + @override + String get openDownloadProgress => 'Open the download progress screen'; + + @override + String get testingHelpers => 'Testing Helpers'; + + @override + String get createTestTab => 'Create test tab'; + + @override + String get addRandomItems => 'Add tab with random items'; + + @override + String get addTestProducts => 'Add 100 test products'; + + @override + String get stressTestGrid => 'Stress test product grid'; + + @override + String get simulateLowStock => 'Simulate low stock'; + + @override + String get setProductsBelowThreshold => 'Set all products below threshold'; + + @override + String get generateMockOrders => 'Generate 100 mock orders'; + + @override + String get randomCustomersItemsAmounts => + 'Random customers, items, and amounts'; + + @override + String get performance => 'Performance'; + + @override + String get clearImageCache => 'Clear image cache'; + + @override + String get reloadProductImages => 'Reload product images'; + + @override + String get debugOverlay => 'Debug Overlay'; + + @override + String get debugOverlayDescription => + 'The debug overlay shows FPS, memory usage, and widget counts. Enable it via Flutter DevTools in debug mode.'; + + @override + String get ok => 'OK'; + + @override + String get simulateUpdate => 'Simulate update'; + + @override + String get chooseSimulationMode => 'Choose a simulation mode:'; + + @override + String get successfulDownload => 'Successful download'; + + @override + String get progressThenInstall => 'Progress 0→100%, then install, then done'; + + @override + String get downloadError => 'Download error'; + + @override + String get failsWithNetworkTimeout => 'Fails at 50% with a network timeout'; + + @override + String get realDownloadWillFail => 'Real download (will fail)'; + + @override + String get attemptsFakeUrl => 'Attempts real OTA with fake URL'; + + @override + String get categoryBeer => 'Beer'; + + @override + String get categoryWine => 'Wine'; + + @override + String get categorySoftDrinks => 'Soft drinks'; + + @override + String get categoryCocktails => 'Cocktails'; + + @override + String get categorySnacks => 'Snacks'; + + @override + String get categoryCoffee => 'Coffee'; + + @override + String get categoryTea => 'Tea'; + + @override + String get categoryOther => 'Other'; + + @override + String get devActionClearedOpenTabs => 'Cleared all open tabs'; + + @override + String get devActionClearedHistory => 'Cleared closed tab history'; + + @override + String get devActionClearedProducts => 'Cleared all products'; + + @override + String devActionSeededProducts(int count) { + return 'Seeded $count products'; + } + + @override + String devActionReseededProducts(int count) { + return 'Cleared and reseeded $count products'; + } + + @override + String get devActionImagesCleared => 'Product images cleared'; + + @override + String get devActionResetPin => 'PIN reset (no PIN required)'; + + @override + String get devActionNoProducts => 'No products available'; + + @override + String get devActionImagesReloaded => 'Product images reloaded'; + + @override + String get devActionCacheCleared => + 'Image cache cleared (no-op in debug mode)'; + + @override + String devActionLowStock(int count) { + return 'Simulated low stock for $count products'; + } + + @override + String devActionTestTab(int count) { + return 'Created test tab with $count items'; + } + + @override + String devActionTestProducts(int count) { + return 'Added $count test products'; + } + + @override + String devActionMockOrders(int orders, int customers) { + return 'Generated $orders mock orders across $customers customers'; + } + + @override + String get devActionFailedImages => 'Failed to clear images.'; + + @override + String devActionResetStock(int count) { + return 'Reset stock for $count products'; + } + + @override + String get paymentCash => 'Cash'; + + @override + String get paymentPayconiq => 'Payconiq'; + + @override + String get simulatedUpdateNotes => 'Bug fixes and performance improvements.'; +} diff --git a/lib/l10n/app_localizations_helpers.dart b/lib/l10n/app_localizations_helpers.dart new file mode 100644 index 0000000..96cf5d3 --- /dev/null +++ b/lib/l10n/app_localizations_helpers.dart @@ -0,0 +1,157 @@ +import 'app_localizations.dart'; + +extension AppLocalizationsHelpers on AppLocalizations { + String categoryLabel(String category) { + return switch (category.trim().toLowerCase()) { + 'bier' || 'beer' => categoryBeer, + 'wijn' || 'wine' => categoryWine, + 'frisdrank' || 'soft drinks' => categorySoftDrinks, + 'cocktails' => categoryCocktails, + 'snacks' => categorySnacks, + 'coffee' || 'koffie' => categoryCoffee, + 'thee' || 'tea' => categoryTea, + 'other' || 'overig' => categoryOther, + _ => category, + }; + } + + String localizedError(String? message) { + return switch (message) { + 'Could not load bar screen.' => couldNotLoadBar, + 'Could not create tab.' => couldNotCreateTabMessage, + 'Product is out of stock.' => productOutOfStock, + 'Could not add product to tab.' => couldNotAddProductToTab, + 'Could not update item quantity.' => couldNotUpdateQuantity, + 'Could not close tab.' => couldNotCloseTab, + 'Could not reload tabs.' => couldNotReloadTabs, + 'Could not load tab history.' => couldNotLoadHistory, + 'Could not load more tabs.' => couldNotLoadMoreTabs, + 'Could not load products.' => couldNotLoadProducts, + 'Could not add product.' => couldNotAddProduct, + 'Could not update product.' => couldNotUpdateProduct, + 'Could not check PIN status.' => couldNotCheckPinStatus, + 'Could not save PIN.' => couldNotSavePin, + 'Incorrect PIN.' => incorrectPin, + 'Could not verify PIN.' => couldNotVerifyPin, + 'Current PIN is incorrect.' => currentPinIncorrect, + 'Could not load settings.' => couldNotLoadSettings, + 'Could not save settings.' => couldNotSaveSettings, + 'Could not load product.' => couldNotLoadProduct, + 'Could not save product.' => couldNotSaveProduct, + 'Could not delete product.' => couldNotDeleteProduct, + 'Update failed' => updateFailed, + 'Download failed' => downloadFailed, + 'Installation failed' => installationFailed, + 'Update already in progress' => updateAlreadyInProgress, + _ => message ?? genericError, + }; + } + + String updateStatus(String status) { + if (status.startsWith('Downloading ')) { + return downloading(status.substring('Downloading '.length)); + } + + return switch (status) { + 'Preparing download…' => preparingDownload, + 'Download failed' => downloadFailed, + 'Installing update…' => installingUpdate, + 'Update installed! Restarting…' => updateInstalledRestarting, + 'Installation failed' => installationFailed, + 'Update already running' => updateAlreadyRunning, + _ => status, + }; + } + + String updateError(String? message) { + if (message == null) return updateFailed; + if (message.startsWith('Download failed')) return downloadFailedWithDetail; + if (message.startsWith('Installation failed')) { + return installationFailedWithDetail; + } + if (message == 'Simulated download error (network timeout)') { + return simulatedDownloadError; + } + if (message == 'Update already in progress') return updateAlreadyInProgress; + + return localizedError(message); + } + + String devAction(String? message) { + if (message == null) return ''; + + final seeded = RegExp( + r'Seeded (?:default products )?\((\d+) total\)', + ).firstMatch(message); + if (seeded != null) { + return devActionSeededProducts(int.parse(seeded.group(1)!)); + } + + final reseeded = RegExp( + r'Cleared and reseeded \((\d+) products\)', + ).firstMatch(message); + if (reseeded != null) { + return devActionReseededProducts(int.parse(reseeded.group(1)!)); + } + + final resetStock = RegExp( + r'Reset stock for (\d+) products', + ).firstMatch(message); + if (resetStock != null) { + return devActionResetStock(int.parse(resetStock.group(1)!)); + } + + final seededDemo = RegExp( + r'Seeded (\d+) demo products', + ).firstMatch(message); + if (seededDemo != null) { + return devActionSeededProducts(int.parse(seededDemo.group(1)!)); + } + + final testTab = RegExp( + r'Created test tab with (\d+) items', + ).firstMatch(message); + if (testTab != null) { + return devActionTestTab(int.parse(testTab.group(1)!)); + } + + final testProducts = RegExp( + r'Added (\d+) test products', + ).firstMatch(message); + if (testProducts != null) { + return devActionTestProducts(int.parse(testProducts.group(1)!)); + } + + final lowStock = RegExp( + r'Simulated low stock for (\d+) products', + ).firstMatch(message); + if (lowStock != null) { + return devActionLowStock(int.parse(lowStock.group(1)!)); + } + + final mockOrders = RegExp( + r'Generated (\d+) mock orders across (\d+) customers', + ).firstMatch(message); + if (mockOrders != null) { + return devActionMockOrders( + int.parse(mockOrders.group(1)!), + int.parse(mockOrders.group(2)!), + ); + } + + return switch (message) { + 'Cleared all open tabs' => devActionClearedOpenTabs, + 'Cleared closed tab history' => devActionClearedHistory, + 'Cleared all products' => devActionClearedProducts, + 'Product images cleared' => devActionImagesCleared, + 'PIN reset (no PIN required)' => devActionResetPin, + 'No products to add to test tab' => devActionNoProducts, + 'No products available — seed demo data first' => devActionNoProducts, + 'Product images reloaded' => devActionImagesReloaded, + 'Image cache cleared (no-op in debug mode)' => devActionCacheCleared, + _ when message.startsWith('Failed to clear images') => + devActionFailedImages, + _ => message, + }; + } +} diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart new file mode 100644 index 0000000..3c989b8 --- /dev/null +++ b/lib/l10n/app_localizations_nl.dart @@ -0,0 +1,734 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Dutch Flemish (`nl`). +class AppLocalizationsNl extends AppLocalizations { + AppLocalizationsNl([String locale = 'nl']) : super(locale); + + @override + String get appTitle => 'KoolTab'; + + @override + String get barTabs => 'Poefs'; + + @override + String get products => 'Producten'; + + @override + String get addProduct => 'Product toevoegen'; + + @override + String get noProductsYet => 'Nog geen producten.'; + + @override + String get addFirstProduct => 'Voeg je eerste product toe om te beginnen.'; + + @override + String get manageProducts => 'Producten beheren'; + + @override + String get tabHistory => 'Geschiedenis'; + + @override + String get refresh => 'Vernieuwen'; + + @override + String get settings => 'Instellingen'; + + @override + String get logout => 'Uitloggen'; + + @override + String get openOrSelectTab => 'Open of selecteer eerst een poef.'; + + @override + String get openNewTab => 'Nieuw poef openen'; + + @override + String get customerGroupName => 'Naam klant / groep'; + + @override + String get cancel => 'Annuleren'; + + @override + String get openTab => 'Poef openen'; + + @override + String couldNotCreateTab(String error) { + return 'Poef kon niet worden aangemaakt: $error'; + } + + @override + String get searchByName => 'Zoeken op naam…'; + + @override + String get allCustomers => 'Alle klanten'; + + @override + String get customer => 'Klant'; + + @override + String get noClosedTabs => 'Nog geen gesloten poefs'; + + @override + String get settingsTitle => 'Instellingen'; + + @override + String get security => 'Beveiliging'; + + @override + String get pinRequired => 'PIN vereist'; + + @override + String get changePin => 'PIN wijzigen'; + + @override + String get appearance => 'Uiterlijk'; + + @override + String get language => 'Taal'; + + @override + String get languageSystem => 'Systeemstandaard'; + + @override + String get languageEnglish => 'Engels'; + + @override + String get languageDutch => 'Nederlands'; + + @override + String get theme => 'Thema'; + + @override + String get system => 'Systeem'; + + @override + String get light => 'Licht'; + + @override + String get dark => 'Donker'; + + @override + String get ugly => 'Lelijk'; + + @override + String get setPinCode => 'PIN instellen'; + + @override + String get enterPin => 'PIN invoeren'; + + @override + String get enterCurrentPin => 'Huidige PIN invoeren'; + + @override + String get enterNewPin => 'Nieuwe PIN invoeren'; + + @override + String get fourDigits => '4 cijfers'; + + @override + String get enterPinFourDigits => 'PIN invoeren (4 cijfers)'; + + @override + String get confirm => 'Bevestigen'; + + @override + String get confirmPin => 'PIN bevestigen'; + + @override + String get pinExactlyFour => 'PIN moet precies 4 cijfers bevatten'; + + @override + String get couldNotSavePin => 'PIN-instelling kon niet worden opgeslagen.'; + + @override + String get editProduct => 'Product bewerken'; + + @override + String get productName => 'Productnaam'; + + @override + String get category => 'Categorie'; + + @override + String get price => 'Prijs'; + + @override + String get stock => 'Voorraad'; + + @override + String get lowStockThreshold => 'Drempel lage voorraad'; + + @override + String get chooseProductImage => 'Productafbeelding kiezen'; + + @override + String get changeImage => 'Afbeelding wijzigen'; + + @override + String get deleteProduct => 'Product verwijderen?'; + + @override + String get deleteProductDescription => + 'Dit verwijdert het product uit de productlijst.'; + + @override + String get delete => 'Verwijderen'; + + @override + String get productNotFound => 'Product niet gevonden.'; + + @override + String get chooseImage => 'Kies een productafbeelding.'; + + @override + String get selectCategory => 'Selecteer een categorie.'; + + @override + String get enterProductName => 'Voer een productnaam in.'; + + @override + String version(String version) { + return 'Versie $version'; + } + + @override + String get selectTabToAddItems => 'Selecteer een poef om items toe te voegen'; + + @override + String get openTabs => 'Open poefs'; + + @override + String get selectOrOpenTab => 'Selecteer of open een poef'; + + @override + String get noOpenTabs => 'Geen open poefs'; + + @override + String get edit => 'Bewerken'; + + @override + String get close => 'Sluiten'; + + @override + String tabItemSummary(int count, String total) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items - $total', + one: '1 item - $total', + ); + return '$_temp0'; + } + + @override + String tabItemCount(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: '$count items', + one: '1 item', + ); + return '$_temp0'; + } + + @override + String get tapProductsToAdd => 'Tik op producten om ze toe te voegen'; + + @override + String get total => 'Totaal'; + + @override + String get closeTab => 'Poef sluiten'; + + @override + String productStockSummary(String category, String price, int stock) { + return '$category • $price • Voorraad: $stock'; + } + + @override + String get outOfStock => 'UITVERKOCHT'; + + @override + String get tabsYouCloseAppearHere => 'Gesloten poefs verschijnen hier.'; + + @override + String get enterPinSubtitle => 'Je gebruikt dit om de app te ontgrendelen'; + + @override + String get confirmPinSubtitle => 'Voer dezelfde PIN opnieuw in'; + + @override + String get createPin => 'PIN aanmaken'; + + @override + String get pinsDidNotMatch => + 'PINs kwamen niet overeen. Probeer het opnieuw.'; + + @override + String get somethingWentWrong => 'Er is iets misgegaan.'; + + @override + String get incorrectPin => 'Onjuiste PIN.'; + + @override + String get enterPrice => 'Voer een prijs in.'; + + @override + String get enterValidPrice => 'Voer een geldige prijs in.'; + + @override + String get enterValidStock => 'Voer een geldige voorraad in.'; + + @override + String get enterValidThreshold => 'Voer een geldige drempel in.'; + + @override + String get couldNotLoadProduct => 'Product kon niet worden geladen.'; + + @override + String get couldNotSaveProduct => 'Product kon niet worden opgeslagen.'; + + @override + String get couldNotDeleteProduct => 'Product kon niet worden verwijderd.'; + + @override + String get paymentMethod => 'Betaalmethode'; + + @override + String get cash => 'Contant'; + + @override + String get payconiq => 'Payconiq'; + + @override + String closeTabForCustomer(String customerName) { + return 'Poef van $customerName sluiten?'; + } + + @override + String currentTotal(String total) { + return 'Huidig totaal: $total'; + } + + @override + String get couldNotCloseTab => 'Poef kon niet worden gesloten.'; + + @override + String get closingTab => 'Poef sluiten…'; + + @override + String get slideToConfirmClosing => 'Schuif om het sluiten te bevestigen'; + + @override + String get errorScreen => 'Foutscherm'; + + @override + String get unexpectedError => + 'Er is een onverwachte fout opgetreden.\nProbeer de app opnieuw te starten.'; + + @override + String get goToBarScreen => 'Naar barkassa'; + + @override + String get updates => 'Updates'; + + @override + String get checkingForUpdates => 'Controleren op updates…'; + + @override + String get checkForUpdates => 'Controleren op updates'; + + @override + String get latestVersion => 'Je gebruikt al de nieuwste versie.'; + + @override + String get updateCheckFailed => 'Controleren op updates mislukt.'; + + @override + String get updateAvailable => 'Update beschikbaar'; + + @override + String versionAvailable(String version) { + return 'Versie $version is beschikbaar.'; + } + + @override + String get later => 'Later'; + + @override + String get update => 'Updaten'; + + @override + String get updateReady => 'Update klaar'; + + @override + String get updateReadyDescription => + 'De update is gedownload en geïnstalleerd. Start de app nu opnieuw om de wijzigingen toe te passen.'; + + @override + String get restartApp => 'App opnieuw starten'; + + @override + String get updateFailed => 'Update mislukt'; + + @override + String get retry => 'Opnieuw proberen'; + + @override + String get updating => 'Updaten'; + + @override + String get installing => 'Installeren'; + + @override + String get doNotCloseDuringUpdate => 'Sluit de app niet tijdens de update'; + + @override + String get cancelUpdate => 'Update annuleren?'; + + @override + String get cancelUpdateDescription => + 'De update is bezig. Als je nu weggaat, kan de app instabiel worden.'; + + @override + String get continueUpdate => 'Update voortzetten'; + + @override + String get cancelUpdateAction => 'Update annuleren'; + + @override + String get preparingDownload => 'Download voorbereiden…'; + + @override + String get simulatedDownloadError => + 'Gesimuleerde downloadfout (netwerktime-out)'; + + @override + String get downloadFailed => 'Download mislukt'; + + @override + String downloading(String percent) { + return 'Downloaden $percent%'; + } + + @override + String get installingUpdate => 'Update installeren…'; + + @override + String get updateInstalledRestarting => + 'Update geïnstalleerd! Opnieuw starten…'; + + @override + String get installationFailed => 'Installatie mislukt'; + + @override + String get updateAlreadyInProgress => 'Update is al bezig'; + + @override + String get updateAlreadyRunning => 'Update wordt al uitgevoerd'; + + @override + String get genericError => 'Er is een onverwachte fout opgetreden.'; + + @override + String get couldNotLoadBar => 'Barkassa kon niet worden geladen.'; + + @override + String get couldNotCreateTabMessage => 'Poef kon niet worden aangemaakt.'; + + @override + String get productOutOfStock => 'Product is niet op voorraad.'; + + @override + String get couldNotAddProductToTab => + 'Product kon niet aan de poef worden toegevoegd.'; + + @override + String get couldNotUpdateQuantity => 'Aantal kon niet worden bijgewerkt.'; + + @override + String get couldNotReloadTabs => 'Poefs konden niet opnieuw worden geladen.'; + + @override + String get couldNotLoadHistory => 'Geschiedenis kon niet worden geladen.'; + + @override + String get couldNotLoadMoreTabs => 'Meer poefs konden niet worden geladen.'; + + @override + String get couldNotLoadProducts => 'Producten konden niet worden geladen.'; + + @override + String get couldNotAddProduct => 'Product kon niet worden toegevoegd.'; + + @override + String get couldNotUpdateProduct => 'Product kon niet worden bijgewerkt.'; + + @override + String get couldNotDeleteProductMessage => + 'Product kon niet worden verwijderd.'; + + @override + String get couldNotCheckPinStatus => + 'PIN-status kon niet worden gecontroleerd.'; + + @override + String get couldNotVerifyPin => 'PIN kon niet worden gecontroleerd.'; + + @override + String get currentPinIncorrect => 'Huidige PIN is onjuist.'; + + @override + String get couldNotLoadSettings => 'Instellingen konden niet worden geladen.'; + + @override + String get couldNotSaveSettings => + 'Instellingen konden niet worden opgeslagen.'; + + @override + String get downloadFailedWithDetail => 'Download mislukt.'; + + @override + String get installationFailedWithDetail => 'Installatie mislukt.'; + + @override + String get devMenu => 'Ontwikkelaarsmenu'; + + @override + String get dataManagement => 'Databeheer'; + + @override + String get clearOpenTabs => 'Alle open poefs wissen'; + + @override + String get clearClosedHistory => 'Geschiedenis van gesloten poefs wissen'; + + @override + String get seedDefaultProducts => 'Standaardproducten vullen'; + + @override + String get onlySeedsWhenEmpty => 'Alleen vullen als de tabel leeg is'; + + @override + String get clearAndReseedProducts => 'Producten wissen en opnieuw vullen'; + + @override + String get wipesAndReseeds => + 'Wist alle producten en vult de standaardproducten opnieuw'; + + @override + String get clearAllProducts => 'Alle producten wissen'; + + @override + String get clearProductImages => 'Productafbeeldingen wissen'; + + @override + String get deletesProductImages => + 'Verwijdert afbeeldingen uit de map product_images'; + + @override + String get debugging => 'Debuggen'; + + @override + String get resetPin => 'PIN resetten'; + + @override + String get removePinLock => 'PIN-vergrendeling verwijderen'; + + @override + String get toggleDebugOverlay => 'Debug-overlay in-/uitschakelen'; + + @override + String get showFpsMemoryWidgets => 'FPS, geheugen en widgets tonen'; + + @override + String get errorScreenMenu => 'Foutscherm'; + + @override + String get viewErrorScreen => 'Het foutscherm bekijken'; + + @override + String get featureToggles => 'Functieschakelaars'; + + @override + String get simulateUpdateDownload => 'Updatedownload simuleren'; + + @override + String get openDownloadProgress => 'Downloadvoortgang openen'; + + @override + String get testingHelpers => 'Testhelpers'; + + @override + String get createTestTab => 'Testtabblad maken'; + + @override + String get addRandomItems => 'Tabblad met willekeurige items toevoegen'; + + @override + String get addTestProducts => '100 testproducten toevoegen'; + + @override + String get stressTestGrid => 'Productraster stresstesten'; + + @override + String get simulateLowStock => 'Lage voorraad simuleren'; + + @override + String get setProductsBelowThreshold => + 'Alle producten onder de drempel instellen'; + + @override + String get generateMockOrders => '100 testbestellingen genereren'; + + @override + String get randomCustomersItemsAmounts => + 'Willekeurige klanten, items en bedragen'; + + @override + String get performance => 'Prestaties'; + + @override + String get clearImageCache => 'Afbeeldingencache wissen'; + + @override + String get reloadProductImages => 'Productafbeeldingen opnieuw laden'; + + @override + String get debugOverlay => 'Debug-overlay'; + + @override + String get debugOverlayDescription => + 'De debug-overlay toont FPS, geheugengebruik en het aantal widgets. Schakel deze in via Flutter DevTools in debugmodus.'; + + @override + String get ok => 'OK'; + + @override + String get simulateUpdate => 'Update simuleren'; + + @override + String get chooseSimulationMode => 'Kies een simulatiemodus:'; + + @override + String get successfulDownload => 'Geslaagde download'; + + @override + String get progressThenInstall => + 'Voortgang 0→100%, daarna installeren en voltooien'; + + @override + String get downloadError => 'Downloadfout'; + + @override + String get failsWithNetworkTimeout => + 'Mislukt bij 50% door een netwerktime-out'; + + @override + String get realDownloadWillFail => 'Echte download (mislukt)'; + + @override + String get attemptsFakeUrl => 'Probeert OTA met een valse URL'; + + @override + String get categoryBeer => 'Bier'; + + @override + String get categoryWine => 'Wijn'; + + @override + String get categorySoftDrinks => 'Frisdrank'; + + @override + String get categoryCocktails => 'Cocktails'; + + @override + String get categorySnacks => 'Snacks'; + + @override + String get categoryCoffee => 'Koffie'; + + @override + String get categoryTea => 'Thee'; + + @override + String get categoryOther => 'Overig'; + + @override + String get devActionClearedOpenTabs => 'Alle open tabbladen gewist'; + + @override + String get devActionClearedHistory => + 'Geschiedenis van gesloten tabbladen gewist'; + + @override + String get devActionClearedProducts => 'Alle producten gewist'; + + @override + String devActionSeededProducts(int count) { + return '$count producten toegevoegd'; + } + + @override + String devActionReseededProducts(int count) { + return '$count producten gewist en opnieuw toegevoegd'; + } + + @override + String get devActionImagesCleared => 'Productafbeeldingen gewist'; + + @override + String get devActionResetPin => 'PIN gereset (geen PIN vereist)'; + + @override + String get devActionNoProducts => 'Geen producten beschikbaar'; + + @override + String get devActionImagesReloaded => 'Productafbeeldingen opnieuw geladen'; + + @override + String get devActionCacheCleared => + 'Afbeeldingencache gewist (geen actie in debugmodus)'; + + @override + String devActionLowStock(int count) { + return 'Lage voorraad gesimuleerd voor $count producten'; + } + + @override + String devActionTestTab(int count) { + return 'Testtabblad gemaakt met $count items'; + } + + @override + String devActionTestProducts(int count) { + return '$count testproducten toegevoegd'; + } + + @override + String devActionMockOrders(int orders, int customers) { + return '$orders testbestellingen gegenereerd voor $customers klanten'; + } + + @override + String get devActionFailedImages => 'Afbeeldingen konden niet worden gewist.'; + + @override + String devActionResetStock(int count) { + return 'Voorraad gereset voor $count producten'; + } + + @override + String get paymentCash => 'Contant'; + + @override + String get paymentPayconiq => 'Payconiq'; + + @override + String get simulatedUpdateNotes => 'Bugfixes en prestatieverbeteringen.'; +} diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb new file mode 100644 index 0000000..69b887a --- /dev/null +++ b/lib/l10n/app_nl.arb @@ -0,0 +1,239 @@ +{ + "@@locale": "nl", + "appTitle": "KoolTab", + "barTabs": "Poefs", + "products": "Producten", + "addProduct": "Product toevoegen", + "noProductsYet": "Nog geen producten.", + "addFirstProduct": "Voeg je eerste product toe om te beginnen.", + "manageProducts": "Producten beheren", + "tabHistory": "Geschiedenis", + "refresh": "Vernieuwen", + "settings": "Instellingen", + "logout": "Uitloggen", + "openOrSelectTab": "Open of selecteer eerst een poef.", + "openNewTab": "Nieuw poef openen", + "customerGroupName": "Naam klant / groep", + "cancel": "Annuleren", + "openTab": "Poef openen", + "couldNotCreateTab": "Poef kon niet worden aangemaakt: {error}", + "@couldNotCreateTab": {"placeholders": {"error": {"type": "String"}}}, + "searchByName": "Zoeken op naam…", + "allCustomers": "Alle klanten", + "customer": "Klant", + "noClosedTabs": "Nog geen gesloten poefs", + "settingsTitle": "Instellingen", + "security": "Beveiliging", + "pinRequired": "PIN vereist", + "changePin": "PIN wijzigen", + "appearance": "Uiterlijk", + "language": "Taal", + "languageSystem": "Systeemstandaard", + "languageEnglish": "Engels", + "languageDutch": "Nederlands", + "theme": "Thema", + "system": "Systeem", + "light": "Licht", + "dark": "Donker", + "ugly": "Lelijk", + "setPinCode": "PIN instellen", + "enterPin": "PIN invoeren", + "enterCurrentPin": "Huidige PIN invoeren", + "enterNewPin": "Nieuwe PIN invoeren", + "fourDigits": "4 cijfers", + "enterPinFourDigits": "PIN invoeren (4 cijfers)", + "confirm": "Bevestigen", + "confirmPin": "PIN bevestigen", + "pinExactlyFour": "PIN moet precies 4 cijfers bevatten", + "couldNotSavePin": "PIN-instelling kon niet worden opgeslagen.", + "editProduct": "Product bewerken", + "productName": "Productnaam", + "category": "Categorie", + "price": "Prijs", + "stock": "Voorraad", + "lowStockThreshold": "Drempel lage voorraad", + "chooseProductImage": "Productafbeelding kiezen", + "changeImage": "Afbeelding wijzigen", + "deleteProduct": "Product verwijderen?", + "deleteProductDescription": "Dit verwijdert het product uit de productlijst.", + "delete": "Verwijderen", + "productNotFound": "Product niet gevonden.", + "chooseImage": "Kies een productafbeelding.", + "selectCategory": "Selecteer een categorie.", + "enterProductName": "Voer een productnaam in.", + "version": "Versie {version}", + "@version": {"placeholders": {"version": {"type": "String"}}}, + "selectTabToAddItems": "Selecteer een poef om items toe te voegen", + "openTabs": "Open poefs", + "selectOrOpenTab": "Selecteer of open een poef", + "noOpenTabs": "Geen open poefs", + "edit": "Bewerken", + "close": "Sluiten", + "tabItemSummary": "{count, plural, =1 {1 item - {total}} other {{count} items - {total}}}", + "@tabItemSummary": {"placeholders": {"count": {"type": "int"}, "total": {"type": "String"}}}, + "tabItemCount": "{count, plural, =1 {1 item} other {{count} items}}", + "@tabItemCount": {"placeholders": {"count": {"type": "int"}}}, + "tapProductsToAdd": "Tik op producten om ze toe te voegen", + "total": "Totaal", + "closeTab": "Poef sluiten", + "productStockSummary": "{category} • {price} • Voorraad: {stock}", + "@productStockSummary": {"placeholders": {"category": {"type": "String"}, "price": {"type": "String"}, "stock": {"type": "int"}}}, + "outOfStock": "UITVERKOCHT", + "tabsYouCloseAppearHere": "Gesloten poefs verschijnen hier.", + "enterPinSubtitle": "Je gebruikt dit om de app te ontgrendelen", + "confirmPinSubtitle": "Voer dezelfde PIN opnieuw in", + "createPin": "PIN aanmaken", + "pinsDidNotMatch": "PINs kwamen niet overeen. Probeer het opnieuw.", + "somethingWentWrong": "Er is iets misgegaan.", + "incorrectPin": "Onjuiste PIN.", + "enterPrice": "Voer een prijs in.", + "enterValidPrice": "Voer een geldige prijs in.", + "enterValidStock": "Voer een geldige voorraad in.", + "enterValidThreshold": "Voer een geldige drempel in.", + "couldNotLoadProduct": "Product kon niet worden geladen.", + "couldNotSaveProduct": "Product kon niet worden opgeslagen.", + "couldNotDeleteProduct": "Product kon niet worden verwijderd.", + "paymentMethod": "Betaalmethode", + "cash": "Contant", + "payconiq": "Payconiq", + "closeTabForCustomer": "Poef van {customerName} sluiten?", + "@closeTabForCustomer": {"placeholders": {"customerName": {"type": "String"}}}, + "currentTotal": "Huidig totaal: {total}", + "@currentTotal": {"placeholders": {"total": {"type": "String"}}}, + "couldNotCloseTab": "Poef kon niet worden gesloten.", + "closingTab": "Poef sluiten…", + "slideToConfirmClosing": "Schuif om het sluiten te bevestigen", + "errorScreen": "Foutscherm", + "unexpectedError": "Er is een onverwachte fout opgetreden.\nProbeer de app opnieuw te starten.", + "goToBarScreen": "Naar barkassa", + "updates": "Updates", + "checkingForUpdates": "Controleren op updates…", + "checkForUpdates": "Controleren op updates", + "latestVersion": "Je gebruikt al de nieuwste versie.", + "updateCheckFailed": "Controleren op updates mislukt.", + "updateAvailable": "Update beschikbaar", + "versionAvailable": "Versie {version} is beschikbaar.", + "@versionAvailable": {"placeholders": {"version": {"type": "String"}}}, + "later": "Later", + "update": "Updaten", + "updateReady": "Update klaar", + "updateReadyDescription": "De update is gedownload en geïnstalleerd. Start de app nu opnieuw om de wijzigingen toe te passen.", + "restartApp": "App opnieuw starten", + "updateFailed": "Update mislukt", + "retry": "Opnieuw proberen", + "updating": "Updaten", + "installing": "Installeren", + "doNotCloseDuringUpdate": "Sluit de app niet tijdens de update", + "cancelUpdate": "Update annuleren?", + "cancelUpdateDescription": "De update is bezig. Als je nu weggaat, kan de app instabiel worden.", + "continueUpdate": "Update voortzetten", + "cancelUpdateAction": "Update annuleren", + "preparingDownload": "Download voorbereiden…", + "simulatedDownloadError": "Gesimuleerde downloadfout (netwerktime-out)", + "downloadFailed": "Download mislukt", + "downloading": "Downloaden {percent}%", + "@downloading": {"placeholders": {"percent": {"type": "String"}}}, + "installingUpdate": "Update installeren…", + "updateInstalledRestarting": "Update geïnstalleerd! Opnieuw starten…", + "installationFailed": "Installatie mislukt", + "updateAlreadyInProgress": "Update is al bezig", + "updateAlreadyRunning": "Update wordt al uitgevoerd", + "genericError": "Er is een onverwachte fout opgetreden.", + "couldNotLoadBar": "Barkassa kon niet worden geladen.", + "couldNotCreateTabMessage": "Poef kon niet worden aangemaakt.", + "productOutOfStock": "Product is niet op voorraad.", + "couldNotAddProductToTab": "Product kon niet aan de poef worden toegevoegd.", + "couldNotUpdateQuantity": "Aantal kon niet worden bijgewerkt.", + "couldNotReloadTabs": "Poefs konden niet opnieuw worden geladen.", + "couldNotLoadHistory": "Geschiedenis kon niet worden geladen.", + "couldNotLoadMoreTabs": "Meer poefs konden niet worden geladen.", + "couldNotLoadProducts": "Producten konden niet worden geladen.", + "couldNotAddProduct": "Product kon niet worden toegevoegd.", + "couldNotUpdateProduct": "Product kon niet worden bijgewerkt.", + "couldNotDeleteProductMessage": "Product kon niet worden verwijderd.", + "couldNotCheckPinStatus": "PIN-status kon niet worden gecontroleerd.", + "couldNotVerifyPin": "PIN kon niet worden gecontroleerd.", + "currentPinIncorrect": "Huidige PIN is onjuist.", + "couldNotLoadSettings": "Instellingen konden niet worden geladen.", + "couldNotSaveSettings": "Instellingen konden niet worden opgeslagen.", + "downloadFailedWithDetail": "Download mislukt.", + "installationFailedWithDetail": "Installatie mislukt.", + "devMenu": "Ontwikkelaarsmenu", + "dataManagement": "Databeheer", + "clearOpenTabs": "Alle open poefs wissen", + "clearClosedHistory": "Geschiedenis van gesloten poefs wissen", + "seedDefaultProducts": "Standaardproducten vullen", + "onlySeedsWhenEmpty": "Alleen vullen als de tabel leeg is", + "clearAndReseedProducts": "Producten wissen en opnieuw vullen", + "wipesAndReseeds": "Wist alle producten en vult de standaardproducten opnieuw", + "clearAllProducts": "Alle producten wissen", + "clearProductImages": "Productafbeeldingen wissen", + "deletesProductImages": "Verwijdert afbeeldingen uit de map product_images", + "debugging": "Debuggen", + "resetPin": "PIN resetten", + "removePinLock": "PIN-vergrendeling verwijderen", + "toggleDebugOverlay": "Debug-overlay in-/uitschakelen", + "showFpsMemoryWidgets": "FPS, geheugen en widgets tonen", + "errorScreenMenu": "Foutscherm", + "viewErrorScreen": "Het foutscherm bekijken", + "featureToggles": "Functieschakelaars", + "simulateUpdateDownload": "Updatedownload simuleren", + "openDownloadProgress": "Downloadvoortgang openen", + "testingHelpers": "Testhelpers", + "createTestTab": "Testtabblad maken", + "addRandomItems": "Tabblad met willekeurige items toevoegen", + "addTestProducts": "100 testproducten toevoegen", + "stressTestGrid": "Productraster stresstesten", + "simulateLowStock": "Lage voorraad simuleren", + "setProductsBelowThreshold": "Alle producten onder de drempel instellen", + "generateMockOrders": "100 testbestellingen genereren", + "randomCustomersItemsAmounts": "Willekeurige klanten, items en bedragen", + "performance": "Prestaties", + "clearImageCache": "Afbeeldingencache wissen", + "reloadProductImages": "Productafbeeldingen opnieuw laden", + "debugOverlay": "Debug-overlay", + "debugOverlayDescription": "De debug-overlay toont FPS, geheugengebruik en het aantal widgets. Schakel deze in via Flutter DevTools in debugmodus.", + "ok": "OK", + "simulateUpdate": "Update simuleren", + "chooseSimulationMode": "Kies een simulatiemodus:", + "successfulDownload": "Geslaagde download", + "progressThenInstall": "Voortgang 0→100%, daarna installeren en voltooien", + "downloadError": "Downloadfout", + "failsWithNetworkTimeout": "Mislukt bij 50% door een netwerktime-out", + "realDownloadWillFail": "Echte download (mislukt)", + "attemptsFakeUrl": "Probeert OTA met een valse URL", + "categoryBeer": "Bier", + "categoryWine": "Wijn", + "categorySoftDrinks": "Frisdrank", + "categoryCocktails": "Cocktails", + "categorySnacks": "Snacks", + "categoryCoffee": "Koffie", + "categoryTea": "Thee", + "categoryOther": "Overig", + "devActionClearedOpenTabs": "Alle open tabbladen gewist", + "devActionClearedHistory": "Geschiedenis van gesloten tabbladen gewist", + "devActionClearedProducts": "Alle producten gewist", + "devActionSeededProducts": "{count} producten toegevoegd", + "@devActionSeededProducts": {"placeholders": {"count": {"type": "int"}}}, + "devActionReseededProducts": "{count} producten gewist en opnieuw toegevoegd", + "@devActionReseededProducts": {"placeholders": {"count": {"type": "int"}}}, + "devActionImagesCleared": "Productafbeeldingen gewist", + "devActionResetPin": "PIN gereset (geen PIN vereist)", + "devActionNoProducts": "Geen producten beschikbaar", + "devActionImagesReloaded": "Productafbeeldingen opnieuw geladen", + "devActionCacheCleared": "Afbeeldingencache gewist (geen actie in debugmodus)", + "devActionLowStock": "Lage voorraad gesimuleerd voor {count} producten", + "@devActionLowStock": {"placeholders": {"count": {"type": "int"}}}, + "devActionTestTab": "Testtabblad gemaakt met {count} items", + "@devActionTestTab": {"placeholders": {"count": {"type": "int"}}}, + "devActionTestProducts": "{count} testproducten toegevoegd", + "@devActionTestProducts": {"placeholders": {"count": {"type": "int"}}}, + "devActionMockOrders": "{orders} testbestellingen gegenereerd voor {customers} klanten", + "@devActionMockOrders": {"placeholders": {"orders": {"type": "int"}, "customers": {"type": "int"}}}, + "devActionFailedImages": "Afbeeldingen konden niet worden gewist.", + "devActionResetStock": "Voorraad gereset voor {count} producten", + "@devActionResetStock": {"placeholders": {"count": {"type": "int"}}}, + "paymentCash": "Contant", + "paymentPayconiq": "Payconiq", + "simulatedUpdateNotes": "Bugfixes en prestatieverbeteringen." +} diff --git a/lib/models/settings.dart b/lib/models/settings.dart index e3836fb..d36ed6f 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,15 +1,26 @@ enum AppThemeMode { system, light, dark, ugly } +enum AppLanguage { system, english, dutch } class AppSettings { final bool pinRequired; final AppThemeMode themeMode; + final AppLanguage language; - const AppSettings({required this.pinRequired, required this.themeMode}); + const AppSettings({ + required this.pinRequired, + required this.themeMode, + this.language = AppLanguage.system, + }); - AppSettings copyWith({bool? pinRequired, AppThemeMode? themeMode}) { + AppSettings copyWith({ + bool? pinRequired, + AppThemeMode? themeMode, + AppLanguage? language, + }) { return AppSettings( pinRequired: pinRequired ?? this.pinRequired, themeMode: themeMode ?? this.themeMode, + language: language ?? this.language, ); } @@ -23,8 +34,9 @@ class AppSettings { identical(this, other) || other is AppSettings && pinRequired == other.pinRequired && - themeMode == other.themeMode; + themeMode == other.themeMode && + language == other.language; @override - int get hashCode => Object.hash(pinRequired, themeMode); + int get hashCode => Object.hash(pinRequired, themeMode, language); } diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index ffa2041..291015b 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -22,6 +22,10 @@ class DriftSettingsService implements SettingsService { (mode) => mode.name == row.themeMode, orElse: () => AppThemeMode.system, ), + language: AppLanguage.values.firstWhere( + (language) => language.name == row.language, + orElse: () => AppLanguage.system, + ), ); } @@ -46,6 +50,7 @@ class DriftSettingsService implements SettingsService { id: _settingsId, pinRequired: Value(settings.pinRequired), themeMode: Value(settings.themeMode.name), + language: Value(settings.language.name), ), ); } diff --git a/lib/utils/app_updater.dart b/lib/utils/app_updater.dart index 176dc0f..54565c3 100644 --- a/lib/utils/app_updater.dart +++ b/lib/utils/app_updater.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:kooltab2/utils/app_update_util.dart'; import 'app_config.dart'; +import '../l10n/app_localizations.dart'; class UpdateChecker { static bool _hasChecked = false; @@ -39,16 +40,18 @@ class UpdateChecker { } static void _showUpdateDialog(BuildContext context, UpdateInfo update) { + final l10n = AppLocalizations.of(context); + showDialog( context: context, barrierDismissible: !update.mandatory, builder: (dialogContext) => AlertDialog( - title: const Text("Update available"), + title: Text(l10n.updateAvailable), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Version ${update.version} is available."), + Text(l10n.versionAvailable(update.version)), const SizedBox(height: 12), Text(update.notes), ], @@ -57,14 +60,14 @@ class UpdateChecker { if (!update.mandatory) TextButton( onPressed: () => Navigator.pop(dialogContext), - child: const Text("Later"), + child: Text(l10n.later), ), FilledButton( onPressed: () { Navigator.pop(dialogContext); context.push('/update-progress', extra: update); }, - child: const Text("Update"), + child: Text(l10n.update), ), ], ), diff --git a/lib/viewmodels/settings_view_model.dart b/lib/viewmodels/settings_view_model.dart index 9d3a115..05239e9 100644 --- a/lib/viewmodels/settings_view_model.dart +++ b/lib/viewmodels/settings_view_model.dart @@ -111,6 +111,9 @@ class SettingsViewModel extends ChangeNotifier { Future updateThemeMode(AppThemeMode mode) => _save(_settings.copyWith(themeMode: mode)); + Future updateLanguage(AppLanguage language) => + _save(_settings.copyWith(language: language)); + Future _save(AppSettings updated) async { final previous = _settings; _settings = updated; diff --git a/lib/views/bar_screen_view.dart b/lib/views/bar_screen_view.dart index 01f872b..0e03357 100644 --- a/lib/views/bar_screen_view.dart +++ b/lib/views/bar_screen_view.dart @@ -14,6 +14,8 @@ import '../models/product.dart'; import '../models/tab_item.dart'; import '../utils/app_updater.dart'; import '../viewmodels/bar_screen_view_model.dart'; +import '../l10n/app_localizations.dart'; +import '../l10n/app_localizations_helpers.dart'; class BarScreenView extends StatefulWidget { const BarScreenView({super.key}); @@ -33,40 +35,41 @@ class _BarScreenViewState extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final viewModel = context.watch(); final productsViewModel = context.watch(); return Scaffold( appBar: AppBar( - title: const Text('Bar Tabs'), + title: Text(l10n.barTabs), actions: [ IconButton( - tooltip: 'Manage products', + tooltip: l10n.manageProducts, onPressed: () => context.go('/products'), icon: const Icon(Icons.inventory_2_outlined), ), const SizedBox(width: 6), IconButton( - tooltip: 'Tab history', + tooltip: l10n.tabHistory, onPressed: () => context.go('/history'), icon: const Icon(Icons.history_rounded), ), const SizedBox(width: 6), IconButton( - tooltip: 'Refresh', + tooltip: l10n.refresh, onPressed: () => viewModel.load(), icon: const Icon(Icons.refresh_rounded), ), const SizedBox(width: 6), IconButton( - tooltip: 'Settings', + tooltip: l10n.settings, onPressed: () => context.go('/settings'), icon: const Icon(Icons.settings), ), if (context.watch().isPinSet) ...[ const SizedBox(width: 6), IconButton( - tooltip: 'Logout', + tooltip: l10n.logout, onPressed: () { Provider.of(context, listen: false).lock(); }, @@ -99,7 +102,7 @@ class _BarScreenViewState extends State { ), const SizedBox(height: 12), Text( - viewModel.errorMessage!, + l10n.localizedError(viewModel.errorMessage), textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), @@ -119,9 +122,7 @@ class _BarScreenViewState extends State { onProductTap: (product) async { if (viewModel.selectedTab == null) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Open or select a tab first.'), - ), + SnackBar(content: Text(l10n.openOrSelectTab)), ); return; } @@ -131,9 +132,9 @@ class _BarScreenViewState extends State { } catch (e, stack) { Sentry.captureException(e, stackTrace: stack); if (!context.mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('$e'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.couldNotAddProductToTab)), + ); } }, ), @@ -177,6 +178,7 @@ class _ProductGrid extends StatelessWidget { @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); if (products.isEmpty) { return Center( @@ -197,19 +199,19 @@ class _ProductGrid extends StatelessWidget { ), const SizedBox(height: 16), Text( - 'No products yet', + l10n.noProductsYet, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text( - 'Add your first product to start selling.', + l10n.addFirstProduct, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 20), FilledButton.icon( onPressed: () => context.go('/products/new'), icon: const Icon(Icons.add), - label: const Text('Add product'), + label: Text(l10n.addProduct), ), ], ), @@ -223,7 +225,10 @@ class _ProductGrid extends StatelessWidget { padding: const EdgeInsets.fromLTRB(20, 18, 20, 4), child: Row( children: [ - Text('Products', style: Theme.of(context).textTheme.titleLarge), + Text( + l10n.products, + style: Theme.of(context).textTheme.titleLarge, + ), const SizedBox(width: 10), if (!hasSelectedTab) Flexible( @@ -247,7 +252,7 @@ class _ProductGrid extends StatelessWidget { const SizedBox(width: 4), Flexible( child: Text( - 'Select a tab to add items', + l10n.selectTabToAddItems, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall, ), @@ -335,6 +340,7 @@ class _TabPanelState extends State<_TabPanel> { @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); final filteredTabs = widget.tabs.where((tab) { return tab.customerName.toLowerCase().contains( @@ -362,8 +368,8 @@ class _TabPanelState extends State<_TabPanel> { _searchQuery = value; }); }, - decoration: const InputDecoration( - hintText: 'Search by name…', + decoration: InputDecoration( + hintText: l10n.searchByName, prefixIcon: Icon(Icons.search_rounded, size: 20), isDense: true, ), @@ -375,7 +381,7 @@ class _TabPanelState extends State<_TabPanel> { IconButton.filled( onPressed: widget.onNewTabPressed, icon: const Icon(Icons.add_rounded), - tooltip: 'Open new tab', + tooltip: l10n.openNewTab, ), ], ), @@ -385,7 +391,7 @@ class _TabPanelState extends State<_TabPanel> { Row( children: [ Text( - 'OPEN TABS', + l10n.openTabs.toUpperCase(), style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.w700, letterSpacing: 0.8, @@ -438,7 +444,7 @@ class _TabPanelState extends State<_TabPanel> { ), const SizedBox(height: 10), Text( - 'Select or open a tab', + l10n.selectOrOpenTab, style: Theme.of(context).textTheme.bodyMedium, ), ], @@ -472,10 +478,11 @@ class _OpenTabsList extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); if (tabs.isEmpty) { return Center( child: Text( - 'No open tabs', + l10n.noOpenTabs, style: Theme.of(context).textTheme.bodyMedium, ), ); @@ -501,7 +508,7 @@ class _OpenTabsList extends StatelessWidget { SlidableAction( onPressed: (_) => onTabSelected(tab.id), icon: Icons.edit_outlined, - label: 'Edit', + label: l10n.edit, backgroundColor: Theme.of(context).colorScheme.secondary, foregroundColor: Theme.of(context).colorScheme.onSecondary, ), @@ -513,13 +520,13 @@ class _OpenTabsList extends StatelessWidget { Sentry.captureException(e, stackTrace: stack); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Could not close tab: $e')), + SnackBar(content: Text(l10n.couldNotCloseTab)), ); } } }, icon: Icons.close_rounded, - label: 'Close', + label: l10n.close, backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, ), @@ -567,7 +574,10 @@ class _OpenTabsList extends StatelessWidget { ), const SizedBox(height: 4), Text( - '${tab.itemCount} items - ${tab.formattedTotal}', + l10n.tabItemSummary( + tab.itemCount, + tab.formattedTotal, + ), style: Theme.of(context).textTheme.bodySmall, ), ], @@ -598,6 +608,7 @@ class _SelectedTabDetails extends StatelessWidget { @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -629,7 +640,7 @@ class _SelectedTabDetails extends StatelessWidget { ), const SizedBox(height: 8), Text( - 'Tap products to add them', + l10n.tapProductsToAdd, style: Theme.of(context).textTheme.bodyMedium, ), ], @@ -668,7 +679,10 @@ class _SelectedTabDetails extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Total', style: Theme.of(context).textTheme.bodySmall), + Text( + l10n.total, + style: Theme.of(context).textTheme.bodySmall, + ), Text( tab.formattedTotal, style: Theme.of( @@ -680,7 +694,7 @@ class _SelectedTabDetails extends StatelessWidget { ), FilledButton( onPressed: tab.items.isEmpty ? null : onCloseTabPressed, - child: const Text('Close tab'), + child: Text(l10n.closeTab), ), ], ), @@ -699,6 +713,7 @@ class _TabItemRow extends StatelessWidget { @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); return Padding( padding: const EdgeInsets.symmetric(vertical: 10), @@ -739,9 +754,7 @@ class _TabItemRow extends StatelessWidget { Sentry.captureException(e, stackTrace: stack); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Could not update quantity: $e'), - ), + SnackBar(content: Text(l10n.couldNotUpdateQuantity)), ); } } @@ -765,9 +778,7 @@ class _TabItemRow extends StatelessWidget { Sentry.captureException(e, stackTrace: stack); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Could not update quantity: $e'), - ), + SnackBar(content: Text(l10n.couldNotUpdateQuantity)), ); } } diff --git a/lib/views/dev_menu_view.dart b/lib/views/dev_menu_view.dart index 2041665..a44e596 100644 --- a/lib/views/dev_menu_view.dart +++ b/lib/views/dev_menu_view.dart @@ -4,6 +4,8 @@ import 'package:provider/provider.dart'; import '../utils/app_update_util.dart'; import '../viewmodels/dev_menu_view_model.dart'; +import '../l10n/app_localizations.dart'; +import '../l10n/app_localizations_helpers.dart'; class DevMenuView extends StatefulWidget { const DevMenuView({super.key}); @@ -35,10 +37,11 @@ class _DevMenuViewState extends State { final message = vm.lastAction; if (message == null) return; + final l10n = AppLocalizations.of(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.devAction(message)))); } @override @@ -50,6 +53,7 @@ class _DevMenuViewState extends State { @override Widget build(BuildContext context) { final vm = context.watch(); + final l10n = AppLocalizations.of(context); return Scaffold( appBar: AppBar( @@ -57,109 +61,109 @@ class _DevMenuViewState extends State { onPressed: () => context.pop(), icon: const Icon(Icons.arrow_back), ), - title: const Text('Dev Menu'), + title: Text(l10n.devMenu), centerTitle: true, ), body: ListView( padding: const EdgeInsets.symmetric(vertical: 12), children: [ _Section( - title: 'Data Management', + title: l10n.dataManagement, children: [ _Tile( icon: Icons.delete_sweep_rounded, - title: 'Clear all open tabs', + title: l10n.clearOpenTabs, onTap: vm.isLoading ? null : () => vm.clearOpenTabs(), ), _Tile( icon: Icons.history_rounded, - title: 'Clear closed tab history', + title: l10n.clearClosedHistory, onTap: vm.isLoading ? null : () => vm.clearClosedTabHistory(), ), _Tile( icon: Icons.add_box_rounded, - title: 'Seed default products', - subtitle: 'Only seeds if table is empty', + title: l10n.seedDefaultProducts, + subtitle: l10n.onlySeedsWhenEmpty, onTap: vm.isLoading ? null : () => vm.seedDefaultProducts(), ), _Tile( icon: Icons.restart_alt_rounded, - title: 'Clear & reseed products', - subtitle: 'Wipes all products, then seeds defaults', + title: l10n.clearAndReseedProducts, + subtitle: l10n.wipesAndReseeds, onTap: vm.isLoading ? null : () => vm.clearAndReseedProducts(), ), _Tile( icon: Icons.delete_forever_rounded, - title: 'Clear all products', + title: l10n.clearAllProducts, onTap: vm.isLoading ? null : () => vm.clearAllProducts(), ), _Tile( icon: Icons.image_rounded, - title: 'Clear product images', - subtitle: 'Deletes images from product_images folder', + title: l10n.clearProductImages, + subtitle: l10n.deletesProductImages, onTap: vm.isLoading ? null : () => vm.clearProductImages(), ), ], ), _Section( - title: 'Debugging', + title: l10n.debugging, children: [ _Tile( icon: Icons.lock_reset_rounded, - title: 'Reset PIN', - subtitle: 'Remove PIN lock', + title: l10n.resetPin, + subtitle: l10n.removePinLock, onTap: vm.isLoading ? null : () => vm.resetPin(), ), _Tile( icon: Icons.bug_report_rounded, - title: 'Toggle debug overlay', - subtitle: 'Show FPS, memory, widget count', + title: l10n.toggleDebugOverlay, + subtitle: l10n.showFpsMemoryWidgets, onTap: () => _showDebugOverlayInfo(), ), _Tile( icon: Icons.error_outline_rounded, - title: 'Error screen', - subtitle: 'View the error screen UI', + title: l10n.errorScreenMenu, + subtitle: l10n.viewErrorScreen, onTap: () => context.push('/error'), ), ], ), _Section( - title: 'Feature Toggles', + title: l10n.featureToggles, children: [ _Tile( icon: Icons.update_rounded, - title: 'Simulate update download', - subtitle: 'Open the download progress screen', + title: l10n.simulateUpdateDownload, + subtitle: l10n.openDownloadProgress, onTap: () => _showSimulateUpdateDialog(), ), ], ), _Section( - title: 'Testing Helpers', + title: l10n.testingHelpers, children: [ _Tile( icon: Icons.receipt_long_rounded, - title: 'Create test tab', - subtitle: 'Add tab with random items', + title: l10n.createTestTab, + subtitle: l10n.addRandomItems, onTap: vm.isLoading ? null : () => vm.createTestTab(), ), _Tile( icon: Icons.grid_view_rounded, - title: 'Add 100 test products', - subtitle: 'Stress test product grid', + title: l10n.addTestProducts, + subtitle: l10n.stressTestGrid, onTap: vm.isLoading ? null : () => vm.addTestProducts(), ), _Tile( icon: Icons.warning_rounded, - title: 'Simulate low stock', - subtitle: 'Set all products below threshold', + title: l10n.simulateLowStock, + subtitle: l10n.setProductsBelowThreshold, onTap: vm.isLoading ? null : () => vm.simulateLowStock(), ), _Tile( icon: Icons.history_rounded, - title: 'Generate 100 mock orders', - subtitle: 'Random customers, items, and amounts', + title: l10n.generateMockOrders, + subtitle: l10n.randomCustomersItemsAmounts, onTap: vm.isLoading ? null : () => vm.generateMockOrders(count: 100), @@ -167,16 +171,16 @@ class _DevMenuViewState extends State { ], ), _Section( - title: 'Performance', + title: l10n.performance, children: [ _Tile( icon: Icons.image_rounded, - title: 'Clear image cache', + title: l10n.clearImageCache, onTap: vm.isLoading ? null : () => vm.clearImageCache(), ), _Tile( icon: Icons.refresh_rounded, - title: 'Reload product images', + title: l10n.reloadProductImages, onTap: vm.isLoading ? null : () => vm.reloadProductImages(), ), ], @@ -192,18 +196,17 @@ class _DevMenuViewState extends State { } void _showDebugOverlayInfo() { + final l10n = AppLocalizations.of(context); + showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Debug Overlay'), - content: const Text( - 'The debug overlay shows FPS, memory usage, and widget counts. ' - 'Enable it via Flutter DevTools in debug mode.', - ), + title: Text(l10n.debugOverlay), + content: Text(l10n.debugOverlayDescription), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('OK'), + child: Text(l10n.ok), ), ], ), @@ -211,10 +214,12 @@ class _DevMenuViewState extends State { } void _showSimulateUpdateDialog() { + final l10n = AppLocalizations.of(context); + final fakeUpdate = UpdateInfo( update: true, version: '99.0.0', - notes: 'Bug fixes and performance improvements.', + notes: l10n.simulatedUpdateNotes, mandatory: false, sha256: 'abc123', download: 'https://example.com/fake-update.apk', @@ -223,16 +228,16 @@ class _DevMenuViewState extends State { showDialog( context: context, builder: (dialogContext) => AlertDialog( - title: const Text('Simulate update'), + title: Text(l10n.simulateUpdate), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text('Choose a simulation mode:'), + Text(l10n.chooseSimulationMode), const SizedBox(height: 16), _SimOption( icon: Icons.download_rounded, - label: 'Successful download', - description: 'Progress 0→100%, then install, then done', + label: l10n.successfulDownload, + description: l10n.progressThenInstall, onTap: () { Navigator.pop(dialogContext); context.push( @@ -244,8 +249,8 @@ class _DevMenuViewState extends State { const SizedBox(height: 8), _SimOption( icon: Icons.error_outline_rounded, - label: 'Download error', - description: 'Fails at 50% with a network timeout', + label: l10n.downloadError, + description: l10n.failsWithNetworkTimeout, onTap: () { Navigator.pop(dialogContext); context.push( @@ -257,8 +262,8 @@ class _DevMenuViewState extends State { const SizedBox(height: 8), _SimOption( icon: Icons.wifi_off_rounded, - label: 'Real download (will fail)', - description: 'Attempts real OTA with fake URL', + label: l10n.realDownloadWillFail, + description: l10n.attemptsFakeUrl, onTap: () { Navigator.pop(dialogContext); context.push('/update-progress', extra: fakeUpdate); @@ -269,7 +274,7 @@ class _DevMenuViewState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), - child: const Text('Cancel'), + child: Text(l10n.cancel), ), ], ), @@ -312,7 +317,9 @@ class _SimOption extends StatelessWidget { Text( description, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], @@ -415,8 +422,8 @@ class _Tile extends StatelessWidget { Text( subtitle!, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: scheme.onSurface.withValues(alpha: 0.5), - ), + color: scheme.onSurface.withValues(alpha: 0.5), + ), ), ], ), diff --git a/lib/views/dialogs/close_tab_dialog.dart b/lib/views/dialogs/close_tab_dialog.dart index e761f89..dab4a9d 100644 --- a/lib/views/dialogs/close_tab_dialog.dart +++ b/lib/views/dialogs/close_tab_dialog.dart @@ -6,9 +6,11 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import '../../models/payment_method.dart'; import '../../viewmodels/bar_screen_view_model.dart'; import '../widgets/slide_confirm.dart'; +import '../../l10n/app_localizations.dart'; Future confirmCloseTab(BuildContext context) async { final viewModel = context.read(); + final l10n = AppLocalizations.of(context); final tab = viewModel.selectedTab; if (tab == null) return; @@ -21,16 +23,16 @@ Future confirmCloseTab(BuildContext context) async { return StatefulBuilder( builder: (context, setDialogState) { return AlertDialog( - title: Text('Close ${tab.customerName}ʼs tab?'), + title: Text(l10n.closeTabForCustomer(tab.customerName)), content: SizedBox( width: 360, child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text('Current total: ${tab.formattedTotal}'), + Text(l10n.currentTotal(tab.formattedTotal)), const SizedBox(height: 20), Text( - 'Payment method', + l10n.paymentMethod, style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 8), @@ -53,7 +55,7 @@ Future confirmCloseTab(BuildContext context) async { actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), - child: const Text('Cancel'), + child: Text(l10n.cancel), ), ], ); @@ -69,9 +71,9 @@ Future confirmCloseTab(BuildContext context) async { } catch (e, stack) { Sentry.captureException(e, stackTrace: stack); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Could not close tab: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.couldNotCloseTab))); } } @@ -79,22 +81,19 @@ class _PaymentPicker extends StatelessWidget { final PaymentMethod selected; final ValueChanged onChanged; - const _PaymentPicker({ - required this.selected, - required this.onChanged, - }); + const _PaymentPicker({required this.selected, required this.onChanged}); - static const _options = [ - (PaymentMethod.cash, 'Cash'), - (PaymentMethod.payconiq, 'Payconiq'), - ]; + static const _options = [PaymentMethod.cash, PaymentMethod.payconiq]; @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + return Row( mainAxisAlignment: MainAxisAlignment.center, children: _options.map((option) { - final (value, label) = option; + final value = option; + final label = value == PaymentMethod.cash ? l10n.cash : l10n.payconiq; final isSelected = selected == value; return Padding( @@ -117,7 +116,10 @@ class _PaymentPicker extends StatelessWidget { 'assets/icons/payconic.svg', width: 16, height: 16, - colorFilter: ColorFilter.mode(Colors.pinkAccent, BlendMode.srcIn), + colorFilter: ColorFilter.mode( + Colors.pinkAccent, + BlendMode.srcIn, + ), ), const SizedBox(width: 6), Text(label), @@ -128,4 +130,4 @@ class _PaymentPicker extends StatelessWidget { }).toList(), ); } -} \ No newline at end of file +} diff --git a/lib/views/dialogs/new_tab_dialog.dart b/lib/views/dialogs/new_tab_dialog.dart index 27c2720..86d8482 100644 --- a/lib/views/dialogs/new_tab_dialog.dart +++ b/lib/views/dialogs/new_tab_dialog.dart @@ -2,19 +2,21 @@ import 'package:flutter/material.dart'; import 'package:kooltab2/viewmodels/bar_screen_view_model.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import '../../l10n/app_localizations.dart'; Future showNewTabDialog(BuildContext context) async { + final l10n = AppLocalizations.of(context); final controller = TextEditingController(); final name = await showDialog( context: context, builder: (dialogContext) { return AlertDialog( - title: const Text('Open new tab'), + title: Text(l10n.openNewTab), content: TextField( controller: controller, autofocus: true, - decoration: const InputDecoration(labelText: 'Customer / group name'), + decoration: InputDecoration(labelText: l10n.customerGroupName), onSubmitted: (value) { Navigator.of(dialogContext).pop(value); }, @@ -23,13 +25,13 @@ Future showNewTabDialog(BuildContext context) async { actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), - child: const Text('Cancel'), + child: Text(l10n.cancel), ), FilledButton( onPressed: () { Navigator.of(dialogContext).pop(controller.text); }, - child: const Text('Open tab'), + child: Text(l10n.openTab), ), ], ); @@ -45,8 +47,8 @@ Future showNewTabDialog(BuildContext context) async { } catch (e, stack) { Sentry.captureException(e, stackTrace: stack); if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Could not create tab: $e')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.couldNotCreateTabMessage))); } } diff --git a/lib/views/error_screen_view.dart b/lib/views/error_screen_view.dart index 80fca91..13b4287 100644 --- a/lib/views/error_screen_view.dart +++ b/lib/views/error_screen_view.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import '../l10n/app_localizations.dart'; + class ErrorScreenView extends StatelessWidget { const ErrorScreenView({super.key}); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); return Scaffold( appBar: AppBar( @@ -14,7 +17,7 @@ class ErrorScreenView extends StatelessWidget { onPressed: () => context.pop(), icon: const Icon(Icons.arrow_back), ), - title: const Text('Error Screen'), + title: Text(l10n.errorScreen), centerTitle: true, ), body: Center( @@ -37,24 +40,24 @@ class ErrorScreenView extends StatelessWidget { ), const SizedBox(height: 24), Text( - 'Something went wrong', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: scheme.error, - ), + l10n.somethingWentWrong, + style: Theme.of( + context, + ).textTheme.headlineMedium?.copyWith(color: scheme.error), ), const SizedBox(height: 12), Text( - 'An unexpected error occurred.\nPlease try restarting the app.', + l10n.unexpectedError, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: scheme.onSurface.withValues(alpha: 0.6), - ), + color: scheme.onSurface.withValues(alpha: 0.6), + ), ), const SizedBox(height: 32), FilledButton.icon( onPressed: () => context.go('/bar'), icon: const Icon(Icons.home_rounded), - label: const Text('Go to bar screen'), + label: Text(l10n.goToBarScreen), ), ], ), @@ -62,4 +65,4 @@ class ErrorScreenView extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/views/history_screen_view.dart b/lib/views/history_screen_view.dart index 43aa411..a323a8f 100644 --- a/lib/views/history_screen_view.dart +++ b/lib/views/history_screen_view.dart @@ -5,6 +5,8 @@ import 'package:provider/provider.dart'; import '../app/router.dart'; import '../viewmodels/history_view_model.dart'; +import '../l10n/app_localizations.dart'; +import '../l10n/app_localizations_helpers.dart'; class HistoryScreenView extends StatefulWidget { const HistoryScreenView({super.key}); @@ -55,6 +57,7 @@ class _HistoryScreenViewState extends State with RouteAware { @override Widget build(BuildContext context) { final viewModel = context.watch(); + final l10n = AppLocalizations.of(context); return Scaffold( appBar: AppBar( @@ -65,13 +68,13 @@ class _HistoryScreenViewState extends State with RouteAware { icon: const Icon(Icons.arrow_back), ), const SizedBox(width: 5), - const Text('Tab History'), + Text(l10n.tabHistory), ], ), actionsPadding: const EdgeInsets.symmetric(horizontal: 8), actions: [ IconButton( - tooltip: 'Refresh', + tooltip: l10n.refresh, onPressed: viewModel.load, icon: const Icon(Icons.refresh_rounded), ), @@ -100,7 +103,7 @@ class _HistoryScreenViewState extends State with RouteAware { ), const SizedBox(height: 12), Text( - viewModel.errorMessage!, + l10n.localizedError(viewModel.errorMessage), textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), @@ -119,8 +122,8 @@ class _HistoryScreenViewState extends State with RouteAware { Expanded( child: TextField( onChanged: viewModel.search, - decoration: const InputDecoration( - hintText: 'Search by name…', + decoration: InputDecoration( + hintText: l10n.searchByName, prefixIcon: Icon(Icons.search_rounded, size: 20), isDense: true, ), @@ -141,12 +144,12 @@ class _HistoryScreenViewState extends State with RouteAware { : ListView.separated( controller: _scrollController, padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), - itemCount: viewModel.closedTabs.length + + itemCount: + viewModel.closedTabs.length + (viewModel.hasMore || viewModel.isLoadingMore ? 1 : 0), - separatorBuilder: (_, _) => - const SizedBox(height: 10), + separatorBuilder: (_, _) => const SizedBox(height: 10), itemBuilder: (context, index) { if (index == viewModel.closedTabs.length) { return const Padding( @@ -189,6 +192,7 @@ class _CustomerDropdown extends StatelessWidget { @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); final isFiltered = selectedCustomer != null; return PopupMenuButton( @@ -204,17 +208,13 @@ class _CustomerDropdown extends StatelessWidget { child: Row( children: [ Icon( - isFiltered - ? Icons.people_outline - : Icons.people_rounded, + isFiltered ? Icons.people_outline : Icons.people_rounded, size: 18, - color: isFiltered - ? null - : scheme.primary, + color: isFiltered ? null : scheme.primary, ), const SizedBox(width: 10), Text( - 'All customers', + l10n.allCustomers, style: TextStyle( fontWeight: isFiltered ? FontWeight.w400 : FontWeight.w700, color: isFiltered ? null : scheme.primary, @@ -223,8 +223,7 @@ class _CustomerDropdown extends StatelessWidget { ], ), ), - if (customerNames.isNotEmpty) - const PopupMenuDivider(height: 1), + if (customerNames.isNotEmpty) const PopupMenuDivider(height: 1), ...customerNames.map( (name) => PopupMenuItem( value: name, @@ -237,9 +236,7 @@ class _CustomerDropdown extends StatelessWidget { size: 18, ), const SizedBox(width: 10), - Expanded( - child: Text(name, overflow: TextOverflow.ellipsis), - ), + Expanded(child: Text(name, overflow: TextOverflow.ellipsis)), ], ), ), @@ -250,9 +247,7 @@ class _CustomerDropdown extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), border: Border.all(color: scheme.onSurface.withValues(alpha: 0.12)), - color: isFiltered - ? scheme.primary.withValues(alpha: 0.08) - : null, + color: isFiltered ? scheme.primary.withValues(alpha: 0.08) : null, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -267,7 +262,7 @@ class _CustomerDropdown extends StatelessWidget { const SizedBox(width: 6), Flexible( child: Text( - selectedCustomer ?? 'Customer', + selectedCustomer ?? l10n.customer, overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.w600, @@ -297,6 +292,7 @@ class _EmptyState extends StatelessWidget { @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); return Center( child: Column( @@ -316,16 +312,16 @@ class _EmptyState extends StatelessWidget { ), const SizedBox(height: 16), Text( - 'No closed tabs yet', + l10n.noClosedTabs, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text( - 'Tabs you close will show up here.', + l10n.tabsYouCloseAppearHere, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ); } -} \ No newline at end of file +} diff --git a/lib/views/pin_lock_view.dart b/lib/views/pin_lock_view.dart index f14b956..5dc2150 100644 --- a/lib/views/pin_lock_view.dart +++ b/lib/views/pin_lock_view.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../viewmodels/pin_lock_view_model.dart'; +import '../l10n/app_localizations.dart'; +import '../l10n/app_localizations_helpers.dart'; enum PinEntryMode { unlock, create } @@ -30,16 +32,14 @@ class _PinEntryViewState extends State { bool get _isCreateFlow => widget.mode == PinEntryMode.create; - String get _title { - if (!_isCreateFlow) return 'Enter PIN'; - return _isConfirmStep ? 'Confirm PIN' : 'Create a PIN'; + String _title(AppLocalizations l10n) { + if (!_isCreateFlow) return l10n.enterPin; + return _isConfirmStep ? l10n.confirmPin : l10n.createPin; } - String? get _subtitle { + String? _subtitle(AppLocalizations l10n) { if (!_isCreateFlow) return null; - return _isConfirmStep - ? 'Enter the same PIN again' - : 'Youʼll use this to unlock the app'; + return _isConfirmStep ? l10n.confirmPinSubtitle : l10n.enterPinSubtitle; } void _onDigitPressed(String digit) { @@ -65,6 +65,7 @@ class _PinEntryViewState extends State { Future _handleComplete() async { final viewModel = context.read(); + final l10n = AppLocalizations.of(context); if (_isCreateFlow && !_isConfirmStep) { // First entry of a new PIN — stash it, then ask for confirmation. @@ -84,7 +85,7 @@ class _PinEntryViewState extends State { _firstEntry = null; _isConfirmStep = false; }); - _fail('PINs didnʼt match. Try again.'); + _fail(l10n.pinsDidNotMatch); return; } @@ -100,7 +101,7 @@ class _PinEntryViewState extends State { _firstEntry = null; _isConfirmStep = false; }); - _fail(viewModel.errorMessage ?? 'Something went wrong.'); + _fail(l10n.localizedError(viewModel.errorMessage)); } return; } @@ -114,7 +115,7 @@ class _PinEntryViewState extends State { if (ok) { widget.onSuccess?.call(); } else { - _fail(viewModel.errorMessage ?? 'Incorrect PIN.'); + _fail(l10n.localizedError(viewModel.errorMessage)); } } @@ -133,6 +134,7 @@ class _PinEntryViewState extends State { @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); return Scaffold( body: SafeArea( @@ -143,11 +145,14 @@ class _PinEntryViewState extends State { const Spacer(flex: 2), Icon(Icons.lock_outline_rounded, size: 36, color: scheme.primary), const SizedBox(height: 16), - Text(_title, style: Theme.of(context).textTheme.headlineMedium), - if (_subtitle != null) ...[ + Text( + _title(l10n), + style: Theme.of(context).textTheme.headlineMedium, + ), + if (_subtitle(l10n) != null) ...[ const SizedBox(height: 6), Text( - _subtitle!, + _subtitle(l10n)!, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium, ), diff --git a/lib/views/product_form_view.dart b/lib/views/product_form_view.dart index 29fe887..3a82d09 100644 --- a/lib/views/product_form_view.dart +++ b/lib/views/product_form_view.dart @@ -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 { } Future _loadProductIfNeeded() async { + final l10n = AppLocalizations.of(context); if (!widget.isEditing) { setState(() => _isLoading = false); return; @@ -62,7 +65,7 @@ class _ProductFormViewState extends State { 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 { 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 { Future _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 { } Future _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 { 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 { Future _delete() async { if (!widget.isEditing) return; + final l10n = AppLocalizations.of(context); final confirmed = await showDialog( 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 { } 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 { } Widget _buildImagePicker(BuildContext context) { + final l10n = AppLocalizations.of(context); return InkWell( onTap: _pickImage, @@ -289,7 +293,7 @@ class _ProductFormViewState extends State { 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 { 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 { @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 { 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 { 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( value: category, - label: category, + label: l10n.categoryLabel(category), ), ) .toList(), @@ -393,8 +398,8 @@ class _ProductFormViewState extends State { 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 { 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 { 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 { 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 { 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 { 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 { ) : const Icon(Icons.save), label: Text( - widget.isEditing ? 'Save changes' : 'Add product', + widget.isEditing ? l10n.confirm : l10n.addProduct, ), ), ], diff --git a/lib/views/product_list_view.dart b/lib/views/product_list_view.dart index a14044c..29e7dad 100644 --- a/lib/views/product_list_view.dart +++ b/lib/views/product_list_view.dart @@ -5,6 +5,8 @@ import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import '../viewmodels/product_list_view_model.dart'; +import '../l10n/app_localizations.dart'; +import '../l10n/app_localizations_helpers.dart'; class ProductListView extends StatefulWidget { const ProductListView({super.key}); @@ -26,6 +28,7 @@ class _ProductListViewState extends State { @override Widget build(BuildContext context) { final viewModel = context.watch(); + final l10n = AppLocalizations.of(context); return Scaffold( appBar: AppBar( @@ -36,7 +39,7 @@ class _ProductListViewState extends State { icon: const Icon(Icons.arrow_back), ), const SizedBox(width: 8), - const Text('Products'), + Text(l10n.products), ], ), ), @@ -44,7 +47,7 @@ class _ProductListViewState extends State { floatingActionButton: FloatingActionButton.extended( onPressed: () => context.go('/products/new'), icon: const Icon(Icons.add), - label: const Text('Add product'), + label: Text(l10n.addProduct), ), body: Builder( builder: (context) { @@ -53,11 +56,13 @@ class _ProductListViewState extends State { } if (viewModel.errorMessage != null) { - return Center(child: Text(viewModel.errorMessage!)); + return Center( + child: Text(l10n.localizedError(viewModel.errorMessage)), + ); } if (viewModel.products.isEmpty) { - return const Center(child: Text('No products yet.')); + return Center(child: Text(l10n.noProductsYet)); } return ListView.separated( @@ -73,22 +78,28 @@ class _ProductListViewState extends State { width: 56, height: 56, child: Center( - child: product.imagePath != null && + child: + product.imagePath != null && product.imagePath!.isNotEmpty ? Image.file( File(product.imagePath!), fit: BoxFit.contain, cacheWidth: 112, - errorBuilder: - (context, error, stackTrace) => - const Icon(Icons.image_not_supported_outlined), + errorBuilder: (context, error, stackTrace) => + const Icon( + Icons.image_not_supported_outlined, + ), ) : const Icon(Icons.image_not_supported_outlined), ), ), title: Text(product.name), subtitle: Text( - '${product.category} • ${product.formattedPrice} • Stock: ${product.stockQuantity}', + l10n.productStockSummary( + l10n.categoryLabel(product.category), + product.formattedPrice, + product.stockQuantity, + ), ), trailing: const Icon(Icons.chevron_right), onTap: () => context.go('/products/${product.id}/edit'), diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 8b90a94..262bf86 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -10,6 +10,8 @@ import '../models/settings.dart'; import '../utils/app_update_util.dart'; import '../viewmodels/pin_lock_view_model.dart'; import '../viewmodels/settings_view_model.dart'; +import '../l10n/app_localizations.dart'; +import '../l10n/app_localizations_helpers.dart'; class SettingsScreenView extends StatefulWidget { const SettingsScreenView({super.key}); @@ -58,8 +60,9 @@ class _SettingsScreenViewState extends State { }); } - Future _promptPin(String title, {String hint = 'Enter PIN (4 digits)'}) { + Future _promptPin(String title, {String? hint}) { final controller = TextEditingController(); + final l10n = AppLocalizations.of(context); return showDialog( context: context, @@ -81,12 +84,12 @@ class _SettingsScreenViewState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text(l10n.cancel), ), const SizedBox(width: 12), FilledButton( onPressed: () => Navigator.pop(context, controller.text), - child: const Text('Confirm'), + child: Text(l10n.confirm), ), ], ), @@ -101,11 +104,12 @@ class _SettingsScreenViewState extends State { } Future _enablePin() async { - final pin = await _promptPin('Set PIN Code', hint: '4 digits'); + final l10n = AppLocalizations.of(context); + final pin = await _promptPin(l10n.setPinCode, hint: l10n.fourDigits); if (pin == null) return; if (pin.length != 4) { - _showError('PIN must be exactly 4 digits'); + _showError(l10n.pinExactlyFour); return; } @@ -113,7 +117,7 @@ class _SettingsScreenViewState extends State { final success = await pinLockViewModel.setPin(pin); if (!success) { - _showError(pinLockViewModel.errorMessage ?? 'Could not set PIN.'); + _showError(l10n.localizedError(pinLockViewModel.errorMessage)); return; } @@ -123,19 +127,20 @@ class _SettingsScreenViewState extends State { await context.read().updatePinRequired(true); } catch (e, stack) { Sentry.captureException(e, stackTrace: stack); - _showError('Could not save PIN setting.'); + _showError(l10n.couldNotSavePin); } } Future _changePin() async { - final current = await _promptPin('Enter Current PIN'); + final l10n = AppLocalizations.of(context); + final current = await _promptPin(l10n.enterCurrentPin); if (current == null) return; - final newPin = await _promptPin('Enter New PIN', hint: '4 digits'); + final newPin = await _promptPin(l10n.enterNewPin, hint: l10n.fourDigits); if (newPin == null) return; if (newPin.length != 4) { - _showError('PIN must be exactly 4 digits'); + _showError(l10n.pinExactlyFour); return; } @@ -146,19 +151,20 @@ class _SettingsScreenViewState extends State { ); if (!success) { - _showError(pinLockViewModel.errorMessage ?? 'Could not change PIN.'); + _showError(l10n.localizedError(pinLockViewModel.errorMessage)); } } Future _disablePin() async { - final current = await _promptPin('Enter Current PIN to Disable'); + final l10n = AppLocalizations.of(context); + final current = await _promptPin(l10n.enterCurrentPin); if (current == null) return; final pinLockViewModel = context.read(); final success = await pinLockViewModel.disablePin(current); if (!success) { - _showError(pinLockViewModel.errorMessage ?? 'Could not disable PIN.'); + _showError(l10n.localizedError(pinLockViewModel.errorMessage)); return; } @@ -168,12 +174,13 @@ class _SettingsScreenViewState extends State { await context.read().updatePinRequired(false); } catch (e, stack) { Sentry.captureException(e, stackTrace: stack); - _showError('Could not save PIN setting.'); + _showError(l10n.couldNotSavePin); } } @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final settingsViewModel = context.watch(); final pinLockViewModel = context.watch(); @@ -186,7 +193,7 @@ class _SettingsScreenViewState extends State { icon: const Icon(Icons.arrow_back), ), const SizedBox(width: 5), - const Text('Settings'), + Text(l10n.settingsTitle), ], ), ), @@ -209,11 +216,11 @@ class _SettingsScreenViewState extends State { padding: const EdgeInsets.symmetric(vertical: 12), children: [ _SettingsSection( - title: 'Security', + title: l10n.security, children: [ _SettingsSwitchTile( icon: Icons.lock_outline_rounded, - title: 'PIN Required', + title: l10n.pinRequired, value: settings.pinRequired, onChanged: (value) { if (value) { @@ -226,22 +233,58 @@ class _SettingsScreenViewState extends State { if (settings.pinRequired) _SettingsTile( icon: Icons.pin_rounded, - title: 'Change PIN', + title: l10n.changePin, onTap: _changePin, ), ], ), _SettingsSection( - title: 'Appearance', + title: l10n.appearance, children: [ + _SettingsTile( + icon: Icons.language_rounded, + title: l10n.language, + subtitle: switch (settings.language) { + AppLanguage.system => l10n.languageSystem, + AppLanguage.english => l10n.languageEnglish, + AppLanguage.dutch => l10n.languageDutch, + }, + onTap: () async { + final selected = await showModalBottomSheet( + context: context, + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: AppLanguage.values.map((language) { + return RadioListTile( + value: language, + groupValue: settings.language, + title: Text(switch (language) { + AppLanguage.system => l10n.languageSystem, + AppLanguage.english => l10n.languageEnglish, + AppLanguage.dutch => l10n.languageDutch, + }), + onChanged: (value) => + Navigator.pop(context, value), + ); + }).toList(), + ), + ), + ); + + if (selected != null) { + await settingsViewModel.updateLanguage(selected); + } + }, + ), _SettingsTile( icon: Icons.brightness_6_rounded, - title: 'Theme', + title: l10n.theme, subtitle: switch (settings.themeMode) { - AppThemeMode.system => 'System', - AppThemeMode.light => 'Light', - AppThemeMode.dark => 'Dark', - AppThemeMode.ugly => 'Ugly', + AppThemeMode.system => l10n.system, + AppThemeMode.light => l10n.light, + AppThemeMode.dark => l10n.dark, + AppThemeMode.ugly => l10n.ugly, }, onTap: () async { final selected = await showModalBottomSheet( @@ -254,10 +297,10 @@ class _SettingsScreenViewState extends State { value: mode, groupValue: settings.themeMode, title: Text(switch (mode) { - AppThemeMode.system => 'System', - AppThemeMode.light => 'Light', - AppThemeMode.dark => 'Dark', - AppThemeMode.ugly => 'Ugly', + AppThemeMode.system => l10n.system, + AppThemeMode.light => l10n.light, + AppThemeMode.dark => l10n.dark, + AppThemeMode.ugly => l10n.ugly, }), onChanged: (value) => Navigator.pop(context, value), @@ -276,13 +319,13 @@ class _SettingsScreenViewState extends State { ), if (Platform.isAndroid) _SettingsSection( - title: 'Updates', + title: l10n.updates, children: [ _SettingsTile( icon: Icons.system_update_alt_rounded, title: settingsViewModel.checkingForUpdates - ? 'Checking for updates...' - : 'Check for updates', + ? l10n.checkingForUpdates + : l10n.checkForUpdates, onTap: settingsViewModel.checkingForUpdates ? null : _checkForUpdates, @@ -294,7 +337,7 @@ class _SettingsScreenViewState extends State { onTap: _onVersionTap, child: Center( child: Text( - 'Version $_appVersion', + l10n.version(_appVersion), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of( context, @@ -313,6 +356,7 @@ class _SettingsScreenViewState extends State { Future _checkForUpdates() async { if (!Platform.isAndroid) return; + final l10n = AppLocalizations.of(context); final vm = context.read(); try { @@ -321,11 +365,9 @@ class _SettingsScreenViewState extends State { if (!mounted) return; if (update == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('You are already on the latest version.'), - ), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(l10n.latestVersion))); return; } @@ -336,21 +378,23 @@ class _SettingsScreenViewState extends State { ScaffoldMessenger.of( context, - ).showSnackBar(SnackBar(content: Text('Update check failed: $e'))); + ).showSnackBar(SnackBar(content: Text(l10n.updateCheckFailed))); } } void _showUpdateDialog(UpdateInfo update) { + final l10n = AppLocalizations.of(context); + showDialog( context: context, barrierDismissible: !update.mandatory, builder: (dialogContext) => AlertDialog( - title: const Text("Update available"), + title: Text(l10n.updateAvailable), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Version ${update.version} is available."), + Text(l10n.versionAvailable(update.version)), const SizedBox(height: 12), Text(update.notes), ], @@ -359,14 +403,14 @@ class _SettingsScreenViewState extends State { if (!update.mandatory) TextButton( onPressed: () => Navigator.pop(dialogContext), - child: const Text("Later"), + child: Text(l10n.later), ), FilledButton( onPressed: () { Navigator.pop(dialogContext); context.push('/update-progress', extra: update); }, - child: const Text("Update"), + child: Text(l10n.update), ), ], ), diff --git a/lib/views/update_progress_view.dart b/lib/views/update_progress_view.dart index 155cbe3..ce2c552 100644 --- a/lib/views/update_progress_view.dart +++ b/lib/views/update_progress_view.dart @@ -3,6 +3,8 @@ import 'package:go_router/go_router.dart'; import '../utils/app_update_util.dart'; import '../viewmodels/update_progress_view_model.dart'; +import '../l10n/app_localizations.dart'; +import '../l10n/app_localizations_helpers.dart'; class UpdateProgressView extends StatefulWidget { final UpdateInfo update; @@ -46,28 +48,27 @@ class _UpdateProgressViewState extends State { } void _showRestartDialog() { + final l10n = AppLocalizations.of(context); + showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( - title: const Text('Update ready'), - content: const Text( - 'The update has been downloaded and installed. ' - 'Restart the app now to apply the changes.', - ), + title: Text(l10n.updateReady), + content: Text(l10n.updateReadyDescription), actions: [ TextButton( onPressed: () { Navigator.pop(context); this.context.go('/bar'); }, - child: const Text('Later'), + child: Text(l10n.later), ), FilledButton( onPressed: () { Navigator.pop(context); }, - child: const Text('Restart app'), + child: Text(l10n.restartApp), ), ], ), @@ -75,12 +76,14 @@ class _UpdateProgressViewState extends State { } void _showErrorSnackBar() { + final l10n = AppLocalizations.of(context); + ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(_vm.errorMessage ?? 'Update failed'), + content: Text(l10n.updateError(_vm.errorMessage)), backgroundColor: Theme.of(context).colorScheme.error, action: SnackBarAction( - label: 'Retry', + label: l10n.retry, onPressed: () { _vm.cancel(); _vm.start(); @@ -99,13 +102,15 @@ class _UpdateProgressViewState extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.close), onPressed: () => _showCancelDialog(), ), - title: const Text('Updating'), + title: Text(l10n.updating), centerTitle: true, automaticallyImplyLeading: false, ), @@ -138,6 +143,8 @@ class _UpdateProgressViewState extends State { } Widget _buildHeader() { + final l10n = AppLocalizations.of(context); + return Column( children: [ Container( @@ -153,13 +160,10 @@ class _UpdateProgressViewState extends State { ), ), const SizedBox(height: 24), - Text( - 'KoolTab', - style: Theme.of(context).textTheme.headlineMedium, - ), + Text('KoolTab', style: Theme.of(context).textTheme.headlineMedium), const SizedBox(height: 8), Text( - 'Version ${widget.update.version}', + l10n.version(widget.update.version), style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.primary, ), @@ -170,6 +174,7 @@ class _UpdateProgressViewState extends State { Widget _buildProgress(UpdateProgressViewModel vm) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); return Column( children: [ @@ -211,7 +216,7 @@ class _UpdateProgressViewState extends State { ), const SizedBox(height: 4), Text( - vm.phase == OtaPhase.installing ? 'Installing' : '', + vm.phase == OtaPhase.installing ? l10n.installing : '', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: scheme.onSurface.withValues(alpha: 0.6), ), @@ -234,6 +239,7 @@ class _UpdateProgressViewState extends State { Widget _buildStatus(UpdateProgressViewModel vm) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); final color = switch (vm.phase) { OtaPhase.error => scheme.error, @@ -242,13 +248,15 @@ class _UpdateProgressViewState extends State { }; return Text( - vm.statusText, + l10n.updateStatus(vm.statusText), style: Theme.of(context).textTheme.titleMedium?.copyWith(color: color), textAlign: TextAlign.center, ); } Widget _buildVersionInfo(UpdateProgressViewModel vm) { + final l10n = AppLocalizations.of(context); + return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -259,9 +267,11 @@ class _UpdateProgressViewState extends State { ), const SizedBox(width: 6), Text( - 'Do not close the app during the update', + l10n.doNotCloseDuringUpdate, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.4), ), ), ], @@ -269,18 +279,17 @@ class _UpdateProgressViewState extends State { } void _showCancelDialog() { + final l10n = AppLocalizations.of(context); + showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Cancel update?'), - content: const Text( - 'The update is in progress. If you leave now, ' - 'the app may become unstable.', - ), + title: Text(l10n.cancelUpdate), + content: Text(l10n.cancelUpdateDescription), actions: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Continue update'), + child: Text(l10n.continueUpdate), ), FilledButton( style: FilledButton.styleFrom( @@ -291,7 +300,7 @@ class _UpdateProgressViewState extends State { _vm.cancel(); this.context.go('/bar'); }, - child: const Text('Cancel update'), + child: Text(l10n.cancelUpdateAction), ), ], ), diff --git a/lib/views/widgets/closed_tab_card.dart b/lib/views/widgets/closed_tab_card.dart index 961c3ae..9c56999 100644 --- a/lib/views/widgets/closed_tab_card.dart +++ b/lib/views/widgets/closed_tab_card.dart @@ -4,6 +4,8 @@ import 'package:kooltab2/models/closed_tab.dart'; import 'package:kooltab2/models/closed_tab_item.dart'; import 'package:kooltab2/models/payment_method.dart'; +import '../../l10n/app_localizations.dart'; + class ClosedTabCard extends StatefulWidget { final ClosedTab closedTab; @@ -26,7 +28,9 @@ class _ClosedTabCardState extends State { @override Widget build(BuildContext context) { final closedTab = widget.closedTab; - final dateFormat = DateFormat('MMM d, y · h:mm a'); + final l10n = AppLocalizations.of(context); + final locale = Localizations.localeOf(context).toLanguageTag(); + final dateFormat = DateFormat.yMMMd(locale).add_jm(); final scheme = Theme.of(context).colorScheme; return Container( @@ -91,7 +95,10 @@ class _ClosedTabCardState extends State { ), const SizedBox(width: 4), Text( - closedTab.formattedPaymentMethod, + switch (closedTab.paymentMethod) { + PaymentMethod.cash => l10n.cash, + PaymentMethod.payconiq => l10n.payconiq, + }, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, @@ -105,7 +112,7 @@ class _ClosedTabCardState extends State { ), const SizedBox(height: 2), Text( - '${dateFormat.format(closedTab.closedAt)} · ${closedTab.itemCount} items', + '${dateFormat.format(closedTab.closedAt)} · ${l10n.tabItemCount(closedTab.itemCount)}', style: Theme.of(context).textTheme.bodySmall, ), ], @@ -164,12 +171,13 @@ class _ClosedTabItemRow extends StatelessWidget { @override Widget build(BuildContext context) { - final unitPrice = NumberFormat.simpleCurrency().format( - item.unitPriceInCents / 100, - ); - final lineTotal = NumberFormat.simpleCurrency().format( - item.lineTotalInCents / 100, - ); + final locale = Localizations.localeOf(context).toLanguageTag(); + final unitPrice = NumberFormat.simpleCurrency( + locale: locale, + ).format(item.unitPriceInCents / 100); + final lineTotal = NumberFormat.simpleCurrency( + locale: locale, + ).format(item.lineTotalInCents / 100); final scheme = Theme.of(context).colorScheme; return Padding( @@ -207,4 +215,4 @@ class _ClosedTabItemRow extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/views/widgets/product_tile.dart b/lib/views/widgets/product_tile.dart index 77c03c2..123b2a6 100644 --- a/lib/views/widgets/product_tile.dart +++ b/lib/views/widgets/product_tile.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:kooltab2/models/product.dart'; +import '../../l10n/app_localizations.dart'; + class ProductTile extends StatelessWidget { final Product product; final bool enabled; @@ -20,6 +22,7 @@ class ProductTile extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; + final l10n = AppLocalizations.of(context); final outOfStock = product.stockQuantity <= 0; final lowStock = @@ -101,7 +104,7 @@ class ProductTile extends StatelessWidget { borderRadius: BorderRadius.circular(999), ), child: Text( - 'OUT OF STOCK', + l10n.outOfStock, style: TextStyle( color: scheme.onError, fontWeight: FontWeight.w800, diff --git a/lib/views/widgets/slide_confirm.dart b/lib/views/widgets/slide_confirm.dart index 081b1f3..47c3582 100644 --- a/lib/views/widgets/slide_confirm.dart +++ b/lib/views/widgets/slide_confirm.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import '../../l10n/app_localizations.dart'; + class SlideConfirm extends StatefulWidget { final VoidCallback onConfirmed; @@ -18,6 +20,7 @@ class _SlideConfirmState extends State { @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); return SizedBox( width: double.infinity, @@ -37,7 +40,7 @@ class _SlideConfirmState extends State { children: [ Center( child: Text( - _confirmed ? 'Closing tab...' : 'Slide to confirm closing', + _confirmed ? l10n.closingTab : l10n.slideToConfirmClosing, style: TextStyle( color: scheme.error, fontWeight: FontWeight.w700, diff --git a/pubspec.lock b/pubspec.lock index 3eecc91..da0753e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -318,6 +318,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -532,10 +537,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.20.3" + version: "0.20.2" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 19e095a..728caf9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -43,7 +45,7 @@ dependencies: image_picker: ^1.2.3 path: ^1.9.1 flutter_slidable: ^4.0.3 - intl: ^0.20.3 + intl: ^0.20.2 flutter_secure_storage: 10.3.1 crypto: ^3.0.0 http: ^1.6.0 @@ -75,6 +77,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in