feat: add localisation
This commit is contained in:
@@ -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
@@ -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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,14 +2354,20 @@ 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(
|
||||
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(
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "KoolTab",
|
||||
"barTabs": "Bar Tabs",
|
||||
"products": "Products",
|
||||
"addProduct": "Add product",
|
||||
"noProductsYet": "No products yet.",
|
||||
"addFirstProduct": "Add your first product to start selling.",
|
||||
"manageProducts": "Manage products",
|
||||
"tabHistory": "Tab history",
|
||||
"refresh": "Refresh",
|
||||
"settings": "Settings",
|
||||
"logout": "Logout",
|
||||
"openOrSelectTab": "Open or select a tab first.",
|
||||
"openNewTab": "Open new tab",
|
||||
"customerGroupName": "Customer / group name",
|
||||
"cancel": "Cancel",
|
||||
"openTab": "Open tab",
|
||||
"couldNotCreateTab": "Could not create tab: {error}",
|
||||
"@couldNotCreateTab": {"placeholders": {"error": {"type": "String"}}},
|
||||
"searchByName": "Search by name…",
|
||||
"allCustomers": "All customers",
|
||||
"customer": "Customer",
|
||||
"noClosedTabs": "No closed tabs yet",
|
||||
"settingsTitle": "Settings",
|
||||
"security": "Security",
|
||||
"pinRequired": "PIN Required",
|
||||
"changePin": "Change PIN",
|
||||
"appearance": "Appearance",
|
||||
"language": "Language",
|
||||
"languageSystem": "System default",
|
||||
"languageEnglish": "English",
|
||||
"languageDutch": "Dutch",
|
||||
"theme": "Theme",
|
||||
"system": "System",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"ugly": "Ugly",
|
||||
"setPinCode": "Set PIN Code",
|
||||
"enterPin": "Enter PIN",
|
||||
"enterCurrentPin": "Enter Current PIN",
|
||||
"enterNewPin": "Enter New PIN",
|
||||
"fourDigits": "4 digits",
|
||||
"enterPinFourDigits": "Enter PIN (4 digits)",
|
||||
"confirm": "Confirm",
|
||||
"confirmPin": "Confirm PIN",
|
||||
"pinExactlyFour": "PIN must be exactly 4 digits",
|
||||
"couldNotSavePin": "Could not save PIN setting.",
|
||||
"editProduct": "Edit product",
|
||||
"productName": "Product name",
|
||||
"category": "Category",
|
||||
"price": "Price",
|
||||
"stock": "Stock",
|
||||
"lowStockThreshold": "Low stock threshold",
|
||||
"chooseProductImage": "Choose product image",
|
||||
"changeImage": "Change image",
|
||||
"deleteProduct": "Delete product?",
|
||||
"deleteProductDescription": "This will remove the product from the product list.",
|
||||
"delete": "Delete",
|
||||
"productNotFound": "Product not found.",
|
||||
"chooseImage": "Choose a product image.",
|
||||
"selectCategory": "Please select a category.",
|
||||
"enterProductName": "Enter a product name.",
|
||||
"version": "Version {version}",
|
||||
"@version": {"placeholders": {"version": {"type": "String"}}},
|
||||
"selectTabToAddItems": "Select a tab to add items",
|
||||
"openTabs": "Open tabs",
|
||||
"selectOrOpenTab": "Select or open a tab",
|
||||
"noOpenTabs": "No open tabs",
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"tabItemSummary": "{count, plural, =1 {1 item - {total}} other {{count} items - {total}}}",
|
||||
"@tabItemSummary": {"placeholders": {"count": {"type": "int"}, "total": {"type": "String"}}},
|
||||
"tabItemCount": "{count, plural, =1 {1 item} other {{count} items}}",
|
||||
"@tabItemCount": {"placeholders": {"count": {"type": "int"}}},
|
||||
"tapProductsToAdd": "Tap products to add them",
|
||||
"total": "Total",
|
||||
"closeTab": "Close tab",
|
||||
"productStockSummary": "{category} • {price} • Stock: {stock}",
|
||||
"@productStockSummary": {"placeholders": {"category": {"type": "String"}, "price": {"type": "String"}, "stock": {"type": "int"}}},
|
||||
"outOfStock": "OUT OF STOCK",
|
||||
"tabsYouCloseAppearHere": "Tabs you close will show up here.",
|
||||
"enterPinSubtitle": "You’ll use this to unlock the app",
|
||||
"confirmPinSubtitle": "Enter the same PIN again",
|
||||
"createPin": "Create a PIN",
|
||||
"pinsDidNotMatch": "PINs didn’t match. Try again.",
|
||||
"somethingWentWrong": "Something went wrong.",
|
||||
"incorrectPin": "Incorrect PIN.",
|
||||
"enterPrice": "Enter a price.",
|
||||
"enterValidPrice": "Enter a valid price.",
|
||||
"enterValidStock": "Enter a valid stock amount.",
|
||||
"enterValidThreshold": "Enter a valid threshold.",
|
||||
"couldNotLoadProduct": "Could not load product.",
|
||||
"couldNotSaveProduct": "Could not save product.",
|
||||
"couldNotDeleteProduct": "Could not delete product.",
|
||||
"paymentMethod": "Payment method",
|
||||
"cash": "Cash",
|
||||
"payconiq": "Payconiq",
|
||||
"closeTabForCustomer": "Close tab for {customerName}?",
|
||||
"@closeTabForCustomer": {"placeholders": {"customerName": {"type": "String"}}},
|
||||
"currentTotal": "Current total: {total}",
|
||||
"@currentTotal": {"placeholders": {"total": {"type": "String"}}},
|
||||
"couldNotCloseTab": "Could not close tab.",
|
||||
"closingTab": "Closing tab…",
|
||||
"slideToConfirmClosing": "Slide to confirm closing",
|
||||
"errorScreen": "Error Screen",
|
||||
"unexpectedError": "An unexpected error occurred.\nPlease try restarting the app.",
|
||||
"goToBarScreen": "Go to bar screen",
|
||||
"updates": "Updates",
|
||||
"checkingForUpdates": "Checking for updates…",
|
||||
"checkForUpdates": "Check for updates",
|
||||
"latestVersion": "You are already on the latest version.",
|
||||
"updateCheckFailed": "Update check failed.",
|
||||
"updateAvailable": "Update available",
|
||||
"versionAvailable": "Version {version} is available.",
|
||||
"@versionAvailable": {"placeholders": {"version": {"type": "String"}}},
|
||||
"later": "Later",
|
||||
"update": "Update",
|
||||
"updateReady": "Update ready",
|
||||
"updateReadyDescription": "The update has been downloaded and installed. Restart the app now to apply the changes.",
|
||||
"restartApp": "Restart app",
|
||||
"updateFailed": "Update failed",
|
||||
"retry": "Retry",
|
||||
"updating": "Updating",
|
||||
"installing": "Installing",
|
||||
"doNotCloseDuringUpdate": "Do not close the app during the update",
|
||||
"cancelUpdate": "Cancel update?",
|
||||
"cancelUpdateDescription": "The update is in progress. If you leave now, the app may become unstable.",
|
||||
"continueUpdate": "Continue update",
|
||||
"cancelUpdateAction": "Cancel update",
|
||||
"preparingDownload": "Preparing download…",
|
||||
"simulatedDownloadError": "Simulated download error (network timeout)",
|
||||
"downloadFailed": "Download failed",
|
||||
"downloading": "Downloading {percent}%",
|
||||
"@downloading": {"placeholders": {"percent": {"type": "String"}}},
|
||||
"installingUpdate": "Installing update…",
|
||||
"updateInstalledRestarting": "Update installed! Restarting…",
|
||||
"installationFailed": "Installation failed",
|
||||
"updateAlreadyInProgress": "Update already in progress",
|
||||
"updateAlreadyRunning": "Update already running",
|
||||
"genericError": "An unexpected error occurred.",
|
||||
"couldNotLoadBar": "Could not load bar screen.",
|
||||
"couldNotCreateTabMessage": "Could not create tab.",
|
||||
"productOutOfStock": "Product is out of stock.",
|
||||
"couldNotAddProductToTab": "Could not add product to tab.",
|
||||
"couldNotUpdateQuantity": "Could not update item quantity.",
|
||||
"couldNotReloadTabs": "Could not reload tabs.",
|
||||
"couldNotLoadHistory": "Could not load tab history.",
|
||||
"couldNotLoadMoreTabs": "Could not load more tabs.",
|
||||
"couldNotLoadProducts": "Could not load products.",
|
||||
"couldNotAddProduct": "Could not add product.",
|
||||
"couldNotUpdateProduct": "Could not update product.",
|
||||
"couldNotDeleteProductMessage": "Could not delete product.",
|
||||
"couldNotCheckPinStatus": "Could not check PIN status.",
|
||||
"couldNotVerifyPin": "Could not verify PIN.",
|
||||
"currentPinIncorrect": "Current PIN is incorrect.",
|
||||
"couldNotLoadSettings": "Could not load settings.",
|
||||
"couldNotSaveSettings": "Could not save settings.",
|
||||
"downloadFailedWithDetail": "Download failed.",
|
||||
"installationFailedWithDetail": "Installation failed.",
|
||||
"devMenu": "Dev Menu",
|
||||
"dataManagement": "Data Management",
|
||||
"clearOpenTabs": "Clear all open tabs",
|
||||
"clearClosedHistory": "Clear closed tab history",
|
||||
"seedDefaultProducts": "Seed default products",
|
||||
"onlySeedsWhenEmpty": "Only seeds if table is empty",
|
||||
"clearAndReseedProducts": "Clear & reseed products",
|
||||
"wipesAndReseeds": "Wipes all products, then seeds defaults",
|
||||
"clearAllProducts": "Clear all products",
|
||||
"clearProductImages": "Clear product images",
|
||||
"deletesProductImages": "Deletes images from product_images folder",
|
||||
"debugging": "Debugging",
|
||||
"resetPin": "Reset PIN",
|
||||
"removePinLock": "Remove PIN lock",
|
||||
"toggleDebugOverlay": "Toggle debug overlay",
|
||||
"showFpsMemoryWidgets": "Show FPS, memory, widget count",
|
||||
"errorScreenMenu": "Error screen",
|
||||
"viewErrorScreen": "View the error screen UI",
|
||||
"featureToggles": "Feature Toggles",
|
||||
"simulateUpdateDownload": "Simulate update download",
|
||||
"openDownloadProgress": "Open the download progress screen",
|
||||
"testingHelpers": "Testing Helpers",
|
||||
"createTestTab": "Create test tab",
|
||||
"addRandomItems": "Add tab with random items",
|
||||
"addTestProducts": "Add 100 test products",
|
||||
"stressTestGrid": "Stress test product grid",
|
||||
"simulateLowStock": "Simulate low stock",
|
||||
"setProductsBelowThreshold": "Set all products below threshold",
|
||||
"generateMockOrders": "Generate 100 mock orders",
|
||||
"randomCustomersItemsAmounts": "Random customers, items, and amounts",
|
||||
"performance": "Performance",
|
||||
"clearImageCache": "Clear image cache",
|
||||
"reloadProductImages": "Reload product images",
|
||||
"debugOverlay": "Debug Overlay",
|
||||
"debugOverlayDescription": "The debug overlay shows FPS, memory usage, and widget counts. Enable it via Flutter DevTools in debug mode.",
|
||||
"ok": "OK",
|
||||
"simulateUpdate": "Simulate update",
|
||||
"chooseSimulationMode": "Choose a simulation mode:",
|
||||
"successfulDownload": "Successful download",
|
||||
"progressThenInstall": "Progress 0→100%, then install, then done",
|
||||
"downloadError": "Download error",
|
||||
"failsWithNetworkTimeout": "Fails at 50% with a network timeout",
|
||||
"realDownloadWillFail": "Real download (will fail)",
|
||||
"attemptsFakeUrl": "Attempts real OTA with fake URL",
|
||||
"categoryBeer": "Beer",
|
||||
"categoryWine": "Wine",
|
||||
"categorySoftDrinks": "Soft drinks",
|
||||
"categoryCocktails": "Cocktails",
|
||||
"categorySnacks": "Snacks",
|
||||
"categoryCoffee": "Coffee",
|
||||
"categoryTea": "Tea",
|
||||
"categoryOther": "Other",
|
||||
"devActionClearedOpenTabs": "Cleared all open tabs",
|
||||
"devActionClearedHistory": "Cleared closed tab history",
|
||||
"devActionClearedProducts": "Cleared all products",
|
||||
"devActionSeededProducts": "Seeded {count} products",
|
||||
"@devActionSeededProducts": {"placeholders": {"count": {"type": "int"}}},
|
||||
"devActionReseededProducts": "Cleared and reseeded {count} products",
|
||||
"@devActionReseededProducts": {"placeholders": {"count": {"type": "int"}}},
|
||||
"devActionImagesCleared": "Product images cleared",
|
||||
"devActionResetPin": "PIN reset (no PIN required)",
|
||||
"devActionNoProducts": "No products available",
|
||||
"devActionImagesReloaded": "Product images reloaded",
|
||||
"devActionCacheCleared": "Image cache cleared (no-op in debug mode)",
|
||||
"devActionLowStock": "Simulated low stock for {count} products",
|
||||
"@devActionLowStock": {"placeholders": {"count": {"type": "int"}}},
|
||||
"devActionTestTab": "Created test tab with {count} items",
|
||||
"@devActionTestTab": {"placeholders": {"count": {"type": "int"}}},
|
||||
"devActionTestProducts": "Added {count} test products",
|
||||
"@devActionTestProducts": {"placeholders": {"count": {"type": "int"}}},
|
||||
"devActionMockOrders": "Generated {orders} mock orders across {customers} customers",
|
||||
"@devActionMockOrders": {"placeholders": {"orders": {"type": "int"}, "customers": {"type": "int"}}},
|
||||
"devActionFailedImages": "Failed to clear images.",
|
||||
"devActionResetStock": "Reset stock for {count} products",
|
||||
"@devActionResetStock": {"placeholders": {"count": {"type": "int"}}},
|
||||
"paymentCash": "Cash",
|
||||
"paymentPayconiq": "Payconiq",
|
||||
"simulatedUpdateNotes": "Bug fixes and performance improvements."
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,723 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'KoolTab';
|
||||
|
||||
@override
|
||||
String get barTabs => 'Bar Tabs';
|
||||
|
||||
@override
|
||||
String get products => 'Products';
|
||||
|
||||
@override
|
||||
String get addProduct => 'Add product';
|
||||
|
||||
@override
|
||||
String get noProductsYet => 'No products yet.';
|
||||
|
||||
@override
|
||||
String get addFirstProduct => 'Add your first product to start selling.';
|
||||
|
||||
@override
|
||||
String get manageProducts => 'Manage products';
|
||||
|
||||
@override
|
||||
String get tabHistory => 'Tab history';
|
||||
|
||||
@override
|
||||
String get refresh => 'Refresh';
|
||||
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
|
||||
@override
|
||||
String get logout => 'Logout';
|
||||
|
||||
@override
|
||||
String get openOrSelectTab => 'Open or select a tab first.';
|
||||
|
||||
@override
|
||||
String get openNewTab => 'Open new tab';
|
||||
|
||||
@override
|
||||
String get customerGroupName => 'Customer / group name';
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get openTab => 'Open tab';
|
||||
|
||||
@override
|
||||
String couldNotCreateTab(String error) {
|
||||
return 'Could not create tab: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get searchByName => 'Search by name…';
|
||||
|
||||
@override
|
||||
String get allCustomers => 'All customers';
|
||||
|
||||
@override
|
||||
String get customer => 'Customer';
|
||||
|
||||
@override
|
||||
String get noClosedTabs => 'No closed tabs yet';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Settings';
|
||||
|
||||
@override
|
||||
String get security => 'Security';
|
||||
|
||||
@override
|
||||
String get pinRequired => 'PIN Required';
|
||||
|
||||
@override
|
||||
String get changePin => 'Change PIN';
|
||||
|
||||
@override
|
||||
String get appearance => 'Appearance';
|
||||
|
||||
@override
|
||||
String get language => 'Language';
|
||||
|
||||
@override
|
||||
String get languageSystem => 'System default';
|
||||
|
||||
@override
|
||||
String get languageEnglish => 'English';
|
||||
|
||||
@override
|
||||
String get languageDutch => 'Dutch';
|
||||
|
||||
@override
|
||||
String get theme => 'Theme';
|
||||
|
||||
@override
|
||||
String get system => 'System';
|
||||
|
||||
@override
|
||||
String get light => 'Light';
|
||||
|
||||
@override
|
||||
String get dark => 'Dark';
|
||||
|
||||
@override
|
||||
String get ugly => 'Ugly';
|
||||
|
||||
@override
|
||||
String get setPinCode => 'Set PIN Code';
|
||||
|
||||
@override
|
||||
String get enterPin => 'Enter PIN';
|
||||
|
||||
@override
|
||||
String get enterCurrentPin => 'Enter Current PIN';
|
||||
|
||||
@override
|
||||
String get enterNewPin => 'Enter New PIN';
|
||||
|
||||
@override
|
||||
String get fourDigits => '4 digits';
|
||||
|
||||
@override
|
||||
String get enterPinFourDigits => 'Enter PIN (4 digits)';
|
||||
|
||||
@override
|
||||
String get confirm => 'Confirm';
|
||||
|
||||
@override
|
||||
String get confirmPin => 'Confirm PIN';
|
||||
|
||||
@override
|
||||
String get pinExactlyFour => 'PIN must be exactly 4 digits';
|
||||
|
||||
@override
|
||||
String get couldNotSavePin => 'Could not save PIN setting.';
|
||||
|
||||
@override
|
||||
String get editProduct => 'Edit product';
|
||||
|
||||
@override
|
||||
String get productName => 'Product name';
|
||||
|
||||
@override
|
||||
String get category => 'Category';
|
||||
|
||||
@override
|
||||
String get price => 'Price';
|
||||
|
||||
@override
|
||||
String get stock => 'Stock';
|
||||
|
||||
@override
|
||||
String get lowStockThreshold => 'Low stock threshold';
|
||||
|
||||
@override
|
||||
String get chooseProductImage => 'Choose product image';
|
||||
|
||||
@override
|
||||
String get changeImage => 'Change image';
|
||||
|
||||
@override
|
||||
String get deleteProduct => 'Delete product?';
|
||||
|
||||
@override
|
||||
String get deleteProductDescription =>
|
||||
'This will remove the product from the product list.';
|
||||
|
||||
@override
|
||||
String get delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get productNotFound => 'Product not found.';
|
||||
|
||||
@override
|
||||
String get chooseImage => 'Choose a product image.';
|
||||
|
||||
@override
|
||||
String get selectCategory => 'Please select a category.';
|
||||
|
||||
@override
|
||||
String get enterProductName => 'Enter a product name.';
|
||||
|
||||
@override
|
||||
String version(String version) {
|
||||
return 'Version $version';
|
||||
}
|
||||
|
||||
@override
|
||||
String get selectTabToAddItems => 'Select a tab to add items';
|
||||
|
||||
@override
|
||||
String get openTabs => 'Open tabs';
|
||||
|
||||
@override
|
||||
String get selectOrOpenTab => 'Select or open a tab';
|
||||
|
||||
@override
|
||||
String get noOpenTabs => 'No open tabs';
|
||||
|
||||
@override
|
||||
String get edit => 'Edit';
|
||||
|
||||
@override
|
||||
String get close => 'Close';
|
||||
|
||||
@override
|
||||
String tabItemSummary(int count, String total) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count items - $total',
|
||||
one: '1 item - $total',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String tabItemCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count items',
|
||||
one: '1 item',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get tapProductsToAdd => 'Tap products to add them';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
||||
@override
|
||||
String get closeTab => 'Close tab';
|
||||
|
||||
@override
|
||||
String productStockSummary(String category, String price, int stock) {
|
||||
return '$category • $price • Stock: $stock';
|
||||
}
|
||||
|
||||
@override
|
||||
String get outOfStock => 'OUT OF STOCK';
|
||||
|
||||
@override
|
||||
String get tabsYouCloseAppearHere => 'Tabs you close will show up here.';
|
||||
|
||||
@override
|
||||
String get enterPinSubtitle => 'You’ll use this to unlock the app';
|
||||
|
||||
@override
|
||||
String get confirmPinSubtitle => 'Enter the same PIN again';
|
||||
|
||||
@override
|
||||
String get createPin => 'Create a PIN';
|
||||
|
||||
@override
|
||||
String get pinsDidNotMatch => 'PINs didn’t match. Try again.';
|
||||
|
||||
@override
|
||||
String get somethingWentWrong => 'Something went wrong.';
|
||||
|
||||
@override
|
||||
String get incorrectPin => 'Incorrect PIN.';
|
||||
|
||||
@override
|
||||
String get enterPrice => 'Enter a price.';
|
||||
|
||||
@override
|
||||
String get enterValidPrice => 'Enter a valid price.';
|
||||
|
||||
@override
|
||||
String get enterValidStock => 'Enter a valid stock amount.';
|
||||
|
||||
@override
|
||||
String get enterValidThreshold => 'Enter a valid threshold.';
|
||||
|
||||
@override
|
||||
String get couldNotLoadProduct => 'Could not load product.';
|
||||
|
||||
@override
|
||||
String get couldNotSaveProduct => 'Could not save product.';
|
||||
|
||||
@override
|
||||
String get couldNotDeleteProduct => 'Could not delete product.';
|
||||
|
||||
@override
|
||||
String get paymentMethod => 'Payment method';
|
||||
|
||||
@override
|
||||
String get cash => 'Cash';
|
||||
|
||||
@override
|
||||
String get payconiq => 'Payconiq';
|
||||
|
||||
@override
|
||||
String closeTabForCustomer(String customerName) {
|
||||
return 'Close tab for $customerName?';
|
||||
}
|
||||
|
||||
@override
|
||||
String currentTotal(String total) {
|
||||
return 'Current total: $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String get couldNotCloseTab => 'Could not close tab.';
|
||||
|
||||
@override
|
||||
String get closingTab => 'Closing tab…';
|
||||
|
||||
@override
|
||||
String get slideToConfirmClosing => 'Slide to confirm closing';
|
||||
|
||||
@override
|
||||
String get errorScreen => 'Error Screen';
|
||||
|
||||
@override
|
||||
String get unexpectedError =>
|
||||
'An unexpected error occurred.\nPlease try restarting the app.';
|
||||
|
||||
@override
|
||||
String get goToBarScreen => 'Go to bar screen';
|
||||
|
||||
@override
|
||||
String get updates => 'Updates';
|
||||
|
||||
@override
|
||||
String get checkingForUpdates => 'Checking for updates…';
|
||||
|
||||
@override
|
||||
String get checkForUpdates => 'Check for updates';
|
||||
|
||||
@override
|
||||
String get latestVersion => 'You are already on the latest version.';
|
||||
|
||||
@override
|
||||
String get updateCheckFailed => 'Update check failed.';
|
||||
|
||||
@override
|
||||
String get updateAvailable => 'Update available';
|
||||
|
||||
@override
|
||||
String versionAvailable(String version) {
|
||||
return 'Version $version is available.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get later => 'Later';
|
||||
|
||||
@override
|
||||
String get update => 'Update';
|
||||
|
||||
@override
|
||||
String get updateReady => 'Update ready';
|
||||
|
||||
@override
|
||||
String get updateReadyDescription =>
|
||||
'The update has been downloaded and installed. Restart the app now to apply the changes.';
|
||||
|
||||
@override
|
||||
String get restartApp => 'Restart app';
|
||||
|
||||
@override
|
||||
String get updateFailed => 'Update failed';
|
||||
|
||||
@override
|
||||
String get retry => 'Retry';
|
||||
|
||||
@override
|
||||
String get updating => 'Updating';
|
||||
|
||||
@override
|
||||
String get installing => 'Installing';
|
||||
|
||||
@override
|
||||
String get doNotCloseDuringUpdate => 'Do not close the app during the update';
|
||||
|
||||
@override
|
||||
String get cancelUpdate => 'Cancel update?';
|
||||
|
||||
@override
|
||||
String get cancelUpdateDescription =>
|
||||
'The update is in progress. If you leave now, the app may become unstable.';
|
||||
|
||||
@override
|
||||
String get continueUpdate => 'Continue update';
|
||||
|
||||
@override
|
||||
String get cancelUpdateAction => 'Cancel update';
|
||||
|
||||
@override
|
||||
String get preparingDownload => 'Preparing download…';
|
||||
|
||||
@override
|
||||
String get simulatedDownloadError =>
|
||||
'Simulated download error (network timeout)';
|
||||
|
||||
@override
|
||||
String get downloadFailed => 'Download failed';
|
||||
|
||||
@override
|
||||
String downloading(String percent) {
|
||||
return 'Downloading $percent%';
|
||||
}
|
||||
|
||||
@override
|
||||
String get installingUpdate => 'Installing update…';
|
||||
|
||||
@override
|
||||
String get updateInstalledRestarting => 'Update installed! Restarting…';
|
||||
|
||||
@override
|
||||
String get installationFailed => 'Installation failed';
|
||||
|
||||
@override
|
||||
String get updateAlreadyInProgress => 'Update already in progress';
|
||||
|
||||
@override
|
||||
String get updateAlreadyRunning => 'Update already running';
|
||||
|
||||
@override
|
||||
String get genericError => 'An unexpected error occurred.';
|
||||
|
||||
@override
|
||||
String get couldNotLoadBar => 'Could not load bar screen.';
|
||||
|
||||
@override
|
||||
String get couldNotCreateTabMessage => 'Could not create tab.';
|
||||
|
||||
@override
|
||||
String get productOutOfStock => 'Product is out of stock.';
|
||||
|
||||
@override
|
||||
String get couldNotAddProductToTab => 'Could not add product to tab.';
|
||||
|
||||
@override
|
||||
String get couldNotUpdateQuantity => 'Could not update item quantity.';
|
||||
|
||||
@override
|
||||
String get couldNotReloadTabs => 'Could not reload tabs.';
|
||||
|
||||
@override
|
||||
String get couldNotLoadHistory => 'Could not load tab history.';
|
||||
|
||||
@override
|
||||
String get couldNotLoadMoreTabs => 'Could not load more tabs.';
|
||||
|
||||
@override
|
||||
String get couldNotLoadProducts => 'Could not load products.';
|
||||
|
||||
@override
|
||||
String get couldNotAddProduct => 'Could not add product.';
|
||||
|
||||
@override
|
||||
String get couldNotUpdateProduct => 'Could not update product.';
|
||||
|
||||
@override
|
||||
String get couldNotDeleteProductMessage => 'Could not delete product.';
|
||||
|
||||
@override
|
||||
String get couldNotCheckPinStatus => 'Could not check PIN status.';
|
||||
|
||||
@override
|
||||
String get couldNotVerifyPin => 'Could not verify PIN.';
|
||||
|
||||
@override
|
||||
String get currentPinIncorrect => 'Current PIN is incorrect.';
|
||||
|
||||
@override
|
||||
String get couldNotLoadSettings => 'Could not load settings.';
|
||||
|
||||
@override
|
||||
String get couldNotSaveSettings => 'Could not save settings.';
|
||||
|
||||
@override
|
||||
String get downloadFailedWithDetail => 'Download failed.';
|
||||
|
||||
@override
|
||||
String get installationFailedWithDetail => 'Installation failed.';
|
||||
|
||||
@override
|
||||
String get devMenu => 'Dev Menu';
|
||||
|
||||
@override
|
||||
String get dataManagement => 'Data Management';
|
||||
|
||||
@override
|
||||
String get clearOpenTabs => 'Clear all open tabs';
|
||||
|
||||
@override
|
||||
String get clearClosedHistory => 'Clear closed tab history';
|
||||
|
||||
@override
|
||||
String get seedDefaultProducts => 'Seed default products';
|
||||
|
||||
@override
|
||||
String get onlySeedsWhenEmpty => 'Only seeds if table is empty';
|
||||
|
||||
@override
|
||||
String get clearAndReseedProducts => 'Clear & reseed products';
|
||||
|
||||
@override
|
||||
String get wipesAndReseeds => 'Wipes all products, then seeds defaults';
|
||||
|
||||
@override
|
||||
String get clearAllProducts => 'Clear all products';
|
||||
|
||||
@override
|
||||
String get clearProductImages => 'Clear product images';
|
||||
|
||||
@override
|
||||
String get deletesProductImages =>
|
||||
'Deletes images from product_images folder';
|
||||
|
||||
@override
|
||||
String get debugging => 'Debugging';
|
||||
|
||||
@override
|
||||
String get resetPin => 'Reset PIN';
|
||||
|
||||
@override
|
||||
String get removePinLock => 'Remove PIN lock';
|
||||
|
||||
@override
|
||||
String get toggleDebugOverlay => 'Toggle debug overlay';
|
||||
|
||||
@override
|
||||
String get showFpsMemoryWidgets => 'Show FPS, memory, widget count';
|
||||
|
||||
@override
|
||||
String get errorScreenMenu => 'Error screen';
|
||||
|
||||
@override
|
||||
String get viewErrorScreen => 'View the error screen UI';
|
||||
|
||||
@override
|
||||
String get featureToggles => 'Feature Toggles';
|
||||
|
||||
@override
|
||||
String get simulateUpdateDownload => 'Simulate update download';
|
||||
|
||||
@override
|
||||
String get openDownloadProgress => 'Open the download progress screen';
|
||||
|
||||
@override
|
||||
String get testingHelpers => 'Testing Helpers';
|
||||
|
||||
@override
|
||||
String get createTestTab => 'Create test tab';
|
||||
|
||||
@override
|
||||
String get addRandomItems => 'Add tab with random items';
|
||||
|
||||
@override
|
||||
String get addTestProducts => 'Add 100 test products';
|
||||
|
||||
@override
|
||||
String get stressTestGrid => 'Stress test product grid';
|
||||
|
||||
@override
|
||||
String get simulateLowStock => 'Simulate low stock';
|
||||
|
||||
@override
|
||||
String get setProductsBelowThreshold => 'Set all products below threshold';
|
||||
|
||||
@override
|
||||
String get generateMockOrders => 'Generate 100 mock orders';
|
||||
|
||||
@override
|
||||
String get randomCustomersItemsAmounts =>
|
||||
'Random customers, items, and amounts';
|
||||
|
||||
@override
|
||||
String get performance => 'Performance';
|
||||
|
||||
@override
|
||||
String get clearImageCache => 'Clear image cache';
|
||||
|
||||
@override
|
||||
String get reloadProductImages => 'Reload product images';
|
||||
|
||||
@override
|
||||
String get debugOverlay => 'Debug Overlay';
|
||||
|
||||
@override
|
||||
String get debugOverlayDescription =>
|
||||
'The debug overlay shows FPS, memory usage, and widget counts. Enable it via Flutter DevTools in debug mode.';
|
||||
|
||||
@override
|
||||
String get ok => 'OK';
|
||||
|
||||
@override
|
||||
String get simulateUpdate => 'Simulate update';
|
||||
|
||||
@override
|
||||
String get chooseSimulationMode => 'Choose a simulation mode:';
|
||||
|
||||
@override
|
||||
String get successfulDownload => 'Successful download';
|
||||
|
||||
@override
|
||||
String get progressThenInstall => 'Progress 0→100%, then install, then done';
|
||||
|
||||
@override
|
||||
String get downloadError => 'Download error';
|
||||
|
||||
@override
|
||||
String get failsWithNetworkTimeout => 'Fails at 50% with a network timeout';
|
||||
|
||||
@override
|
||||
String get realDownloadWillFail => 'Real download (will fail)';
|
||||
|
||||
@override
|
||||
String get attemptsFakeUrl => 'Attempts real OTA with fake URL';
|
||||
|
||||
@override
|
||||
String get categoryBeer => 'Beer';
|
||||
|
||||
@override
|
||||
String get categoryWine => 'Wine';
|
||||
|
||||
@override
|
||||
String get categorySoftDrinks => 'Soft drinks';
|
||||
|
||||
@override
|
||||
String get categoryCocktails => 'Cocktails';
|
||||
|
||||
@override
|
||||
String get categorySnacks => 'Snacks';
|
||||
|
||||
@override
|
||||
String get categoryCoffee => 'Coffee';
|
||||
|
||||
@override
|
||||
String get categoryTea => 'Tea';
|
||||
|
||||
@override
|
||||
String get categoryOther => 'Other';
|
||||
|
||||
@override
|
||||
String get devActionClearedOpenTabs => 'Cleared all open tabs';
|
||||
|
||||
@override
|
||||
String get devActionClearedHistory => 'Cleared closed tab history';
|
||||
|
||||
@override
|
||||
String get devActionClearedProducts => 'Cleared all products';
|
||||
|
||||
@override
|
||||
String devActionSeededProducts(int count) {
|
||||
return 'Seeded $count products';
|
||||
}
|
||||
|
||||
@override
|
||||
String devActionReseededProducts(int count) {
|
||||
return 'Cleared and reseeded $count products';
|
||||
}
|
||||
|
||||
@override
|
||||
String get devActionImagesCleared => 'Product images cleared';
|
||||
|
||||
@override
|
||||
String get devActionResetPin => 'PIN reset (no PIN required)';
|
||||
|
||||
@override
|
||||
String get devActionNoProducts => 'No products available';
|
||||
|
||||
@override
|
||||
String get devActionImagesReloaded => 'Product images reloaded';
|
||||
|
||||
@override
|
||||
String get devActionCacheCleared =>
|
||||
'Image cache cleared (no-op in debug mode)';
|
||||
|
||||
@override
|
||||
String devActionLowStock(int count) {
|
||||
return 'Simulated low stock for $count products';
|
||||
}
|
||||
|
||||
@override
|
||||
String devActionTestTab(int count) {
|
||||
return 'Created test tab with $count items';
|
||||
}
|
||||
|
||||
@override
|
||||
String devActionTestProducts(int count) {
|
||||
return 'Added $count test products';
|
||||
}
|
||||
|
||||
@override
|
||||
String devActionMockOrders(int orders, int customers) {
|
||||
return 'Generated $orders mock orders across $customers customers';
|
||||
}
|
||||
|
||||
@override
|
||||
String get devActionFailedImages => 'Failed to clear images.';
|
||||
|
||||
@override
|
||||
String devActionResetStock(int count) {
|
||||
return 'Reset stock for $count products';
|
||||
}
|
||||
|
||||
@override
|
||||
String get paymentCash => 'Cash';
|
||||
|
||||
@override
|
||||
String get paymentPayconiq => 'Payconiq';
|
||||
|
||||
@override
|
||||
String get simulatedUpdateNotes => 'Bug fixes and performance improvements.';
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,14 +40,14 @@ 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),
|
||||
@@ -54,7 +57,7 @@ class ErrorScreenView extends StatelessWidget {
|
||||
FilledButton.icon(
|
||||
onPressed: () => context.go('/bar'),
|
||||
icon: const Icon(Icons.home_rounded),
|
||||
label: const Text('Go to bar screen'),
|
||||
label: Text(l10n.goToBarScreen),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user