feat: add localisation

This commit is contained in:
2026-07-31 21:46:05 +02:00
parent b58c9a8722
commit f5b2041612
30 changed files with 4099 additions and 326 deletions
+5
View File
@@ -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
+14 -4
View File
@@ -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<KoolTabApp> {
final settings = context.watch<SettingsViewModel>().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<KoolTabApp> {
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,
};
}
Locale? _localeFor(AppLanguage language) {
return switch (language) {
AppLanguage.system => null,
AppLanguage.english => const Locale('en'),
AppLanguage.dutch => const Locale('nl'),
};
}
}
+7 -1
View File
@@ -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<Column> 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);
}
},
);
}
+76 -10
View File
@@ -2219,8 +2219,20 @@ class $AppSettingsTableTable extends AppSettingsTable
requiredDuringInsert: false,
defaultValue: const Constant('system'),
);
static const VerificationMeta _languageMeta = const VerificationMeta(
'language',
);
@override
List<GeneratedColumn> get $columns => [id, pinRequired, themeMode];
late final GeneratedColumn<String> language = GeneratedColumn<String>(
'language',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant('system'),
);
@override
List<GeneratedColumn> 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<AppSettingsRow> {
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<String, Expression> toColumns(bool nullToAbsent) {
@@ -2298,6 +2322,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
map['id'] = Variable<String>(id);
map['pin_required'] = Variable<bool>(pinRequired);
map['theme_mode'] = Variable<String>(themeMode);
map['language'] = Variable<String>(language);
return map;
}
@@ -2306,6 +2331,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
id: Value(id),
pinRequired: Value(pinRequired),
themeMode: Value(themeMode),
language: Value(language),
);
}
@@ -2318,6 +2344,7 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
id: serializer.fromJson<String>(json['id']),
pinRequired: serializer.fromJson<bool>(json['pinRequired']),
themeMode: serializer.fromJson<String>(json['themeMode']),
language: serializer.fromJson<String>(json['language']),
);
}
@override
@@ -2327,15 +2354,21 @@ class AppSettingsRow extends DataClass implements Insertable<AppSettingsRow> {
'id': serializer.toJson<String>(id),
'pinRequired': serializer.toJson<bool>(pinRequired),
'themeMode': serializer.toJson<String>(themeMode),
'language': serializer.toJson<String>(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<AppSettingsRow> {
? 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<AppSettingsRow> {
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<AppSettingsRow> {
final Value<String> id;
final Value<bool> pinRequired;
final Value<String> themeMode;
final Value<String> language;
final Value<int> 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<AppSettingsRow> custom({
Expression<String>? id,
Expression<bool>? pinRequired,
Expression<String>? themeMode,
Expression<String>? language,
Expression<int>? 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<AppSettingsRow> {
Value<String>? id,
Value<bool>? pinRequired,
Value<String>? themeMode,
Value<String>? language,
Value<int>? 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<AppSettingsRow> {
if (themeMode.present) {
map['theme_mode'] = Variable<String>(themeMode.value);
}
if (language.present) {
map['language'] = Variable<String>(language.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
@@ -2436,6 +2482,7 @@ class AppSettingsTableCompanion extends UpdateCompanion<AppSettingsRow> {
..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<bool> pinRequired,
Value<String> themeMode,
Value<String> language,
Value<int> rowid,
});
typedef $$AppSettingsTableTableUpdateCompanionBuilder =
@@ -4030,6 +4078,7 @@ typedef $$AppSettingsTableTableUpdateCompanionBuilder =
Value<String> id,
Value<bool> pinRequired,
Value<String> themeMode,
Value<String> language,
Value<int> rowid,
});
@@ -4056,6 +4105,11 @@ class $$AppSettingsTableTableFilterComposer
column: $table.themeMode,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> 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<String> get language => $composableBuilder(
column: $table.language,
builder: (column) => ColumnOrderings(column),
);
}
class $$AppSettingsTableTableAnnotationComposer
@@ -4102,6 +4161,9 @@ class $$AppSettingsTableTableAnnotationComposer
GeneratedColumn<String> get themeMode =>
$composableBuilder(column: $table.themeMode, builder: (column) => column);
GeneratedColumn<String> get language =>
$composableBuilder(column: $table.language, builder: (column) => column);
}
class $$AppSettingsTableTableTableManager
@@ -4144,11 +4206,13 @@ class $$AppSettingsTableTableTableManager
Value<String> id = const Value.absent(),
Value<bool> pinRequired = const Value.absent(),
Value<String> themeMode = const Value.absent(),
Value<String> language = const Value.absent(),
Value<int> 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<bool> pinRequired = const Value.absent(),
Value<String> themeMode = const Value.absent(),
Value<String> language = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => AppSettingsTableCompanion.insert(
id: id,
pinRequired: pinRequired,
themeMode: themeMode,
language: language,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
+239
View File
@@ -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": "Youll use this to unlock the app",
"confirmPinSubtitle": "Enter the same PIN again",
"createPin": "Create a PIN",
"pinsDidNotMatch": "PINs didnt 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."
}
File diff suppressed because it is too large Load Diff
+723
View File
@@ -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 => 'Youll 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 didnt 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.';
}
+157
View File
@@ -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,
};
}
}
+734
View File
@@ -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.';
}
+239
View File
@@ -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."
}
+16 -4
View File
@@ -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);
}
+5
View File
@@ -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),
),
);
}
+7 -4
View File
@@ -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),
),
],
),
+3
View File
@@ -111,6 +111,9 @@ class SettingsViewModel extends ChangeNotifier {
Future<void> updateThemeMode(AppThemeMode mode) =>
_save(_settings.copyWith(themeMode: mode));
Future<void> updateLanguage(AppLanguage language) =>
_save(_settings.copyWith(language: language));
Future<void> _save(AppSettings updated) async {
final previous = _settings;
_settings = updated;
+48 -37
View File
@@ -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<BarScreenView> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final viewModel = context.watch<BarScreenViewModel>();
final productsViewModel = context.watch<ProductListViewModel>();
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<PinLockViewModel>().isPinSet) ...[
const SizedBox(width: 6),
IconButton(
tooltip: 'Logout',
tooltip: l10n.logout,
onPressed: () {
Provider.of<PinLockViewModel>(context, listen: false).lock();
},
@@ -99,7 +102,7 @@ class _BarScreenViewState extends State<BarScreenView> {
),
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<BarScreenView> {
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<BarScreenView> {
} 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)),
);
}
}
+62 -55
View File
@@ -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<DevMenuView> {
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<DevMenuView> {
@override
Widget build(BuildContext context) {
final vm = context.watch<DevMenuViewModel>();
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
@@ -57,109 +61,109 @@ class _DevMenuViewState extends State<DevMenuView> {
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<DevMenuView> {
],
),
_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<DevMenuView> {
}
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<DevMenuView> {
}
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<DevMenuView> {
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<DevMenuView> {
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<DevMenuView> {
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<DevMenuView> {
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),
),
),
],
),
+19 -17
View File
@@ -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<void> confirmCloseTab(BuildContext context) async {
final viewModel = context.read<BarScreenViewModel>();
final l10n = AppLocalizations.of(context);
final tab = viewModel.selectedTab;
if (tab == null) return;
@@ -21,16 +23,16 @@ Future<void> 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<void> 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<void> 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<PaymentMethod> 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),
+9 -7
View File
@@ -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<void> showNewTabDialog(BuildContext context) async {
final l10n = AppLocalizations.of(context);
final controller = TextEditingController();
final name = await showDialog<String>(
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<void> 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<void> 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)));
}
}
+12 -9
View File
@@ -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),
),
],
),
+22 -26
View File
@@ -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<HistoryScreenView> with RouteAware {
@override
Widget build(BuildContext context) {
final viewModel = context.watch<HistoryViewModel>();
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
@@ -65,13 +68,13 @@ class _HistoryScreenViewState extends State<HistoryScreenView> 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<HistoryScreenView> 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<HistoryScreenView> 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<HistoryScreenView> 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<String>(
@@ -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<String>(
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,12 +312,12 @@ 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,
),
],
+18 -13
View File
@@ -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<PinEntryView> {
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<PinEntryView> {
Future<void> _handleComplete() async {
final viewModel = context.read<PinLockViewModel>();
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<PinEntryView> {
_firstEntry = null;
_isConfirmStep = false;
});
_fail('PINs didnʼt match. Try again.');
_fail(l10n.pinsDidNotMatch);
return;
}
@@ -100,7 +101,7 @@ class _PinEntryViewState extends State<PinEntryView> {
_firstEntry = null;
_isConfirmStep = false;
});
_fail(viewModel.errorMessage ?? 'Something went wrong.');
_fail(l10n.localizedError(viewModel.errorMessage));
}
return;
}
@@ -114,7 +115,7 @@ class _PinEntryViewState extends State<PinEntryView> {
if (ok) {
widget.onSuccess?.call();
} else {
_fail(viewModel.errorMessage ?? 'Incorrect PIN.');
_fail(l10n.localizedError(viewModel.errorMessage));
}
}
@@ -133,6 +134,7 @@ class _PinEntryViewState extends State<PinEntryView> {
@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<PinEntryView> {
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,
),
+48 -43
View File
@@ -10,6 +10,8 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import '../models/product.dart';
import '../viewmodels/product_list_view_model.dart';
import '../l10n/app_localizations.dart';
import '../l10n/app_localizations_helpers.dart';
class ProductFormView extends StatefulWidget {
final String? productId;
@@ -48,6 +50,7 @@ class _ProductFormViewState extends State<ProductFormView> {
}
Future<void> _loadProductIfNeeded() async {
final l10n = AppLocalizations.of(context);
if (!widget.isEditing) {
setState(() => _isLoading = false);
return;
@@ -62,7 +65,7 @@ class _ProductFormViewState extends State<ProductFormView> {
if (product == null) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Product not found.')));
).showSnackBar(SnackBar(content: Text(l10n.productNotFound)));
context.go('/products');
return;
@@ -83,7 +86,7 @@ class _ProductFormViewState extends State<ProductFormView> {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Could not load product: $e')));
).showSnackBar(SnackBar(content: Text(l10n.couldNotLoadProduct)));
context.go('/products');
}
}
@@ -120,11 +123,11 @@ class _ProductFormViewState extends State<ProductFormView> {
Future<void> _pickImage() async {
final pickedFile = await _imagePicker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
maxWidth: 1000,
maxHeight: 1000,
);
source: ImageSource.gallery,
imageQuality: 80,
maxWidth: 1000,
maxHeight: 1000,
);
if (pickedFile == null) return;
@@ -146,19 +149,20 @@ class _ProductFormViewState extends State<ProductFormView> {
}
Future<void> _save() async {
final l10n = AppLocalizations.of(context);
if (!_formKey.currentState!.validate()) return;
if (_imagePath == null || _imagePath!.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Choose a product image.')));
).showSnackBar(SnackBar(content: Text(l10n.chooseImage)));
return;
}
if (_selectedCategory == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select a category.')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.selectCategory)));
return;
}
@@ -198,9 +202,9 @@ class _ProductFormViewState extends State<ProductFormView> {
Sentry.captureException(e, stackTrace: stack);
if (!mounted) return;
setState(() => _isSaving = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not save product: $e')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.couldNotSaveProduct)));
return;
}
@@ -212,23 +216,22 @@ class _ProductFormViewState extends State<ProductFormView> {
Future<void> _delete() async {
if (!widget.isEditing) return;
final l10n = AppLocalizations.of(context);
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Delete product?'),
content: const Text(
'This will remove the product from the product list.',
),
title: Text(l10n.deleteProduct),
content: Text(l10n.deleteProductDescription),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Cancel'),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Delete'),
child: Text(l10n.delete),
),
],
);
@@ -244,9 +247,9 @@ class _ProductFormViewState extends State<ProductFormView> {
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not delete product: $e')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.couldNotDeleteProduct)));
return;
}
@@ -256,6 +259,7 @@ class _ProductFormViewState extends State<ProductFormView> {
}
Widget _buildImagePicker(BuildContext context) {
final l10n = AppLocalizations.of(context);
return InkWell(
onTap: _pickImage,
@@ -289,7 +293,7 @@ class _ProductFormViewState extends State<ProductFormView> {
child: FilledButton.icon(
onPressed: _pickImage,
icon: const Icon(Icons.image),
label: const Text('Change image'),
label: Text(l10n.changeImage),
),
),
],
@@ -301,7 +305,7 @@ class _ProductFormViewState extends State<ProductFormView> {
const Icon(Icons.add_photo_alternate_outlined, size: 48),
const SizedBox(height: 12),
Text(
'Choose product image',
l10n.chooseProductImage,
style: Theme.of(context).textTheme.titleMedium,
),
],
@@ -313,7 +317,8 @@ class _ProductFormViewState extends State<ProductFormView> {
@override
Widget build(BuildContext context) {
final title = widget.isEditing ? 'Edit product' : 'Add product';
final l10n = AppLocalizations.of(context);
final title = widget.isEditing ? l10n.editProduct : l10n.addProduct;
return Scaffold(
appBar: AppBar(
@@ -345,14 +350,14 @@ class _ProductFormViewState extends State<ProductFormView> {
const SizedBox(height: 24),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Product name',
decoration: InputDecoration(
labelText: l10n.productName,
border: OutlineInputBorder(),
),
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Enter a product name.';
return l10n.enterProductName;
}
return null;
@@ -370,13 +375,13 @@ class _ProductFormViewState extends State<ProductFormView> {
enableSearch: true,
controller: _categoryController,
requestFocusOnTap: true,
label: const Text('Category'),
hintText: 'Select a category',
label: Text(l10n.category),
hintText: l10n.selectCategory,
dropdownMenuEntries: viewModel.categories
.map(
(category) => DropdownMenuEntry<String>(
value: category,
label: category,
label: l10n.categoryLabel(category),
),
)
.toList(),
@@ -393,8 +398,8 @@ class _ProductFormViewState extends State<ProductFormView> {
const SizedBox(height: 16),
TextFormField(
controller: _priceController,
decoration: const InputDecoration(
labelText: 'Price',
decoration: InputDecoration(
labelText: l10n.price,
prefixText: '',
border: OutlineInputBorder(),
),
@@ -404,14 +409,14 @@ class _ProductFormViewState extends State<ProductFormView> {
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Enter a price.';
return l10n.enterPrice;
}
final normalized = value.replaceAll(',', '.');
final price = double.tryParse(normalized);
if (price == null || price < 0) {
return 'Enter a valid price.';
return l10n.enterValidPrice;
}
return null;
@@ -420,8 +425,8 @@ class _ProductFormViewState extends State<ProductFormView> {
const SizedBox(height: 16),
TextFormField(
controller: _stockController,
decoration: const InputDecoration(
labelText: 'Current stock',
decoration: InputDecoration(
labelText: l10n.stock,
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
@@ -430,7 +435,7 @@ class _ProductFormViewState extends State<ProductFormView> {
final number = int.tryParse(value ?? '');
if (number == null || number < 0) {
return 'Enter a valid stock amount.';
return l10n.enterValidStock;
}
return null;
@@ -439,8 +444,8 @@ class _ProductFormViewState extends State<ProductFormView> {
const SizedBox(height: 16),
TextFormField(
controller: _lowStockController,
decoration: const InputDecoration(
labelText: 'Low stock warning threshold',
decoration: InputDecoration(
labelText: l10n.lowStockThreshold,
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
@@ -448,7 +453,7 @@ class _ProductFormViewState extends State<ProductFormView> {
final number = int.tryParse(value ?? '');
if (number == null || number < 0) {
return 'Enter a valid threshold.';
return l10n.enterValidThreshold;
}
return null;
@@ -467,7 +472,7 @@ class _ProductFormViewState extends State<ProductFormView> {
)
: const Icon(Icons.save),
label: Text(
widget.isEditing ? 'Save changes' : 'Add product',
widget.isEditing ? l10n.confirm : l10n.addProduct,
),
),
],
+20 -9
View File
@@ -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<ProductListView> {
@override
Widget build(BuildContext context) {
final viewModel = context.watch<ProductListViewModel>();
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
@@ -36,7 +39,7 @@ class _ProductListViewState extends State<ProductListView> {
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 8),
const Text('Products'),
Text(l10n.products),
],
),
),
@@ -44,7 +47,7 @@ class _ProductListViewState extends State<ProductListView> {
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<ProductListView> {
}
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<ProductListView> {
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'),
+86 -42
View File
@@ -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<SettingsScreenView> {
});
}
Future<String?> _promptPin(String title, {String hint = 'Enter PIN (4 digits)'}) {
Future<String?> _promptPin(String title, {String? hint}) {
final controller = TextEditingController();
final l10n = AppLocalizations.of(context);
return showDialog<String>(
context: context,
@@ -81,12 +84,12 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
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<SettingsScreenView> {
}
Future<void> _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<SettingsScreenView> {
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<SettingsScreenView> {
await context.read<SettingsViewModel>().updatePinRequired(true);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
_showError('Could not save PIN setting.');
_showError(l10n.couldNotSavePin);
}
}
Future<void> _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<SettingsScreenView> {
);
if (!success) {
_showError(pinLockViewModel.errorMessage ?? 'Could not change PIN.');
_showError(l10n.localizedError(pinLockViewModel.errorMessage));
}
}
Future<void> _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<PinLockViewModel>();
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<SettingsScreenView> {
await context.read<SettingsViewModel>().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<SettingsViewModel>();
final pinLockViewModel = context.watch<PinLockViewModel>();
@@ -186,7 +193,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 5),
const Text('Settings'),
Text(l10n.settingsTitle),
],
),
),
@@ -209,11 +216,11 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
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<SettingsScreenView> {
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<AppLanguage>(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: AppLanguage.values.map((language) {
return RadioListTile<AppLanguage>(
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<AppThemeMode>(
@@ -254,10 +297,10 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
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<SettingsScreenView> {
),
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<SettingsScreenView> {
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<SettingsScreenView> {
Future<void> _checkForUpdates() async {
if (!Platform.isAndroid) return;
final l10n = AppLocalizations.of(context);
final vm = context.read<SettingsViewModel>();
try {
@@ -321,11 +365,9 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
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<SettingsScreenView> {
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<SettingsScreenView> {
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),
),
],
),
+35 -26
View File
@@ -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<UpdateProgressView> {
}
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<UpdateProgressView> {
}
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<UpdateProgressView> {
@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<UpdateProgressView> {
}
Widget _buildHeader() {
final l10n = AppLocalizations.of(context);
return Column(
children: [
Container(
@@ -153,13 +160,10 @@ class _UpdateProgressViewState extends State<UpdateProgressView> {
),
),
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<UpdateProgressView> {
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<UpdateProgressView> {
),
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<UpdateProgressView> {
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<UpdateProgressView> {
};
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<UpdateProgressView> {
),
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<UpdateProgressView> {
}
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<UpdateProgressView> {
_vm.cancel();
this.context.go('/bar');
},
child: const Text('Cancel update'),
child: Text(l10n.cancelUpdateAction),
),
],
),
+17 -9
View File
@@ -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<ClosedTabCard> {
@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<ClosedTabCard> {
),
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<ClosedTabCard> {
),
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(
+4 -1
View File
@@ -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,
+4 -1
View File
@@ -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<SlideConfirm> {
@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<SlideConfirm> {
children: [
Center(
child: Text(
_confirmed ? 'Closing tab...' : 'Slide to confirm closing',
_confirmed ? l10n.closingTab : l10n.slideToConfirmClosing,
style: TextStyle(
color: scheme.error,
fontWeight: FontWeight.w700,
+7 -2
View File
@@ -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:
+4 -1
View File
@@ -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