From 03a1eea050d2629f50352873336e12ddb2b2dd27 Mon Sep 17 00:00:00 2001 From: Lu Shueh Chou Date: Sat, 18 May 2024 18:26:36 +0800 Subject: [PATCH] feat: using english as english (#205) --- .github/workflows/deploy-to-playstore.yaml | 2 +- .github/workflows/release-candidate.yaml | 4 +- .vscode/arb.code-snippets | 45 - .vscode/arb.schema.json | 256 +- .vscode/settings.json | 12 +- Makefile | 14 +- README.md | 18 +- assets/l10n/en/analysis.yaml | 142 ++ assets/l10n/en/cashier.yaml | 102 + assets/l10n/en/global.yaml | 119 + assets/l10n/en/menu.yaml | 138 ++ assets/l10n/en/order.yaml | 190 ++ assets/l10n/en/order_attribute.yaml | 108 + assets/l10n/en/setting.yaml | 69 + assets/l10n/en/stock.yaml | 121 + assets/l10n/en/transit.yaml | 523 ++++ assets/l10n/zh/analysis.yaml | 129 + assets/l10n/zh/cashier.yaml | 82 + assets/l10n/zh/global.yaml | 58 + assets/l10n/zh/menu.yaml | 103 + assets/l10n/zh/order.yaml | 147 ++ assets/l10n/zh/order_attribute.yaml | 82 + assets/l10n/zh/setting.yaml | 53 + assets/l10n/zh/stock.yaml | 98 + assets/l10n/zh/transit.yaml | 357 +++ docs/CODE_OF_CONDUCT.md | 22 +- docs/PRIVACY_POLICY.md | 14 +- docs/README.md | 18 +- docs/about/contribute.md | 82 +- docs/maintenance/bump-dependencies.md | 14 +- docs/maintenance/deployment.md | 28 +- docs/maintenance/development.md | 74 +- docs/untranslated.json | 1041 +------- l10n.yaml | 10 +- lib/components/dialog/delete_dialog.dart | 5 +- .../models/order_attribute_value_widget.dart | 41 +- lib/components/models/order_loader.dart | 8 +- .../scaffold/item_list_scaffold.dart | 52 - .../{mixin => scaffold}/item_modal.dart | 0 lib/components/search_bar_wrapper.dart | 1 + lib/components/sign_in_button.dart | 34 +- .../style/{more_button.dart => buttons.dart} | 5 +- lib/components/style/date_range_picker.dart | 45 + lib/components/style/empty_body.dart | 8 +- lib/components/style/image_holder.dart | 3 +- lib/components/style/percentile_bar.dart | 20 +- .../style/route_circular_button.dart | 62 +- lib/components/style/search_bar_inline.dart | 1 + lib/components/style/snackbar.dart | 9 +- ...kbar_action.dart => snackbar_actions.dart} | 2 +- lib/components/tutorial.dart | 11 +- lib/constants/constant.dart | 2 + lib/constants/icons.dart | 2 - lib/debug/debug_page.dart | 36 + lib/debug/random_gen_order.dart | 60 +- lib/debug/rerun_migration.dart | 22 +- lib/firebase_compatible_options.dart | 5 +- .../exporter/google_sheet_exporter.dart | 12 +- lib/helpers/formatter/formatter.dart | 23 +- .../formatter/google_sheet_formatter.dart | 20 +- .../formatter/plain_text_formatter.dart | 391 ++- lib/helpers/util.dart | 42 +- lib/helpers/validator.dart | 6 +- lib/l10n/app_en.arb | 1923 ++++++++++++++- lib/l10n/app_zh.arb | 2164 ++++++++++++----- lib/l10n/app_zh_Hant.arb | 1 - lib/l10n/app_zh_Hant_TW.arb | 1 - lib/l10n/app_zh_TW.arb | 1 - lib/main.dart | 44 +- lib/models/analysis/chart.dart | 8 +- lib/models/menu/product_ingredient.dart | 4 +- lib/models/menu/product_quantity.dart | 4 +- lib/models/model.dart | 4 +- lib/models/objects/order_object.dart | 8 +- lib/models/repository.dart | 11 +- lib/models/repository/cashier.dart | 22 +- lib/models/repository/seller.dart | 58 +- lib/my_app.dart | 18 +- lib/routes.dart | 40 +- lib/services/image_dumper.dart | 3 +- lib/settings/checkout_warning.dart | 17 +- lib/settings/collect_events_setting.dart | 10 +- lib/settings/currency_setting.dart | 41 +- lib/settings/language_setting.dart | 43 +- lib/settings/order_awakening_setting.dart | 10 +- lib/settings/order_outlook_setting.dart | 13 +- .../order_product_axis_count_setting.dart | 10 +- lib/settings/setting.dart | 4 +- lib/settings/settings_provider.dart | 31 +- lib/settings/theme_setting.dart | 10 +- lib/ui/analysis/analysis_view.dart | 74 +- lib/ui/analysis/history_page.dart | 18 +- lib/ui/analysis/widgets/chart_card_view.dart | 20 +- ...hart_order_modal.dart => chart_modal.dart} | 44 +- lib/ui/analysis/widgets/chart_range_page.dart | 58 +- lib/ui/analysis/widgets/chart_reorder.dart | 2 +- lib/ui/analysis/widgets/goals_card_view.dart | 78 +- ...r_view.dart => history_calendar_view.dart} | 25 +- .../analysis/widgets/history_order_list.dart | 6 +- .../analysis/widgets/history_order_modal.dart | 14 +- lib/ui/cashier/cashier_view.dart | 78 +- lib/ui/cashier/changer_page.dart | 11 +- lib/ui/cashier/surplus_page.dart | 34 +- .../cashier/widgets/changer_custom_view.dart | 27 +- .../widgets/changer_favorite_view.dart | 20 +- lib/ui/cashier/widgets/unit_list_view.dart | 12 +- lib/ui/home/feature_request_page.dart | 5 +- lib/ui/home/features_page.dart | 371 +-- lib/ui/home/home_page.dart | 33 +- lib/ui/home/setting_view.dart | 66 +- lib/ui/home/widgets/feature_slider.dart | 4 + lib/ui/home/widgets/feature_switch.dart | 4 + lib/ui/image_gallery_page.dart | 26 +- lib/ui/menu/menu_page.dart | 120 +- lib/ui/menu/product_page.dart | 18 +- lib/ui/menu/widgets/catalog_modal.dart | 6 +- lib/ui/menu/widgets/catalog_reorder.dart | 2 +- lib/ui/menu/widgets/menu_catalog_list.dart | 12 +- lib/ui/menu/widgets/menu_product_list.dart | 6 +- .../widgets/product_ingredient_modal.dart | 11 +- .../menu/widgets/product_ingredient_view.dart | 11 +- lib/ui/menu/widgets/product_modal.dart | 10 +- .../menu/widgets/product_quantity_modal.dart | 10 +- lib/ui/menu/widgets/product_reorder.dart | 2 +- lib/ui/order/cart/cart_actions.dart | 33 +- lib/ui/order/cart/cart_metadata_view.dart | 5 +- lib/ui/order/cart/cart_product_list.dart | 9 +- lib/ui/order/cart/cart_product_selector.dart | 6 +- .../cart/cart_product_state_selector.dart | 4 +- lib/ui/order/cart/cart_snapshot.dart | 24 +- .../checkout_attribute_view.dart} | 14 +- .../checkout_cashier_calculator.dart} | 24 +- .../checkout_cashier_snapshot.dart} | 10 +- .../stashed_order_list_view.dart | 32 +- ...ils_page.dart => order_checkout_page.dart} | 31 +- lib/ui/order/order_page.dart | 17 +- .../order/widgets/draggable_sheet_view.dart | 7 +- lib/ui/order/widgets/order_actions.dart | 18 +- .../widgets/order_catalog_list_view.dart | 2 +- lib/ui/order/widgets/order_object_view.dart | 47 +- .../widgets/order_product_list_view.dart | 11 +- lib/ui/order_attr/order_attribute_page.dart | 6 +- .../widgets/order_attribute_list.dart | 39 +- .../widgets/order_attribute_modal.dart | 12 +- .../widgets/order_attribute_option_modal.dart | 20 +- .../order_attribute_option_reorder.dart | 2 +- .../widgets/order_attribute_reorder.dart | 2 +- lib/ui/stock/quantity_page.dart | 6 +- lib/ui/stock/replenishment_page.dart | 106 +- lib/ui/stock/stock_view.dart | 54 +- lib/ui/stock/widgets/replenishment_apply.dart | 11 +- lib/ui/stock/widgets/replenishment_modal.dart | 10 +- .../stock/widgets/stock_ingredient_list.dart | 16 +- .../stock/widgets/stock_ingredient_modal.dart | 17 +- lib/ui/stock/widgets/stock_quantity_list.dart | 10 +- .../stock/widgets/stock_quantity_modal.dart | 18 +- .../google_sheet/export_basic_view.dart | 28 +- .../google_sheet/export_order_view.dart | 73 +- .../google_sheet/import_basic_view.dart | 61 +- .../transit/google_sheet/order_formatter.dart | 83 +- .../google_sheet/order_setting_page.dart | 32 +- lib/ui/transit/google_sheet/order_table.dart | 27 +- lib/ui/transit/google_sheet/sheet_namer.dart | 20 +- .../transit/google_sheet/sheet_selector.dart | 6 +- .../google_sheet/spreadsheet_selector.dart | 134 +- .../export_basic_view.dart | 13 +- .../export_order_view.dart | 67 +- .../import_basic_view.dart | 12 +- .../views.dart | 0 .../previews/ingredient_preview_page.dart | 7 +- .../order_attribute_preview_page.dart | 6 +- lib/ui/transit/previews/preview_page.dart | 6 +- .../previews/product_preview_page.dart | 7 +- .../previews/quantity_preview_page.dart | 4 +- .../previews/replenishment_preview_page.dart | 2 +- lib/ui/transit/transit_order_list.dart | 26 +- lib/ui/transit/transit_order_range.dart | 37 +- lib/ui/transit/transit_page.dart | 23 +- lib/ui/transit/transit_station.dart | 28 +- pubspec.lock | 8 + pubspec.yaml | 10 + test/components/snackbar_test.dart | 10 +- test/debug/random_gen_order_test.dart | 15 +- test/debug/rerun_migration_test.dart | 11 +- .../google_sheet_formatter_test.dart | 7 +- .../formatter/plain_text_formatter_test.dart | 67 +- test/helpers/util_test.dart | 8 + test/image_gallery_page_test.dart | 5 +- test/models/initialize_test.dart | 2 +- test/models/repository_test.dart | 12 +- test/my_app_test.dart | 12 +- test/settings/currency_settting_test.dart | 7 +- test/settings/language_setting_test.dart | 15 +- test/test_helpers/order_setter.dart | 4 +- test/test_helpers/translator.dart | 4 +- test/ui/analysis/analysis_view_test.dart | 31 +- test/ui/analysis/history_page_test.dart | 15 +- .../widgets/chart_card_view_test.dart | 22 +- .../widgets/chart_range_page_test.dart | 29 +- .../widgets/goals_card_view_test.dart | 15 +- .../widgets/history_order_list_test.dart | 46 +- test/ui/cashier/cashier_view_test.dart | 19 +- test/ui/cashier/changer_page_test.dart | 9 +- test/ui/home/features_page_test.dart | 78 +- test/ui/home/home_page_test.dart | 6 +- test/ui/menu/menu_page_test.dart | 10 +- test/ui/menu/product_page_test.dart | 8 +- test/ui/order/order_actions_test.dart | 22 +- ...est.dart => order_checkout_page_test.dart} | 17 +- test/ui/order/order_page_test.dart | 102 +- test/ui/order/stashed_order_test.dart | 8 +- .../order_attr/order_attribute_page_test.dart | 8 +- test/ui/stock/replenishment_page_test.dart | 64 +- test/ui/stock/stock_view_test.dart | 5 +- .../google_sheet/export_basic_test.dart | 24 +- .../google_sheet/export_order_test.dart | 80 +- .../google_sheet/import_basic_test.dart | 30 +- .../google_sheet/select_spreadsheet_test.dart | 31 +- .../transit/plain_text/export_basic_test.dart | 14 +- .../transit/plain_text/export_order_test.dart | 60 +- .../transit/plain_text/import_basic_test.dart | 17 +- test/ui/transit/transit_order_list_test.dart | 12 +- test/ui/transit/transit_page_test.dart | 2 - 223 files changed, 8754 insertions(+), 4496 deletions(-) delete mode 100644 .vscode/arb.code-snippets create mode 100644 assets/l10n/en/analysis.yaml create mode 100644 assets/l10n/en/cashier.yaml create mode 100644 assets/l10n/en/global.yaml create mode 100644 assets/l10n/en/menu.yaml create mode 100644 assets/l10n/en/order.yaml create mode 100644 assets/l10n/en/order_attribute.yaml create mode 100644 assets/l10n/en/setting.yaml create mode 100644 assets/l10n/en/stock.yaml create mode 100644 assets/l10n/en/transit.yaml create mode 100644 assets/l10n/zh/analysis.yaml create mode 100644 assets/l10n/zh/cashier.yaml create mode 100644 assets/l10n/zh/global.yaml create mode 100644 assets/l10n/zh/menu.yaml create mode 100644 assets/l10n/zh/order.yaml create mode 100644 assets/l10n/zh/order_attribute.yaml create mode 100644 assets/l10n/zh/setting.yaml create mode 100644 assets/l10n/zh/stock.yaml create mode 100644 assets/l10n/zh/transit.yaml delete mode 100644 lib/components/scaffold/item_list_scaffold.dart rename lib/components/{mixin => scaffold}/item_modal.dart (100%) rename lib/components/style/{more_button.dart => buttons.dart} (90%) create mode 100644 lib/components/style/date_range_picker.dart rename lib/components/style/{launcher_snackbar_action.dart => snackbar_actions.dart} (100%) create mode 100644 lib/debug/debug_page.dart delete mode 100644 lib/l10n/app_zh_Hant.arb delete mode 100644 lib/l10n/app_zh_Hant_TW.arb delete mode 100644 lib/l10n/app_zh_TW.arb rename lib/ui/analysis/widgets/{chart_order_modal.dart => chart_modal.dart} (86%) rename lib/ui/analysis/widgets/{calendar_view.dart => history_calendar_view.dart} (88%) rename lib/ui/order/{cashier/order_setting_view.dart => checkout/checkout_attribute_view.dart} (83%) rename lib/ui/order/{cashier/order_cashier_calculator.dart => checkout/checkout_cashier_calculator.dart} (92%) rename lib/ui/order/{cashier/order_cashier_snapshot.dart => checkout/checkout_cashier_snapshot.dart} (86%) rename lib/ui/order/{cashier => checkout}/stashed_order_list_view.dart (85%) rename lib/ui/order/{cashier/order_details_page.dart => order_checkout_page.dart} (83%) rename lib/ui/transit/{plain_text_widgets => plain_text}/export_basic_view.dart (93%) rename lib/ui/transit/{plain_text_widgets => plain_text}/export_order_view.dart (60%) rename lib/ui/transit/{plain_text_widgets => plain_text}/import_basic_view.dart (85%) rename lib/ui/transit/{plain_text_widgets => plain_text}/views.dart (100%) rename test/ui/order/{order_details_page_test.dart => order_checkout_page_test.dart} (98%) diff --git a/.github/workflows/deploy-to-playstore.yaml b/.github/workflows/deploy-to-playstore.yaml index dd9f8638..c61553c0 100644 --- a/.github/workflows/deploy-to-playstore.yaml +++ b/.github/workflows/deploy-to-playstore.yaml @@ -147,7 +147,7 @@ jobs: run: echo "$GOOGLE_SERVICES_JSON" > google-services.json env: GOOGLE_SERVICES_JSON: | - ${{ secrets.GOOGLE_SERVICES_JSON }} + ${{ needs.var.outputs.lane == 'internal' && secrets.GOOGLE_SERVICES_JSON_DEV || secrets.GOOGLE_SERVICES_JSON }} working-directory: android/app - name: Configure Keystore diff --git a/.github/workflows/release-candidate.yaml b/.github/workflows/release-candidate.yaml index 956b18e3..bfdcfcc0 100644 --- a/.github/workflows/release-candidate.yaml +++ b/.github/workflows/release-candidate.yaml @@ -125,7 +125,7 @@ jobs: - name: Configure Google Services run: echo "$GOOGLE_SERVICES_JSON" > google-services.json env: - GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON_DEV }} working-directory: android/app - name: Configure Play Store @@ -145,7 +145,7 @@ jobs: # Build the application. - name: Start building run: | - flutter build -v apk --release --flavor dev + flutter build -v apk --release --flavor dev --dart-define=appFlavor=dev --dart-define=logLevel=info mv build/app/outputs/flutter-apk/app-dev-release.apk \ $GITHUB_WORKSPACE/pos_system.apk diff --git a/.vscode/arb.code-snippets b/.vscode/arb.code-snippets deleted file mode 100644 index ac2f8d05..00000000 --- a/.vscode/arb.code-snippets +++ /dev/null @@ -1,45 +0,0 @@ -{ - "Add key value": { - "scope": "json", - "prefix": "add", - "body": [ - "\"${1:name}\": \"$2\",", - "\"@$1\": {", - "\t\"description\": \"${3:description}\"", - "}," - ], - "description": "Add one item and description for ARB" - }, - "Add plural": { - "scope": "json", - "prefix": "plural", - "body": [ - "\"${1:name}\": \"{${2:placeholder}, plural, =0{$3} =1{$4} other{{$2}}}\",", - "\"@$1\": {", - "\t\"description\": \"${0:description}\",", - "\t\"placeholders\": {", - "\t\t\"$2\": {", - "\t\t\t\"type\": \"int\"", - "\t\t}", - "\t}", - "}," - ], - "description": "Add one plural for ARB" - }, - "Add select": { - "scope": "json", - "prefix": "select", - "body": [ - "\"${1:name}\": \"{${2:placeholder}, select, other{${3:UNKNOWN}}}\",", - "\"@$1\": {", - "\t\"description\": \"${0:description}\",", - "\t\"placeholders\": {", - "\t\t\"$2\": {", - "\t\t\t\"type\": \"String\"", - "\t\t}", - "\t}", - "}," - ], - "description": "Add one select for ARB" - } -} diff --git a/.vscode/arb.schema.json b/.vscode/arb.schema.json index 7a3e95a3..d3d8f4dd 100644 --- a/.vscode/arb.schema.json +++ b/.vscode/arb.schema.json @@ -1,150 +1,146 @@ { - "title": "ARB placeholder configuration files", - "$schema": "http://json-schema.org/draft-04/schema#", + "title": "ARB yaml configuration files", + "$schema": "https://json-schema.org/draft-07/schema", + "type": "object", "patternProperties": { + "$prefix": { "type": "string" }, "^[\\w]+": { - "type": "string" + "type": ["string", "array", "object"], + "oneOf": [ + { "type": "string" }, + { + "type": "array", + "items": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/$defs/placeholders" }, + { "type": "object", "description": "allo empty value" }, + { + "description": "Select or plural key-value pair", + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + } + } + ] + } + }, + { "$ref": "#" } + ] }, - "^@[\\w]": { + "^@[\\w]": { "$ref": "#/$defs/meta" } + }, + "$defs": { + "meta": { "type": "object", "properties": { "description": { "type": "string" }, - "placeholders": { + "placeholders": { "$ref": "#/$defs/placeholders" } + } + }, + "placeholders": { + "type": "object", + "patternProperties": { + ".*": { "type": "object", - "patternProperties": { - ".*": { + "properties": { + "type": { + "type": "string", + "enum": [ + "String", + "string", + "integer", + "int", + "double", + "num", + "DateTime", + "Object" + ] + }, + "example": { "type": "string" }, + "description": { "type": "string" }, + "isCustomDateFormat": { + "anyOf": [ + { "type": "string", "enum": ["true", "false"] }, + { "type": "boolean" } + ] + }, + "optionalParameters": { "type": "object", "properties": { - "type": { - "type": "string", - "enum": [ - "String", - "int", - "double", - "num", - "DateTime", - "Object" - ] - }, - "example": { "type": "string" }, - "description": { "type": "string" }, - "optionalParameters": { - "type": "object", - "properties": { - "decimalDigits": { - "type": "number", - "description": "Use for currency, compactCurrency and compactSimpleCurrecy" - }, - "symbol": { - "type": "string", - "description": "Use for currency and compactCurrency" - }, - "customPattern": { - "type": "string", - "description": "Use for currency details in https://pub.dev/documentation/intl/latest/intl/NumberFormat/NumberFormat.currency.html" - } - } + "decimalDigits": { + "type": "number", + "description": "Use for currency, compactCurrency and compactSimpleCurrecy" }, - "format": { + "symbol": { "type": "string", - "enum": [ - "compact", - "compactCurrency", - "compactLong", - "compactSimpleCurrency", - "currency", - "decimalPattern", - "decimalPercentPattern", - "percentPattern", - "scientificPattern", - "simpleCurrency", - "d", - "E", - "EEEE", - "LLL", - "LLLL", - "M", - "Md", - "MEd", - "MMM", - "MMMd", - "MMMEd", - "MMMM", - "MMMMd", - "MMMMEEEEd", - "QQQ", - "QQQQ", - "y", - "yM", - "yMd", - "yMEd", - "yMMM", - "yMMMd", - "yMMMEd", - "yMMMM", - "yMMMMd", - "yMMMMEEEEd", - "yQQQ", - "yQQQQ", - "H", - "Hm", - "Hms", - "j", - "jm", - "jms", - "m", - "ms", - "s" - ], - "description": "See DateTime implement in https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html\nNumber implement in https://pub.dev/documentation/intl/latest/intl/NumberFormat-class.html" - } - }, - "defaultSnippets": [ - { - "label": "String", - "description": "Add description inside @key placeholder", - "body": { "description": "$1", "type": "${2:String}" } - }, - { - "label": "currency", - "description": "Currency format placeholder", - "body": { - "description": "$1", - "type": "num", - "format": "compactCurrency", - "optionalParameters": { - "decimalDigits": 0 - } - } - }, - { - "label": "int", - "description": "int placeholder", - "body": { "description": "$1", "type": "${2:int}" } + "description": "Use for currency and compactCurrency" }, - { - "label": "DateTime", - "description": "DateTime placeholder", - "body": { - "description": "$1", - "type": "DateTime", - "format": "${2:MMMEd}" - } + "customPattern": { + "type": "string", + "description": "Use for currency details in https://pub.dev/documentation/intl/latest/intl/NumberFormat/NumberFormat.currency.html" } - ] + } + }, + "format": { + "type": "string", + "enum": [ + "compact", + "compactCurrency", + "compactLong", + "compactSimpleCurrency", + "currency", + "decimalPattern", + "decimalPercentPattern", + "percentPattern", + "scientificPattern", + "simpleCurrency", + "d", + "E", + "EEEE", + "LLL", + "LLLL", + "M", + "Md", + "MEd", + "MMM", + "MMMd", + "MMMEd", + "MMMM", + "MMMMd", + "MMMMEEEEd", + "QQQ", + "QQQQ", + "y", + "yM", + "yMd", + "yMEd", + "yMMM", + "yMMMd", + "yMMMEd", + "yMMMM", + "yMMMMd", + "yMMMMEEEEd", + "yQQQ", + "yQQQQ", + "H", + "Hm", + "Hms", + "j", + "jm", + "jms", + "m", + "ms", + "s" + ], + "description": "See DateTime implement in https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html\nNumber implement in https://pub.dev/documentation/intl/latest/intl/NumberFormat-class.html" } } } - }, - "defaultSnippets": [ - { - "label": "With description", - "description": "Add description inside @key", - "body": { "description": "$1" } - } - ] + } } - }, - "type": "object" + } } diff --git a/.vscode/settings.json b/.vscode/settings.json index 256c230f..0b9bef3b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,15 +31,7 @@ "files.associations": { "*.arb": "json" }, - "json.schemas": [ - { - "fileMatch": ["/lib/l10n/*.arb"], - "url": "/.vscode/arb.schema.json" - } - ], - "json.format.enable": false, - "json.maxItemsComputed": 100, - "[json]": { - "editor.formatOnSave": false + "yaml.schemas": { + ".vscode/arb.schema.json": ["/assets/l10n/**/*.yaml"] } } diff --git a/Makefile b/Makefile index ba5a2be3..14e56373 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,8 @@ test: ## Run tests .PHONY: test-coverage test-coverage: ## Run tests with coverage flutter test --coverage - genhtml coverage/lcov.info -o coverage/html + @genhtml coverage/lcov.info -o coverage/html + @open coverage/html/index.html ##@ Build .PHONY: bump @@ -65,3 +66,14 @@ bump-beta: ## Bump beta version .PHONY: mock mock: ## Mock dependencies flutter pub run build_runner build --delete-conflicting-outputs + +.PHONY: build-l10n +build-l10n: ## Build localization + dart run arb_glue + flutter pub get --no-example + +.PHONY: clean-version +clean-version: ## Clean beta and rc version + @git pull + @git tag -l | grep -E 'beta|rc' | xargs git push --delete origin + @git tag -l | grep -E 'beta|rc' | xargs git tag -d diff --git a/README.md b/README.md index cac73b3d..50e18a46 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,18 @@ 本 POS 系統的特色。 -- 完全允許離線使用 -- 本系統不會遠端紀錄個資,只會存在你的手機裡,所以可以安心使用 -- 庫存系統幫助你紀錄現有成份庫存 -- 設定顧客資訊 -- 收銀機方便做每日結餘 -- 訂單、菜單等資訊的匯出與備份 -- 客製化折線圖、圓餅圖的分析 +- 完全允許離線使用 +- 本系統不會遠端紀錄個資,只會存在你的手機裡,所以可以安心使用 +- 庫存系統幫助你紀錄現有成份庫存 +- 設定顧客資訊 +- 收銀機方便做每日結餘 +- 訂單、菜單等資訊的匯出與備份 +- 客製化折線圖、圓餅圖的分析 ## 下載 -- Android 可以至 [Google Play](https://play.google.com/store/apps/details?id=com.evanlu.possystem) 下載。 -- iOS 要再等等,已排程準備。 +- Android 可以至 [Google Play](https://play.google.com/store/apps/details?id=com.evanlu.possystem) 下載。 +- iOS 要再等等,已排程準備。 ## 貢獻 diff --git a/assets/l10n/en/analysis.yaml b/assets/l10n/en/analysis.yaml new file mode 100644 index 00000000..42b5bae6 --- /dev/null +++ b/assets/l10n/en/analysis.yaml @@ -0,0 +1,142 @@ +$prefix: analysis +tab: Stats +history: + btn: Records + title: Order Records + _title: + $prefix: title + empty: No Order History Found + calendar: + tutorial: + title: Calendar + content: |- + Swipe up and down to adjust the time period, such as month or week. + Swipe left and right to adjust the date range. + export: + btn: Export + tutorial: + title: Export Orders Data + content: |- + Export orders externally for further analysis or backup. + You can export multi-day orders in the "Transit" page. + orderList: + meta: + price: + - 'Price: {price}' + - Price of specific orders in the order list. + - price: {type: num, format: compactCurrency, symbol: '$'} + paid: + - 'Paid: {paid}' + - Payment amount for specific orders in the order list. + - paid: {type: num, format: compactCurrency, symbol: '$'} + profit: + - 'Profit: {profit}' + - Net profit for specific orders in the order list. + - profit: {type: num, format: compactCurrency, symbol: '$'} + order: + title: Order Details + notFound: No relevant orders found + deleteDialog: + - |- + Are you sure you want to delete the order for {name}? + Cash register and inventory data cannot be recovered. + This action cannot be undone. + - name: +goals: + title: Today's Summary + count: + title: Order Count + description: |- + The order count reflects the attractiveness of products to customers. + It represents the demand for your products in the market and helps you understand which products or time periods are most popular. + A high order count may indicate the success of your pricing strategy or marketing activities and is one of the indicators of business model effectiveness. + However, it's essential to note that simply pursuing a high order count may overlook profitability. + revenue: + title: Revenue + description: |- + Revenue represents the total sales amount and is an indicator of business scale. + High revenue may indicate that your products are popular and selling well, but revenue alone cannot reflect the sustainability and profitability of the business. + Sometimes, to increase revenue, companies may adopt strategies such as price reductions, which may affect profitability. + profit: + title: Profit + description: |- + Profit is the balance after deducting operating costs from operating income and is crucial for the company's ongoing operations. + Profit directly reflects operational efficiency and cost management capabilities. + Unlike revenue, profit considers the business expenses, including raw material costs, labor, rent, etc. + It's a more practical indicator that helps you evaluate the effectiveness and sustainability of operations. + cost: + title: Cost + achievedRate: + - |- + Profit Achievement + {rate} + - rate: +chart: + title: Chart Analysis + _title: + $prefix: title + create: Create Chart + reorder: Reorder Charts + tutorial: + title: Chart Analysis + content: |- + With charts, you can visualize data changes more intuitively. + Start designing charts to track your sales performance now! + card: + emptyData: No Data + title: + update: Edit Chart + metricName: + - revenue: Revenue + cost: Cost + profit: Profit + count: Quantity + - name: + targetName: + - order: Order + catalog: Category + product: Product + ingredient: Ingredient + attribute: Attribute + - name: + range: + yesterday: Yesterday + today: Today + lastWeek: Last Week + thisWeek: This Week + last7Days: Last 7 Days + lastMonth: Last Month + thisMonth: This Month + last30Days: Last 30 Days + tabName: + - day: Date + week: Week + month: Month + custom: Custom + - name: + modal: + name: + label: Chart Name + hint: 'For example: Daily Revenue' + ignoreEmpty: + label: Ignore Empty Data + helper: Do not display if a product or metric has no data for that period. + divider: Data Settings + type: + label: Chart Type + name: + - cartesian: Time Series Chart + circular: Pie Chart + - name: + metric: + label: Metrics to View + helper: Choose different types of metrics based on your objectives. + target: + label: Item Category + helper: Select the information to analyze in the chart. + error: + empty: Please select an item category + targetItem: + label: Item Selection + helper: Choose the items you want to observe, such as the quantity of a specific product within a certain period. + selectAll: Select All diff --git a/assets/l10n/en/cashier.yaml b/assets/l10n/en/cashier.yaml new file mode 100644 index 00000000..9e45a24c --- /dev/null +++ b/assets/l10n/en/cashier.yaml @@ -0,0 +1,102 @@ +$prefix: cashier +tab: Cashier +unitLabel: +- '${unit}' +- unit: +counter: + label: + - Quantity + - Label when setting currency quantity. +toDefault: + title: Set as Default + tutorial: + title: Cash Register Default Status + content: |- + After setting the quantities of various currencies below, + click here to set the default status! + The set quantities will be the "maximum" for each currency status bar. + dialog: + title: Adjust Cash Register Default? + content: |- + This will set the current cash register status as the default status. + This action will override previous settings. +changer: + title: Changer + button: Apply + tutorial: + title: Cash Register Money Changer + content: |- + Exchange one hundred for 10 tens, for example. + Helps to quickly adjust the cash register status. + error: + noSelection: Please select a combination to apply + notEnough: + - "Not enough ${unit}" + - unit: + invalidHead: + - "Cannot exchange {count} of ${unit} to" + - count: {type: int} + unit: + invalidBody: + - "{count} of ${unit}" + - Concatenated multiple lines after `invalidHead` to form a complete sentence. + - count: {type: int} + unit: + favorite: + tab: Favorites + hint: After selecting, please click "Apply" to use the combination. + emptyBody: Here can help you quickly convert different currencies. + item: + from: + - Exchange {count} of ${unit} to + - count: {type: int} + unit: + to: + - "{count} of ${unit}" + - count: {type: int} + unit: + custom: + tab: Custom + addBtn: Add Favorite + count: + label: Quantity + unit: + label: Currency + addBtn: Add Currency + divider: + from: Withdraw from Cash Register + to: Exchange +surplus: + title: Surplus + button: Surplus + tutorial: + title: Daily Surplus + content: |- + Surplus helps us at the end of each day, + calculate the difference between the current amount and the default amount. + error: + emptyDefault: Default status not set yet + tableHint: Once you confirm that there are no issues with the cash register money, you can complete the surplus! + columnName: + - unit: Unit + currentCount: Current + diffCount: Difference + defaultCount: Default + counter: + label: + - Quantity of ${unit} + - Allow users to customize currency when surplus. + - unit: + shortLabel: + - Quantity + - This is for display in error messages, e.g., "Quantity cannot be 0". + currentTotal: + label: Current Total + helper: |- + The total amount the cash register should have now. + If you find that the cash and this value don't match, think about whether you used the cash register to buy something today? + diffTotal: + label: Difference + helper: |- + The difference from the total amount of the cash register at the very beginning. + This can quickly help you understand how much money the cash register has gained today. diff --git a/assets/l10n/en/global.yaml b/assets/l10n/en/global.yaml new file mode 100644 index 00000000..0b01686f --- /dev/null +++ b/assets/l10n/en/global.yaml @@ -0,0 +1,119 @@ +appTitle: POS System +act: + success: + - Successful! + - Action executed successfully and displayed on the Snackbar. + error: + - Unknown error occurred. + - Error message displayed on the Snackbar when an error occurs. + moreInfo: + - More + - Button on the Snackbar to show more details. +singleChoice: +- Select One +- Reminder to the user that only one option can be selected at a time. +multiChoices: +- Select Multiple +- Reminder to the user that multiple options can be selected. +totalCount: +- =0: No Items + =1: '{count} item' + other: '{count} items' +- Total count displayed on the ListView. +- count: {type: int, mode: plural, format: compactLong} +searchCount: +- Found {count} results +- Total count displayed on the SearchScaffold. +- count: {type: int, format: compact} +dialog: + deletionTitle: + - Delete Confirmation + - Title displayed on the DeleteDialog. + deletionContent: + - | + Are you sure you want to delete "{name}"? + + {more}This action cannot be undone! + - Content displayed on the DeleteDialog. + - name: {example: Veggie Sandwich} + more: {example: What other impacts deleting this item will have, description: More details about the side effects of deletion} +image: + holder: + create: Tap to add image + update: Click to update image + btn: + crop: Crop + gallery: + title: Gallery + empty: Start importing your first image! + action: + create: Add Image + delete: Delete + snackbar: + deleteFailed: One or more images failed to delete. + selection: + title: Select Images + deleteConfirm: + - |- + Will delete {count} image(s) permanently. + After deletion, the connected product will not able to display the image. + - count: {type: int, format: compact} +emptyBody: + title: + - Oops! It's empty here. + - Text displayed on EmptyBody, informing the user that there are no items yet. This is the default text. + action: Set Up Now +btn: + navTo: + - View + - Button text to navigate to another screen in trailing. + signInWith: + google: Sign in with Google +semantics: + percentileBar: + - Currently {percent} of total + - percent: {type: num, format: percentPattern} +invalid: + integer: + type: + - "{field} must be an integer." + - Warning message when the input is not an integer. + - field: + number: + type: + - "{field} must be a number." + - Warning message when the input is not a number. + - field: + positive: + - "{field} cannot be negative." + - Warning message when the input is not positive. + - field: + maximum: + - "{field} cannot exceed {maximum}." + - Warning message when the input exceeds the maximum value. + - field: + maximum: {type: num, description: Maximum value, must be less than (and not equal to) this value: '', format: decimalPattern} + minimum: + - "{field} cannot be less than {minimum}." + - Warning message when the input is less than the minimum value. + - field: + minimum: {type: num, description: Minimum value, must be greater than (and not equal to) this value: '', format: decimalPattern} + string: + empty: + - "{field} cannot be empty." + - Warning message when no text is entered. + - field: + maximum: + - "{field} cannot exceed {maximum} characters." + - Warning message when the input exceeds the maximum character limit. + - field: + maximum: {type: int, description: Maximum number of characters} +singleMonth: +- Single Month +- One of the units for calendar period conversion. +singleWeek: +- Single Week +- One of the units for calendar period conversion. +twoWeeks: +- Two Weeks +- One of the units for calendar period conversion. diff --git a/assets/l10n/en/menu.yaml b/assets/l10n/en/menu.yaml new file mode 100644 index 00000000..976260eb --- /dev/null +++ b/assets/l10n/en/menu.yaml @@ -0,0 +1,138 @@ +$prefix: menu +title: Menu +subtitle: Categories, Products +tutorial: + title: Create Your Menu + content: Let's start by creating a menu! +search: + hint: Search for products, ingredients, quantities + notFound: Couldn't find relevant information. Did you misspell something? +catalog: + headerInfo: + - Categories + - Displayed on the upper rectangle in homepage + tutorial: + title: Create First Catalog + emptyBody: |- + Similar "products" will be grouped under "categories", + making it convenient for ordering, such as: + • "Cheese Burger", "Veggie Burger" > "Burgers" + • "Plastic Bag", "Eco Cup" > "Others" + title: + create: + - Add Category + - FloatingActionButton description on the menu page + update: Edit Category + reorder: Reorder Categories + dialogDeletionContent: + - =0: No products inside + other: Will delete {count} products together + - Warning message when deleting product categories on the menu page + - count: {type: int, mode: plural} + name: + label: Category Name + hint: e.g., Burgers + error: + repeat: Name already exists. Please choose a different name! + emptyProducts: No products set yet +product: + headerInfo: + - Products + - Displayed on the upper rectangle in homepage + emptyBody: |- + "Products" are the basic units in the menu, such as: + "Cheese Burger", "Cola" + title: + create: Add Product + update: Edit Product + reorder: Reorder Products + updateImage: Update Photo + meta: + title: + - Product + - Prefix for meta, so users know this is product meta info, not category + price: + - 'Price: {price}' + - Price of the product + - price: {type: num, format: compact} + cost: + - 'Cost: {cost}' + - Cost of the product + - cost: {type: num, format: compact} + empty: + - No ingredients set + - Text displayed in the subtitle in the product list + name: + label: Product Name + hint: e.g., Cheeseburger + error: + repeat: Product name already exists + price: + label: Product Price + helper: Price displayed on the order page + cost: + label: Product Cost + helper: Used to calculate profit, should be less than the price + emptyIngredients: No ingredients set yet +ingredient: + emptyBody: |- + You can set ingredients for the product, such as: + "Cheeseburger" with "Cheese", "Bun" as ingredients + title: + create: Add Ingredient + update: Edit Ingredient + reorder: Reorder Ingredients + meta: + amount: + - 'Amount: {amount}' + - amount: {type: num, format: decimalPattern} + search: + label: Search Ingredients + helper: After adding ingredient, you can set related information in "Inventory". + hint: e.g., Cheese + add: + - Add Ingredient "{name}" + - Button to add ingredient if search result not found + - name: {type: String} + error: + empty: Ingredient must be set, please click to set. + repeat: Product already has the same ingredient, cannot select repeatedly. + amount: + label: Amount Used + helper: |- + Default amount used. + If customers are able to adjust the amount, + set different quantities in "Quantity." +quantity: + title: + create: Add Quantity + update: Edit Quantity + meta: + amount: + - 'Amount: {amount}' + - amount: {type: num, format: decimalPattern} + additionalPrice: + - 'Price: {price}' + - price: + additionalCost: + - 'Cost: {cost}' + - cost: + search: + label: Search Quantity + helper: After adding ingredient quantity, you can set related information in "Quantity". + hint: e.g., Large, Small + add: + - Add Quantity "{name}" + - Button to add quantity if search result not found + - name: + error: + empty: Quantity must be set, please click to set. + repeat: Product already has the same quantity, cannot select repeatedly. + amount: + label: Amount Used + additionalPrice: + label: Additional Price + helper: Set to 0 to indicate no additional charge for extra (or less) quantity. + additionalCost: + label: Additional Cost + helper: Additional cost can be negative, e.g., "Less" reduces ingredient usage, reducing cost accordingly. diff --git a/assets/l10n/en/order.yaml b/assets/l10n/en/order.yaml new file mode 100644 index 00000000..8f099356 --- /dev/null +++ b/assets/l10n/en/order.yaml @@ -0,0 +1,190 @@ +$prefix: order +title: Ordering +btn: Order +snackbar: + cashier: + notEnough: Insufficient cash in the cashier! + usingSmallMoney: Using smaller denominations to give change + usingSmallMoneyHelper: + - |- + When giving change to customers, if the cashier doesn't have the appropriate denominations, this message will appear. + + For example, if the total is $65 and the customer pays $100, the change should be $35. + If the cashier only has two $10 bills and more than three $5 bills, this message will appear. + + To avoid this prompt: + • Go to the changer page and top up various denominations. + • Go to the [settings page]({link}) to disable related prompts from the cashier. + - link: +action: + checkout: + - Checkout + - Proceed to the next step after confirming the items in your cart + exchange: Exchange + stash: Stash + review: Order History +loader: + meta: + totalRevenue: + - 'Revenue: {revenue}' + - Total revenue from orders in the order list + - revenue: + totalCost: + - 'Cost: {cost}' + - Total cost from orders in the order list + - cost: + totalCount: + - 'Count: {count}' + - Total number of orders in the order list + - count: {type: int, format: compact} + empty: No order records found +catalogList: + empty: No product categories set yet +productList: + tutorial: + title: Start Ordering! + content: + - |- + Ordering through images is more convenient! + You can go to "Settings" > "[Items Per Row]({link})" to adjust + and allow text-only ordering here. + - link: +cart: + action: + bulkify: Bulk Actions + toggle: Toggle + selectAll: Select All + discount: Discount + _discount: + $prefix: discount + label: Discount + hint: e.g., 30 means 70% off + helper: The number here represents the "percentage" off, i.e., 85 means 15% off. For precise prices, use "Price Change". + suffix: '%' + changePrice: Price Change + _changePrice: + $prefix: changePrice + label: Price + hint: Price per item + prefix: '$' + suffix: '' + changeCount: Change Quantity + _changeCount: + $prefix: changeCount + label: Quantity + hint: Quantity of items + suffix: items + free: Free + delete: Delete + snapshot: + tutorial: + title: Cart + content: + - |- + To make selecting products more convenient, + we've placed the products you've ordered here. + If you need a layout that shows all information at once (suitable for large screens), + go to "Settings" > "[Ordering Layout]({link})" to adjust. + - link: + empty: No items in cart + meta: + totalPrice: + - 'Price: {price}' + - Total price of items in the cart + - price: + totalCount: + - 'Count: {count}' + - Total number of items in the cart + - count: {type: int, format: compact} + product: + price: + - '0': Free + other: '${price}' + - Price of the product + - price: + increase: Increase Quantity + defaultQuantity: Default Quantity + ingredient: + - "{name} ({quantity})" + - Ingredients and quantities of each item in the product list when ordering + - name: + quantity: + ingredient: + status: + - emptyCart: Please select a product to set its ingredients + differentProducts: Please select the same product to set its ingredients + noNeedIngredient: This product doesn't require ingredient settings + - Prompt to users during ordering if the selected product doesn't require ingredient settings + - status: + quantity: + notAble: + - Please select an ingredient to set quantity + - During ordering, select the ingredient to set the quantity + label: + - '{name} ({amount})' + - name: + amount: {type: num, format: decimalPattern} + defaultLabel: + - Default ({amount}) + - During ingredient setup, the quantity can be customized or set to default (no quantity used) + - amount: {type: num, format: decimalPattern} +checkout: + emptyCart: Please make an order first. + action: + stash: Stash + confirm: Confirm + stash: + tab: Stash + empty: No items currently stashed. + noProducts: No products + action: + checkout: Checkout + restore: Restore + dialog: + calculator: Checkout Calculator + restore: + title: Restore Stashed Order + content: This action will override the current cart contents. + delete: + name: order + attribute: + tab: Customer Settings + cashier: + tab: Cashier + calculator: + label: + paid: Paid + change: Change + snapshot: + label: + change: + - 'Change: {change}' + - Change given by the cashier after the customer's payment + - change: + snackbar: + paidFailed: Payment is less than the order amount. +objectView: + empty: No order records found + change: Change + price: + total: + - 'Total Price: {price}' + - Total price information after ordering + - price: + products: Product Price + attributes: Customer Settings Price + cost: Cost + profit: Profit + paid: Paid + divider: + attribute: Customer Settings + product: Product Information + product: + price: Price + cost: Cost + count: Count + singlePrice: Unit Price + originalPrice: Original Unit Price + catalog: Product Category + ingredient: Ingredients + defaultQuantity: Default diff --git a/assets/l10n/en/order_attribute.yaml b/assets/l10n/en/order_attribute.yaml new file mode 100644 index 00000000..e5120f47 --- /dev/null +++ b/assets/l10n/en/order_attribute.yaml @@ -0,0 +1,108 @@ +$prefix: orderAttribute +title: Customer Settings +description: Information for analysis such as dine-in, takeout, etc. +_title: + $prefix: title + create: Add Customer Setting + update: Edit Customer Setting + reorder: Reorder Customer Settings +emptyBody: |- + Customer settings help us track who comes to consume, such as: + 20-30 years old, takeout, office workers, etc. +headerInfo: +- Customer Settings +- Displayed on the upper rectangle in homepage +tutorial: + title: Customer Settings + content: |- + This is where you set customer information, such as dine-in, takeout, office worker, etc. + This information helps us track who comes to consume and make better business strategies. +meta: + mode: + - 'Mode: {name}' + - name: + default: + - 'Default: {name}' + - name: + noDefault: None +mode: + divider: Customer Setting Mode + name: + - statOnly: Normal + changePrice: Price Change + changeDiscount: Discount + - Customer setting mode name + - name + helper: + - statOnly: Normal setting, selecting won't affect the order price. + changePrice: |- + Selecting this setting may affect the order price. + For example: Takeout +$30, Eco Cup -$5. + changeDiscount: |- + Selecting this setting will affect the total price based on the discount. + For example: Dine-in +10% service charge, Friends & Family Discount -10%. + - Explanation of customer setting categories + - name: +name: + label: Customer Setting Name + hint: e.g., Age + error: + repeat: Name already exists +option: + title: + create: Add Option + createWith: + - Add option for {name} + - name: + update: Edit Option + reorder: Reorder Options + meta: + default: Default + name: + label: Option Name + helper: |- + For example, possible options for age include: + - Under 20 + - 20 to 30 + error: + repeat: Name already exists + mode: + title: Option Mode + helper: + - statOnly: No need to set "Discount" or "Price Change" because this setting is "Normal" + changePrice: Selecting this option during ordering will apply this price change + changeDiscount: Selecting this option during ordering will apply this discount + - Explanation of mode + - name: + hint: + - statOnly: '' + changePrice: 'For example: -30 means decrease by thirty dollars' + changeDiscount: 'For example: 80 means "20% off"' + - name: + toDefault: + label: Set as Default + helper: |- + Set this option as the default value, which will be used for each order by default. + confirmChange: + title: Override Option Default? + content: + - Doing this will make "{name}" no longer the default value + - Prompt to ensure the user knows what the original default value was + - name: +value: # Options can have values, this is the text for `OrderAttributeValue` + empty: No price impact + free: Free + discount: + increase: + - Increase to {value} times + - value: {type: num, format: decimalPattern} + decrease: + - Decrease to {value} times + - value: {type: num, format: decimalPattern} + price: + increase: + - Increase by {value} dollars + - value: + decrease: + - Decrease by {value} dollars + - value: diff --git a/assets/l10n/en/setting.yaml b/assets/l10n/en/setting.yaml new file mode 100644 index 00000000..60fd282e --- /dev/null +++ b/assets/l10n/en/setting.yaml @@ -0,0 +1,69 @@ +$prefix: setting +tab: Settings +version: +- 'Version: {version}' +- Display the app version +- version: +welcome: +- Hi, {name} +- Display user's name +- name: +logoutBtn: Log Out +elf: + title: Suggestions + description: Provide feedback using Google Forms + content: |- + Feel like something's missing here? + Feel free to [give suggestions](https://forms.gle/s8V5SXuqhA1u3zmt7). + You can also check out [upcoming features](https://github.com/evan361425/flutter-pos-system/milestones). +feature: + title: Other Settings + description: Appearance, Language, Tips +theme: + title: Theme + name: + - dark: Dark Mode + light: Light Mode + system: Follow System + - Appearance of the app + - name: +language: + title: Language +orderOutlook: + title: Ordering Outlook + name: + - slidingPanel: Sliding Panel + singleView: Classic Mode + - Appearance during ordering + - name: + tip: + - slidingPanel: Panel slides up during ordering, suitable for small-screen phones + singleView: All info displayed on a single screen, suitable for large-screen tablets + - name: +checkoutWarning: + title: Cash Registry Warnings + name: + - showAll: Show All + onlyNotEnough: Show Only When Not Enough + hideAll: Hide All + - Whether to display cash registry warnings + - name: + tip: + - showAll: |- + Show warning when using smaller denominations to give change. + For example, if $5 is not enough, start using 5 $1 bills for change. + onlyNotEnough: Show warning when cash registry not enough money. + hideAll: Won't display any warnings during ordering. + - name: +orderProductCount: + title: Products per Row during Ordering + hint: Set to "0" to display only text during ordering + minLabel: Text Only +orderAwakening: + title: + - Keep Screen On During Ordering + - Keep the screen on during ordering, even when idle + description: If disabled, the screen will turn off based on system settings during ordering. +report: + title: Collect Error Messages and Events + description: Send error messages when the app encounters issues, helping the app improve diff --git a/assets/l10n/en/stock.yaml b/assets/l10n/en/stock.yaml new file mode 100644 index 00000000..9e13db99 --- /dev/null +++ b/assets/l10n/en/stock.yaml @@ -0,0 +1,121 @@ +$prefix: stock +tab: Inventory +updatedAt: +- 'Last Restock: {updatedAt}' +- updatedAt: {type: DateTime, format: MMMEd} +ingredient: + emptyBody: Once ingredients are added, you can start tracking their inventory! + title: + create: Add Ingredient + update: Edit Ingredient + updateAmount: Edit Inventory + tutorial: + title: Add Ingredient + content: |- + Ingredients help us track product inventory. + + You can add ingredients in "Menu" + and then manage inventory here. + dialogDeletionContent: + - =0: No products currently use this ingredient + other: Deleting this ingredient will also remove it from {count} products + - Indicates how many products will be affected when deleting the ingredient + - count: {type: int, mode: plural} + productsCount: + - '{count} products using it' + - When editing an ingredient, it indicates how many products are using it and allows for navigation to the product page + - count: {type: int, description: Number of products} + name: + label: Ingredient Name + hint: e.g., Cheese + error: + repeat: Ingredient name already exists + amount: + label: Current Amount + maxLabel: Maximum Amount + maxHelper: |- + Setting this value helps you see how much of the ingredient is being used. + Leave blank or don't fill it in, and the value will automatically be set each time inventory is increased. + shortHelper: + - If not set maximum amount, every time increase the amount will be considered as the maximum amount + - Auxiliary text used for quickly increasing inventory +replenishment: + button: Purchase + emptyBody: Purchasing helps you quickly adjust ingredient inventory + title: + list: Purchase List + create: Add Purchase + update: Edit Purchase + meta: + affect: + - Affects {count} Ingredients + - Indicates in the purchase list how many ingredients are affected + - count: {type: int} + never: + - Never Restocked + - The stock page displays the last restock time; if never restocked, this text is set + apply: + button: Apply Purchase + confirm: + button: Apply + title: Apply Purchase? + column: + - name: Name + amount: Amount + - value: + hint: After apply, following ingredients will be adjusted + tutorial: + title: Ingredient Purchases + content: |- + Through purchases, you no longer need to set the inventory of each ingredient one by one. + Set up purchases now and adjust multiple ingredients at once! + name: + label: Purchase Name + hint: e.g., Costco Purchase + error: + repeat: Purchase name already exists + ingredients: + divider: Ingredients + helper: Click to set the quantity of different ingredients to be purchased + ingredientAmount: + hint: Set the amount to increase/decrease +quantity: + title: Quantity + description: Half Sugar, Low Sugar, etc. + _title: + $prefix: title + create: Add Quantity + update: Edit Quantity + emptyBody: |- + Quantity allows for quick adjustments to the amount of ingredients, such as: + Half Sugar, Low Sugar. + meta: + proportion: + - 'Default Ratio: {proportion}' + - Text explaining default ratios in subheadings of quantity items + - proportion: {type: num, format: decimalPattern} + dialogDeletionContent: + - =0: No product ingredients currently use this quantity + other: Deleting this quantity will also remove it from {count} product ingredients + - Indicates how many product ingredients will be affected when deleting the quantity + - count: {type: int, mode: plural} + name: + label: Quantity Name + hint: e.g., Small or Large + error: + repeat: Quantity name already exists + proportion: + label: Default Ratio + helper: |- + Applied when this quantity is used for an ingredient. + + For example: + if this quantity is "Large" and the default ratio is "1.5", + and there's a product "Cheeseburger" with the ingredient "Cheese," + which uses "2" units of cheese per burger, + when adding this quantity, + the quantity of "Cheese" will automatically be set to "3" (2 * 1.5). + + If set to "1," there's no effect. + + If set to "0," the ingredient won't be used. diff --git a/assets/l10n/en/transit.yaml b/assets/l10n/en/transit.yaml new file mode 100644 index 00000000..5ee46e1f --- /dev/null +++ b/assets/l10n/en/transit.yaml @@ -0,0 +1,523 @@ +$prefix: transit +title: Data Transfer +description: Importing and Exporting Store Information and Orders +tutorial: + title: Sync Multiple Devices + content: |- + This is where you can import/export menu, inventory, order records, and other information. + + We provide two methods: Google Sheets and plain text, making it convenient to sync data across different devices. +method: + title: Please Select Transfer Method + name: + - googleSheet: Google Sheets + plainText: Plain Text + - name: +catalog: + name: + - order: Order Records + model: Store Information + - name: + helper: + - order: Export order info for detailed statistical analysis. + model: Store info is usually used to sync menu, inventory, etc., to third-party locations or to import to another device. + - name: +model: + name: + - menu: Menu + stock: Inventory + quantities: Quantities + replenisher: Replenisher + orderAttr: Customer Settings + order: Order + orderDetailsAttr: Order Customer Settings + orderDetailsProduct: Order Product Details + orderDetailsIngredient: Order Ingredient Details + - name: +order: + meta: + range: + - "Orders for {range}" + - range: {example: '01/01 - 01/31'} + rangeDays: + - "Data for {days} Days" + - days: {type: int} + capacity: + title: + - 'Estimated Capacity: {size}' + - size: + content: High capacity may cause execution errors. It's recommended to perform in batches and not export too many records at once. + ok: Capacity Okay + warn: Capacity Warning + danger: Capacity Danger + item: + title: + - "{date}" + - date: {type: DateTime, format: "MMM d HH:mm:ss", isCustomDateFormat: true} + meta: + productCount: + - 'Product Count: {count}' + - count: {type: int} + price: + - 'Total Price: {price}' + - price: + dialog: + title: Order Details +export: + preview: + btn: Preview + title: Preview Output Result + btn: Import +import: + preview: + btn: Preview + title: Preview Import Result + header: 'Note: Importing will remove the data not listed below. Please confirm before executing!' + ingredient: + meta: + amount: + - 'Amount: {amount}' + - amount: {type: num, format: decimalPattern} + maxAmount: + - =0: Not Set + other: 'Max Value: {value}' + - exist: {type: int, mode: plural} + value: {type: num, format: decimalPattern} + header: After import, old ingredients won't be removed to avoid affecting the "Menu" status. + quantity: + header: After import, old quantities won't be removed to avoid affecting the "Menu" status. + btn: Export + error: + columnCount: + - Insufficient data, {columns} columns required + - columns: {type: int} + duplicate: This line will be ignored as the same item appeared earlier + columnStatus: + - normal: (Normal) + staged: (New) + stagedIng: (New Ingredient) + stagedQua: (New Quantity) + updated: (Updated) + - Additional status of the data displayed + - name: +GS: + description: Google Sheets is a powerful mini-database. After exporting, it can be customized for various analyses! + sheet: + name: + label: + - 'Sheet Title of {name}' + - Label of title + - name: + update: Modify Title + spreadsheet: + label: Spreadsheet + action: + select: Select Spreadsheet + clear: Clear Selection + export: + empty: + label: Create & Export + hint: + - Create a new spreadsheet "{name}" and export data to it. + - name: + exist: + label: + - Specify & Export + - Inform the user that data will be exported to the specified spreadsheet. + hint: + - Export to spreadsheet "{name}" + - name: + import: + all: + btn: Import All + hint: There will be no preview screen, directly overwrite all data. + confirm: + title: Import All Data? + content: |- + All data from the selected sheets will be downloaded and completely overwrite local data. + This action cannot be undone. + exist: + label: Load Sheets Name + hint: Get all sheet names from the spreadsheet and ready to import. + empty: + label: Select Spreadsheet + hint: Once you choose the spreadsheet to import, you can start importing data. + confirm: + - This action will {hint} + - hint: + selectionHint: + - _: Enter the spreadsheet URL or spreadsheet ID + other: The current spreadsheet is "{name}" + - name: + model: + defaultName: POS System Data + export: + divider: Select types to export + import: + divider: Select sheet to import + order: + defaultName: POS System Orders + snackbarAction: Open + progressStatus: + addSpreadsheet: Adding Spreadsheet... + addSheets: Adding Sheets... + verifyUser: Verifying Identity + fetchLocalOrders: Retrieving Local Data... + overwriteOrders: Overwriting Order Data... + appendOrders: + - Appended to {name} + - name: + model: + status: + - menu: Updating Menu... + stock: Updating Inventory... + quantities: Updating Quantities... + replenisher: Updating Replenisher... + orderAttr: Exporting Customer Settings... + order: Exporting Orders... + orderDetailsAttr: Exporting Order Customer Settings... + orderDetailsProduct: Exporting Order Product Details... + orderDetailsIngredient: Exporting Order Ingredient Details... + - model: + product: + ingredient: + title: Ingredient Information + note: |- + Information of all product ingredients, format as follows: + - Ingredient 1, Default usage amount + + Quantity a, Additional usage amount, Additional price, Additional cost + + Quantity b, Additional usage amount, Additional price, Additional cost + - Ingredient 2, Default usage amount + replenishment: + title: Replenishment Amount + note: |- + The amount of specific ingredients during each replenishment, format as follows: + - Ingredient 1, Replenishment amount + - Ingredient 2, Replenishment amount + attributeOption: + title: Customer Setting Options + header: + ts: Timestamp + mode: Type + options: Options + note: |- + "Options" will have different meanings depending on the type of customer settings, format as follows: + - Option 1, Is default, Option value + - Option 2, Is default, Option value + order: + setting: + title: Order Export Settings + overwrite: + label: Overwrite Sheet + hint: Overwriting the sheet will start exporting from the first row. + titlePrefix: + label: Add Date Prefix + hint: Add a date prefix to the sheet name, for example, "0101 - 0131 Order Data". + recommendCombination: When not overwriting and using append instead, it's recommended not to add a date prefix to the form name. + name: + label: Sheet Name + helper: |- + Splitting the sheet allows for more flexible data analysis, + for example, you can query the total usage of a certain ingredient in order details. + meta: + overwrite: + - 'true': Will overwrite + 'false': Won't overwrite + - value: + titlePrefix: + - 'true': Has date prefix + 'false': No date prefix + - value: + memoryWarning: |- + The capacity here represents the amount consumed by network transmission, the actual cloud memory occupied may be only one percent of this value. + For detailed capacity limit explanations, please refer to [this document](https://developers.google.com/sheets/api/limits#quota). + header: + ts: Timestamp + time: Time + price: Price + productPrice: Product Price + paid: Paid + cost: Cost + profit: Profit + itemCount: + - Item Count + - how many items in the order + typeCount: + - Type Count + - how many types of products in the order + attribute: + title: Order Customer Settings + header: + ts: Timestamp + name: Setting Category + option: Option + product: + title: Order Product Details + header: + ts: Timestamp + name: Product + catalog: Category + count: Quantity + price: Single Price + cost: Single Cost + origin: Original Price + ingredient: + title: Order Ingredient Details + header: + ts: Timestamp + name: Ingredient + quantity: Quantity + amount: Amount + expandable: + hint: See next table + error: + createSpreadsheet: Unable to Create Spreadsheet + createSpreadsheetHelper: |- + Don't worry, it's usually easy to solve! + Possible reasons include: + • Unstable network conditions; + • POS system not authorized to edit spreadsheets. + spreadsheetEmpty: Please Select a Spreadsheet First + spreadsheetId: + empty: Cannot be Empty + invalid: |- + Invalid text. It must include: + • /spreadsheets/d// + • Or provide the ID directly (combination of letters, numbers, underscores, and hyphens). + createSheet: Unable to Create Sheet in Spreadsheet + createSheetHelper: |- + Don't worry, it's usually easy to solve! + Possible reasons include: + • Unstable network conditions; + • POS system not authorized to create sheets; + • Misspelled spreadsheet ID, try copying the entire URL and pasting it; + • The spreadsheet has been deleted. + sheetRepeat: Sheet name duplicate + sheetEmpty: Please select at least one sheet to export + nonExistName: Spreadsheet not found, has it been deleted? + import: + emptySpreadsheet: Must select a spreadsheet to import + emptySheet: Must select a specific sheet to import + emptyData: No values found in sheet + notFoundSpreadsheet: Spreadsheet not found + notFoundSheets: + - No data found for sheet "{name}" + - name: + notFoundHelper: |- + Don't worry, it's usually easy to solve! + Possible reasons include: + • Unstable network conditions; + • POS system not authorized to read sheets; + • Misspelled spreadsheet ID, try copying the entire URL and pasting it; + • The spreadsheet has been deleted. +PT: + description: Quick check, quick share. + copy: + btn: Copy Text + success: Copied successfully + warning: Copying too much text may cause system crash + import: + hint: Paste copied text here + helper: After pasting the text, it will analyze and determine the type of information to import. + error: + notFound: This text cannot match any corresponding service. Please refer to the exported text content. + format: + order: + price: + - =0: Total price ${price}. + other: Total price ${price}, {productsPrice} of them are product price. + - hasProducts: {type: int, mode: plural} + price: + productsPrice: + money: + - Paid ${paid}, cost ${cost}. + - paid: + cost: + productCount: + - =0: There is no product. + =1: |- + There is 1 product details are: + {products}. + other: |- + There are {count} products ({setCount} types of set) including: + {products}. + - count: {type: int, mode: plural} + setCount: {type: int} + products: + product: + - =0: '{count} of {product} ({catalog}), total price is ${price}, no ingredient settings' + other: '{count} of {product} ({catalog}), total price is ${price}, ingredients are {ingredients}' + - hasIngredient: {type: int, mode: plural} + product: + catalog: + count: {type: int} + price: + ingredients: + ingredient: + - =0: "{ingredient} ({quantity})" + other: "{ingredient} ({quantity}), used {amount}" + - Details of ingredients and quantities for each product in the order list + - amount: {type: num, mode: plural, format: decimalPattern} + ingredient: + quantity: + noQuantity: default quantity + orderAttribute: + - Customer's {options}. + - options: + orderAttributeItem: + - '{name} is {option}' + - name: + option: + model: + menu: + meta: + catalog: + - '{count} categories' + - count: {type: int} + product: + - '{count} products' + - count: {type: int} + header: + - This menu has {catalogs} categories, {products} products. + - catalogs: {type: int} + products: {type: int} + headerPrefix: + - This menu has + - This is used to check if this text is a menu + catalog: + - Category {index} is called {catalog} and {details}. + - Strings are used so that regex can be inserted here during import to obtain information + - index: + catalog: + details: + catalogDetails: + - =0: it has no product + =1: it has one product + other: it has {count} products + - count: {type: int, mode: plural} + product: + - Product {index} is called {name}, with price at ${price}, cost ${cost} and {details} + - Strings are used so that regex can be inserted here during import to obtain information + - index: + name: + price: + cost: + details: + productDetails: + - =0: it has no ingredient. + =1: |- + it has one ingredient: {names}. + Each product requires {details}. + other: |- + it has {count} ingredients: {names}. + Each product requires {details}. + - count: {type: int, mode: plural} + names: + details: + ingredient: + - '{amount} of {name} and {details}' + - Strings are used so that regex can be inserted here during import to obtain information + - amount: + name: + details: + ingredientDetails: + - =0: it is unable to adjust quantity + =1: 'it also has one different quantity {quantities}' + other: 'it also has {count} different quantities {quantities}' + - count: {type: int, mode: plural} + quantities: + quantity: + - quantity {amount} with additional price ${price} and cost ${cost} + - Strings are used so that regex can be inserted here during import to obtain information + - amount: + price: + cost: + stock: + meta: + ingredient: + - '{count} ingredients' + - count: {type: int} + header: + - The inventory has {count} ingredients in total. + - count: {type: int} + headerPrefix: + - The inventory has + - This is used to check if this text is stock + ingredient: + - Ingredient at {index} is called {name}, with {amount} amount{details}. + - Strings are used so that regex can be inserted here during import to obtain information + - index: + name: + amount: + details: + ingredientDetails: + - =0: '' + other: ', with a maximum of {max} pieces' + - String(max) are used so that regex can be inserted here during import to obtain information + - exist: {type: int, mode: plural} + max: + quantities: + meta: + quantity: + - '{count} quantities' + - count: {type: int} + header: + - '{count} quantities have been set.' + - count: {type: int} + headerSuffix: + - quantities have been set. + - This is used to check if this text is quantities + quantity: + - Quantity at {index} is called {name}, which defaults to multiplying ingredient quantity by {prop}. + - Strings are used so that regex can be inserted here during import to obtain information + - index: + name: + prop: + replenisher: + meta: + replenishment: + - '{count} replenishment methods' + - count: {type: int} + header: + - '{count} replenishment methods have been set.' + - count: {type: int} + headerSuffix: + - replenishment methods have been set. + - This is used to check if this text is replenishment quantity + replenishment: + - Replenishment method at {index} is called {name}, {details}. + - Strings are used so that regex can be inserted here during import to obtain information + - index: + name: + details: + replenishmentDetails: + - =0: 'it will not adjust inventory' + other: 'it will adjust the inventory of {count} ingredients' + - count: {type: int, mode: plural} + oa: # order attribute + meta: + oa: + - '{count} customer attributes' + - count: {type: int} + header: + - '{count} customer attributes have been set.' + - count: {type: int} + headerSuffix: + - customer attributes have been set. + - This is used to check if this text is customer settings + oa: + - Attribute at {index} is called {name}, belongs to {mode} type, {details}. + - Strings are used so that regex can be inserted here during import to obtain information + - index: + name: + mode: + details: + oaDetails: + - =0: it has no options + =1: it has one option + other: 'it has {count} options' + - count: {type: int, mode: plural} + defaultOption: default + modeValue: + - option value is {value} + - value: {type: num, format: decimalPattern} diff --git a/assets/l10n/zh/analysis.yaml b/assets/l10n/zh/analysis.yaml new file mode 100644 index 00000000..f69c9cd7 --- /dev/null +++ b/assets/l10n/zh/analysis.yaml @@ -0,0 +1,129 @@ +$prefix: analysis +tab: 統計 +history: + btn: 紀錄 + title: 訂單記錄 + _title: + $prefix: title + empty: 查無點餐紀錄 + calendar: + tutorial: + title: 日曆 + content: |- + 上下滑動可以調整週期單位,如月或週。 + 左右滑動可以調整日期起訖。 + export: + btn: 匯出 + tutorial: + title: 訂單資料匯出 + content: |- + 把訂單匯出到外部,讓你可以做進一步分析或保存。 + 你可以到「資料轉移」去匯出多日訂單。 + orderList: + meta: + price: 售價:{price} + paid: 付額:{paid} + profit: 淨利:{profit} + order: + title: 訂單詳情 + notFound: 找不到相關訂單 + deleteDialog: |- + 確定要刪除 {name} 的訂單嗎? + 將不會復原收銀機和庫存資料。 + 此動作無法復原。 +goals: + title: 本日總結 + count: + title: 訂單數 + description: |- + 訂單數反映了產品對顧客的吸引力。 + 它代表了市場對你產品的需求程度,能幫助你了解何種產品或時段最受歡迎。 + 高訂單數可能意味著你的定價策略或行銷活動取得成功,是商業模型有效性的指標之一。 + 但要注意,單純追求高訂單數可能會忽略盈利能力。 + revenue: + title: 營收 + description: |- + 營收代表總銷售額,是業務規模的指標。 + 高營收可能顯示了你的產品受歡迎且銷售良好,但營收無法反映出業務的可持續性和盈利能力。 + 有時候,為了提高營收,公司可能會採取降價等策略,這可能會對公司的盈利能力造成影響。 + profit: + title: 淨利 + description: |- + 淨利是營業收入減去營業成本後的餘額,是公司能否持續經營的關鍵。 + 盈利直接反映了營運效率和成本管理能力。 + 不同於營收,盈利考慮了生意的開支,包括原料成本、人力、租金等, + 這是一個更實際的指標,能幫助你評估經營是否有效且可持續。 + cost: + title: 成本 + achievedRate: |- + 利潤達成 + {rate} +chart: + title: 圖表分析 + _title: + $prefix: title + create: 新增圖表 + reorder: 排序圖表 + tutorial: + title: 圖表分析 + content: |- + 透過圖表,你可以更直觀地看到數據變化。 + 現在就開始設計圖表追蹤你的銷售狀況吧!。 + card: + emptyData: 沒有資料 + title: + update: 編輯圖表 + metricName: + - revenue: 營收 + cost: 成本 + profit: 淨利 + count: 數量 + - name: + targetName: + - order: 訂單 + catalog: 產品種類 + product: 產品 + ingredient: 成分 + attribute: 顧客屬性 + - name: + range: + yesterday: 昨天 + today: 今天 + lastWeek: 上週 + thisWeek: 本週 + last7Days: 最近7日 + lastMonth: 上月 + thisMonth: 本月 + last30Days: 最近30日 + tabName: + - day: 日期 + week: 週 + month: 月 + custom: 自訂 + - name: + modal: + name: + label: 圖表名稱 + hint: 例如:每日營收 + ignoreEmpty: + label: 忽略空資料 + helper: 某商品或指標在該時段沒有資料,則不顯示。 + divider: 資料設定 + type: + label: 圖表類型 + name: + - cartesian: 時序圖 + circular: 圓餅圖 + - name: + metric: + label: 觀看指標 + helper: 根據不同目的,選擇不同指標類型。 + target: + label: 項目種類 + helper: 選擇圖表中要針對哪些資訊做分析。 + error: + empty: 請選擇一個項目種類 + targetItem: + label: 項目選擇 + helper: 你想要觀察哪些項目的變化,例如區間內某商品的數量。 + selectAll: 全選 diff --git a/assets/l10n/zh/cashier.yaml b/assets/l10n/zh/cashier.yaml new file mode 100644 index 00000000..8e34c155 --- /dev/null +++ b/assets/l10n/zh/cashier.yaml @@ -0,0 +1,82 @@ +$prefix: cashier +tab: 收銀 +unitLabel: +- 幣值:{unit} +- unit: +counter: + label: + - 數量 + - 設定幣值數量時的標籤 +toDefault: + title: 設為預設 + tutorial: + title: 收銀機預設狀態 + content: |- + 在下面設定完收銀機各幣值的數量後, + 按這裡設定預設狀態! + 設定好的數量就會是各個幣值狀態條的「最大值」。 + dialog: + title: 調整收銀臺預設? + content: |- + 這將會把目前的收銀機狀態設定為預設狀態。 + 此動作將會覆蓋掉先前的設定。 +changer: + title: 換錢 + button: 套用 + tutorial: + title: 收銀機換錢 + content: |- + 一百塊換成 10 個十塊之類。 + 幫助快速調整收銀機狀態。 + error: + noSelection: 請選擇要套用的組合 + notEnough: "{unit} 元不夠換" + invalidHead: "{count} 個 {unit} 元沒辦法換" + invalidBody: "{count} 個 {unit} 元" + favorite: + tab: 常用 + hint: 選完後請點選「套用」來使用該組合 + emptyBody: 這裡可以幫助你快速轉換不同幣值 + item: + from: 用 {count} 個 {unit} 元換 + to: "{count} 個 {unit} 元" + custom: + tab: 自訂 + addBtn: 新增常用 + count: + label: 數量 + unit: + label: 幣值 + addBtn: 新增幣種 + divider: + from: 從收銀機中拿出 + to: 換 +surplus: + title: 結餘 + button: 結餘 + tutorial: + title: 每日結餘 + content: |- + 結餘可以幫助我們在每天打烊時, + 計算現有金額和預設金額的差異。 + error: + emptyDefault: 尚未設定預設狀態 + tableHint: 若你確認收銀機的金錢都沒問題之後就可以完成結餘囉! + columnName: + - unit: 單位 + currentCount: 現有 + diffCount: 差異 + defaultCount: 預設 + counter: + label: 幣值{unit}的數量 + shortLabel: 數量 + currentTotal: + label: 現有總額 + helper: |- + 現在收銀機應該要有的總額。 + 若你發現現金和這值對不上,想一想今天有沒有用收銀機的錢買東西? + diffTotal: + label: 差額 + helper: |- + 和收銀機最一開始的總額的差額。 + 這可以快速幫你了解今天收銀機多了多少錢唷。 diff --git a/assets/l10n/zh/global.yaml b/assets/l10n/zh/global.yaml new file mode 100644 index 00000000..0d9b765e --- /dev/null +++ b/assets/l10n/zh/global.yaml @@ -0,0 +1,58 @@ +appTitle: POS 系統 +act: + success: 執行成功 + error: 發生未知錯誤 + moreInfo: 說明 +singleChoice: 一次只能選擇一種 +multiChoices: 可以選擇多種 +totalCount: +- other: 總共 {count} 項 +searchCount: 搜尋到 {count} 個結果 +dialog: + deletionTitle: 刪除確認通知 + deletionContent: |- + 確定要刪除「{name}」嗎? + + {more}此動作將無法復原! +image: + holder: + create: 點選以新增圖片 + update: 點擊以更新圖片 + btn: + crop: 裁切 + gallery: + title: 圖片管理 + empty: 點擊開始匯入你的第一張照片! + action: + create: 新增圖片 + delete: 刪除 + snackbar: + deleteFailed: 有一個或多個圖片沒有刪成功。 + selection: + title: 選擇相片 + deleteConfirm: |- + 將會刪除 {count} 個圖片 + 刪除之後會讓相關產品顯示不到圖片 +emptyBody: + title: 哎呀!這裡還是空的 + action: 立即設定 +btn: + navTo: 查看 + signInWith: + google: 使用 Google 登入 +semantics: + percentileBar: 目前佔總數的 {percent} +invalid: + integer: + type: "{field}必須是整數" + number: + type: "{field}必須是數字" + positive: "{field}不能為負數" + maximum: "{field}不能超過 {maximum}" + minimum: "{field}不能低於 {minimum}" + string: + empty: "{field}不能為空" + maximum: "{field}不能超過 {maximum} 個字" +singleMonth: 單月 +singleWeek: 單週 +twoWeeks: 雙週 diff --git a/assets/l10n/zh/menu.yaml b/assets/l10n/zh/menu.yaml new file mode 100644 index 00000000..34332f4a --- /dev/null +++ b/assets/l10n/zh/menu.yaml @@ -0,0 +1,103 @@ +$prefix: menu +title: 菜單 +subtitle: 產品種類、產品 +tutorial: + title: 建立屬於你的菜單 + content: 首先我們來開始建立一份菜單吧! +search: + hint: 搜尋產品、成分、份量 + notFound: 搜尋不到相關資訊,打錯字了嗎? +catalog: + headerInfo: 種類 + tutorial: + title: Create First Catalog + emptyBody: |- + 我們會把相似「產品」放在「產品種類」中, + 到時候點餐會比較方便,例如: + • 「起司漢堡」、「蔬菜漢堡」整合進「漢堡」 + • 「塑膠袋」、「環保杯」整合進「其他」 + title: + create: 新增產品種類 + update: 編輯產品種類 + reorder: 排序產品種類 + dialogDeletionContent: + - =0: 其內無任何產品 + other: 將會一同刪除掉 {count} 個產品 + name: + label: 產品種類名稱 + hint: 例如:漢堡 + error: + repeat: 名稱重複了,請改個名字吧! + emptyProducts: 尚未設定產品 +product: + headerInfo: 產品 + emptyBody: |- + 「產品」是菜單裡的基本單位,例如: + 「起司漢堡」、「可樂」 + title: + create: 新增產品 + update: 編輯產品 + reorder: 排序產品 + updateImage: 更新照片 + meta: + title: 產品 + price: 價格:{price} + cost: 成本:{cost} + empty: 尚未設定成分 + name: + label: 產品名稱 + hint: 例如:起司漢堡 + error: + repeat: 產品名稱重複 + price: + label: 產品價格 + helper: 訂單頁面會呈現的價錢 + cost: + label: 產品成本 + helper: 用來算出利潤,理應小於價錢 + emptyIngredients: 尚未設定成分 +ingredient: + emptyBody: |- + 你可以在產品中設定成分等資訊,例如: + 「起司漢堡」有「起司」、「麵包」等成分 + title: + create: 新增成分 + update: 編輯成分 + reorder: 排序成分 + meta: + amount: 使用量:{amount} + search: + label: 搜尋成分 + helper: 新增成分後,可至「庫存」設定相關資訊。 + hint: 例如:起司 + add: 新增成分「{name}」 + error: + empty: 必須設定成分,請點選以設定。 + repeat: 產品已經有相同的成分了,不能重複選取。 + amount: + label: 使用量 + helper: 預設的使用量,若餐點可以調整該成分的使用量,請於成分的「份量」中設定。 +quantity: + title: + create: 新增份量 + update: 編輯份量 + meta: + amount: 使用量:{amount} + additionalPrice: 額外售價:{price} + additionalCost: 額外成本:{cost} + search: + label: 搜尋份量 + helper: 新增成分份量後,可至「份量」設定相關資訊。 + hint: 例如:多量、少量 + add: 新增份量「{name}」 + error: + empty: 必須設定份量,請點選以設定。 + repeat: 產品已經有相同的份量了,不能重複選取。 + amount: + label: 使用量 + additionalPrice: + label: 額外售價 + helper: 設為 0 則代表加量(減量)不加價。 + additionalCost: + label: 額外成本 + helper: 預額外成本可以為負數,如「少量」會減少成分的使用,相對成本降低。 diff --git a/assets/l10n/zh/order.yaml b/assets/l10n/zh/order.yaml new file mode 100644 index 00000000..5337292b --- /dev/null +++ b/assets/l10n/zh/order.yaml @@ -0,0 +1,147 @@ +$prefix: order +title: 點餐 +btn: 點餐 +snackbar: + cashier: + notEnough: 收銀機錢不夠找囉! + usingSmallMoney: 收銀機使用小錢去找零 + usingSmallMoneyHelper: |- + 找錢給顧客時,收銀機無法使用最適合的錢,就會顯示這個訊息。 + + 例如,售價「65」,消費者支付「100」,此時應找「35」 + 如果收銀機只有兩個十元,且有三個以上的五元,就會顯示本訊息。 + + 怎麼避免本提示: + • 到換錢頁面把各幣值補足。 + • 到[設定頁]({link})關閉收銀機的相關提示。 +action: + checkout: 結帳 + exchange: 換錢 + stash: 暫存本次點餐 + review: 訂單記錄 +loader: + meta: + totalRevenue: 總營收:{revenue} + totalCost: 總成本:{cost} + totalCount: 總數:{count} + empty: 查無點餐紀錄 +catalogList: + empty: 尚未設定產品種類 +productList: + tutorial: + title: 開始點餐! + content: + - |- + 透過圖片點餐更方便! + 可以至「設定」>「[每行顯示幾個產品]({link})」調整 + 讓這裡僅使用文字點餐。 + - link: +cart: + action: + bulkify: 批量操作 + toggle: 反選 + selectAll: 全選 + discount: 打折 + _discount: + $prefix: discount + label: 折扣 + hint: 例如:50,代表打五折(半價) + helper: 這裡的數字代表「折」,即,85 代表 85 折,總價乘 0.85。若需要準確的價錢請用「變價」。 + suffix: 折 + changePrice: 變價 + _changePrice: + $prefix: changePrice + label: 價錢 + hint: 每項產品的價錢 + prefix: '' + suffix: 元 + changeCount: 變更數量 + _changeCount: + $prefix: changeCount + label: 數量 + hint: 產品數量 + suffix: 個 + free: 招待 + delete: 刪除 + snapshot: + tutorial: + title: 購物車 + content: |- + 為了讓點選產品可以更方便, + 我們把點餐後的產品設定至於此面板。 + 如果需要一次顯示所有訊息的排版(適合大螢幕), + 可以至「設定」>「[點餐的外觀]({link})」調整。 + empty: 尚未點餐 + meta: + totalPrice: 總價:{price} + totalCount: 總數:{count} + product: + price: + - '0': 免費 + other: '{price}元' + increase: 數量加一 + defaultQuantity: 預設份量 + ingredient: "{name}({quantity})" + ingredient: + status: + - emptyCart: 請選擇產品來設定其成分 + differentProducts: 請選擇相同的產品來設定其成分 + noNeedIngredient: 這個產品沒有可以設定的成分 + quantity: + notAble: 請選擇成分來設定份量 + label: '{name}({amount})' + defaultLabel: 預設值({amount}) +checkout: + emptyCart: 請先進行點單。 + action: + stash: 暫存 + confirm: 確認 + stash: + tab: 暫存 + empty: 目前無任何暫存餐點。 + noProducts: 沒有任何產品 + action: + checkout: 結帳 + restore: 還原 + dialog: + calculator: 結帳計算機 + restore: + title: 還原暫存訂單 + content: 此動作將會覆蓋掉現在購物車內的訂單。 + delete: + name: 訂單 + attribute: + tab: 顧客設定 + cashier: + tab: 收銀機 + calculator: + label: + paid: 付額 + change: 找錢 + snapshot: + label: + change: 找錢:{change} + snackbar: + paidFailed: 付額小於訂單總價,無法結帳。 +objectView: + empty: 查無點餐紀錄 + change: 找錢 + price: + total: 訂單總價:{price} + products: 產品總價 + attributes: 顧客設定總價 + cost: 成本 + profit: 淨利 + paid: 付額 + divider: + attribute: 顧客設定 + product: 產品資訊 + product: + price: 總價 + cost: 總成本 + count: 總數 + singlePrice: 單價 + originalPrice: 折扣前單價 + catalog: 產品種類 + ingredient: 成分 + defaultQuantity: 預設 diff --git a/assets/l10n/zh/order_attribute.yaml b/assets/l10n/zh/order_attribute.yaml new file mode 100644 index 00000000..7f08d188 --- /dev/null +++ b/assets/l10n/zh/order_attribute.yaml @@ -0,0 +1,82 @@ +$prefix: orderAttribute +title: 顧客設定 +description: 內用、外帶等幫助分析的資訊 +_title: + $prefix: title + create: 新增顧客設定 + update: 編輯顧客設定 + reorder: 排序顧客設定 +emptyBody: |- + 顧客設定可以幫助我們統計哪些人來消費,例如: + 20-30歲、外帶、上班族。 +headerInfo: 顧客設定 +tutorial: + title: 顧客設定 + content: |- + 這裡是用來設定顧客的資訊,例如:內用、外帶、上班族等。 + 這些資訊可以幫助我們統計哪些人來消費,進而做出更好的經營策略。 +meta: + mode: 種類:{name} + default: 預設:{name} + noDefault: 未設定預設 +mode: + divider: 顧客設定種類 + name: + - statOnly: 一般 + changePrice: 變價 + changeDiscount: 折扣 + helper: + - statOnly: 一般的設定,選取時並不會影響點單價格。 + changePrice: |- + 選取設定時,可能會影響價格。 + 例如:外送 + 30塊錢、環保杯 - 5塊錢。 + changeDiscount: |- + 選取設定時,會根據折扣影響總價。 + 例如:內用 + 10% 服務費、親友價 - 10%。 +name: + label: 顧客設定名稱 + hint: 例如:顧客年齡 + error: + repeat: 名稱不能重複 +option: + title: + create: 新增選項 + createWith: 新增{name}的選項 + update: 編輯選項 + reorder: 排序選項 + meta: + default: 預設 + name: + label: 選項名稱 + helper: |- + 以年齡為例,可能的選項有: + - 20 歲以下 + - 20 到 30 歲 + error: + repeat: 名稱不能重複 + mode: + title: 選項模式 + helper: + - statOnly: 因為本設定為「一般」故無須設定「折價」或「變價」 + changePrice: 訂單時選擇此項會套用此變價 + changeDiscount: 訂單時選擇此項會套用此折價 + hint: + - statOnly: '' + changePrice: 例如:-30 代表減少三十塊 + changeDiscount: 例如:80 代表「八折」 + toDefault: + label: 設為預設 + helper: |- + 設定此選項為預設值,每個訂單預設都會是使用這個選項。 + confirmChange: + title: 覆蓋選項預設? + content: 這麼做會讓「{name}」變成非預設值 +value: + empty: 不影響價錢 + free: 免費 + discount: + increase: 增加至 {value} 倍 + decrease: 減少至 {value} 倍 + price: + increase: 增加 {value} 元 + decrease: 減少 {value} 元 diff --git a/assets/l10n/zh/setting.yaml b/assets/l10n/zh/setting.yaml new file mode 100644 index 00000000..7be2e0ca --- /dev/null +++ b/assets/l10n/zh/setting.yaml @@ -0,0 +1,53 @@ +$prefix: setting +tab: 設定 +version: 版本:{version} +welcome: HI,{name} +logoutBtn: 登出 +elf: + title: 建議 + description: 使用 Google 表單提供回饋 + content: |- + 覺得這裡還少了什麼嗎? + 歡迎[提供建議](https://forms.gle/R1vZDk9ztQLScUdb9)。 + 也可以來看看[排程中的功能](https://github.com/evan361425/flutter-pos-system/milestones)。 +feature: + title: 其他設定 + description: 外觀、語言、提示 +theme: + title: 調色盤 + name: + - dark: 暗色模式 + light: 日光模式 + system: 跟隨系統 +language: + title: 語言 +orderOutlook: + title: 點餐的外觀 + name: + - slidingPanel: 酷炫面板 + singleView: 經典模式 + tip: + - slidingPanel: 點餐時下方會有可拉動的面板,內含點餐中的資訊,適合小螢幕的手機 + singleView: 所有資訊顯示在單一螢幕中,適合大螢幕的平板 +checkoutWarning: + title: 收銀機提示 + name: + - showAll: 全部顯示 + onlyNotEnough: 僅不夠時顯示 + hideAll: 全部隱藏 + tip: + - showAll: |- + 若使用小錢去找,顯示提示。 + 例如 5 塊錢不夠了,開始用 5 個 1 塊去找錢 + onlyNotEnough: 當零錢不夠找的時候,顯示提示。 + hideAll: 當點餐時,收銀機不會顯示任何提示 +orderProductCount: + title: 點餐時每行顯示幾個產品 + hint: 設定「零」則點餐時僅會以文字顯示 + minLabel: 純文字顯示 +orderAwakening: + title: 點餐時不關閉螢幕 + description: 若取消,則會根據系統設定時間關閉螢幕 +report: + title: 收集錯誤訊息和事件 + description: 當應用程式發生錯誤時,寄送錯誤訊息,以幫助應用程式成長 diff --git a/assets/l10n/zh/stock.yaml b/assets/l10n/zh/stock.yaml new file mode 100644 index 00000000..70571685 --- /dev/null +++ b/assets/l10n/zh/stock.yaml @@ -0,0 +1,98 @@ +$prefix: stock +tab: 庫存 +updatedAt: 上次補貨時間:{updatedAt} +ingredient: + emptyBody: 新增成份後,就可以開始追蹤這些成份的庫存囉! + title: + create: 新增成分 + update: 編輯成分 + updateAmount: 編輯庫存 + tutorial: + title: 新增成分 + content: |- + 成份可以幫助我們確認產品的庫存。 + 你可以在「產品」中設定成分,然後在這裡設定庫存。 + dialogDeletionContent: + - =0: 目前無任何產品有本成分 + other: 將會一同刪除掉 {count} 個產品的成分 + productsCount: 共有 {count} 個產品使用此成分 + name: + label: 成分名稱 + hint: 例如:起司 + error: + repeat: 成分名稱重複 + amount: + label: 現有庫存 + maxLabel: 最大庫存 + maxHelper: |- + 設定這個值可以幫助你一眼看出用了多少成分。 + + 填空或不填寫則每次增加庫存,都會自動設定這值, + 這是假設每次補貨都會讓庫存達到最大值。 + shortHelper: 若沒有設定最大庫存量,增加庫存會重設最大值。 +replenishment: + button: 採購 + emptyBody: 採購可以幫你快速調整成分的庫存 + title: + list: 採購列表 + create: 新增採購 + update: 編輯採購 + meta: + affect: 會影響 {count} 項成分 + never: 尚未補貨過 + apply: + button: 套用採購 + confirm: + button: 套用 + title: 套用採購? + column: + - name: 名稱 + amount: 數量 + hint: 選擇套用後,將會影響以下成分的庫存 + tutorial: + title: 成份採購 + content: |- + 透過採購,你不再需要一個一個去設定成分的庫存。 + 馬上設定採購,一次調整多個成份吧! + name: + label: 採購名稱 + hint: 例如:Costco 採購 + error: + repeat: 採購名稱重複 + ingredients: + divider: 成分 + helper: 點選以設定不同成分欲採購的量 + ingredientAmount: + hint: 設定增加/減少的量 +quantity: + title: 份量 + description: 半糖、微糖等。 + _title: + $prefix: title + create: 新增份量 + update: 編輯份量 + emptyBody: |- + 份量可以快速調整成分的量,例如: + 半糖、微糖。 + meta: + proportion: 預設比例:{proportion} + dialogDeletionContent: + - =0: 目前無任何產品成分有本份量 + other: 將會一同刪除掉 {count} 個產品成分的份量' + name: + label: 份量名稱 + hint: 例如:少量或多量 + error: + repeat: 份量名稱重複 + proportion: + label: 預設比例 + helper: |- + 當產品成分使用此份量時,預設替該成分增加的比例。 + + 例如:此份量為「多量」預設份量為「1.5」, + 今有一產品「起司漢堡」的成分「起司」,每份漢堡會使用「2」單位的起司, + 當增加此份量時,則會自動替「起司」設定為「3」(2 * 1.5)的份量。 + + 若設為「1」則無任何影響。 + + 若設為「0」則代表將不會使用此成分 diff --git a/assets/l10n/zh/transit.yaml b/assets/l10n/zh/transit.yaml new file mode 100644 index 00000000..26788ca0 --- /dev/null +++ b/assets/l10n/zh/transit.yaml @@ -0,0 +1,357 @@ +$prefix: "transit" +title: 資料轉移 +description: 匯入、匯出店家資訊和訂單 +tutorial: + title: 同步多台裝置 + content: |- + 這裡是用來匯入匯出菜單、庫存、訂單記錄等資訊的地方。 + + 我們提供了 Google 試算表和純文字兩種方式,讓您可以方便地在不同裝置間同步資料。 +method: + title: 請選擇欲轉移的方式 + name: + - googleSheet: Google 試算表 + plainText: 純文字 +catalog: # 資料的分類 + name: + - order: 訂單記錄 + model: 店家資訊 + helper: + - order: 訂單資訊可以讓你匯出到第三方位置後做更細緻的統計分析。 + model: 商家資訊通常是用來把菜單、庫存等資訊同步到第三方位置或用來匯入到另一台手機。 +model: + name: + - menu: 菜單 + stock: 庫存 + quantities: 份量 + replenisher: 補貨 + orderAttr: 顧客設定 + order: 訂單 + orderDetailsAttr: 訂單顧客設定 + orderDetailsProduct: 訂單產品細項 + orderDetailsIngredient: 訂單成分細項 +order: + meta: + range: "{range}的訂單" + rangeDays: "{days} 天的資料" + capacity: + title: 預估容量為:{size} + content: 過高的容量可能會讓執行錯誤,建議分次執行,不要一次匯出太多筆。 + ok: 容量剛好 + warn: 容量警告 + danger: 容量危險 + item: + title: "{date}" + meta: + productCount: 餐點數:{count} + price: 總價:{price} + dialog: + title: 訂單細節 +export: + preview: + btn: 預覽 + title: 預覽輸出結果 + btn: 匯入 +import: + preview: + btn: 預覽 + title: 預覽匯入結果 + header: 注意:匯入後將會把下面沒列到的資料移除,請確認是否執行! + ingredient: + meta: + amount: 庫存:{amount} + maxAmount: + - =0: 未設定 + other: 最大值:{value} + header: 匯入後,為了避免影響「菜單」的狀況,並不會把舊的成分移除。 + quantity: + header: 匯入後,為了避免影響「菜單」的狀況,並不會把舊的份量移除。 + btn: 匯出 + error: + columnCount: 資料量不足,需要 {columns} 個欄位 + duplicate: 將忽略本行,相同的項目已於前面出現 + columnStatus: + - normal: (一般) + staged: (新增) + stagedIng: (新的成分) + stagedQua: (新的份量) + updated: (異動) +GS: + description: Google 試算表是一個強大的小型資料庫,匯出之後可以做很多客制化的分析! + sheet: + name: + label: '{name}的表單標題' + update: 修改標題 + spreadsheet: + label: 試算表 + action: + select: 選擇試算表 + clear: 清除所選 + export: + empty: + label: 建立匯出 + hint: 建立新的試算表「{name}」,並把資料匯出至此 + exist: + label: 指定匯出 + hint: 匯出至試算表「{name}」 + import: + all: + btn: 匯入全部 + hint: 不會有任何預覽畫面,直接覆寫全部的資料。 + confirm: + title: 匯入全部資料? + content: |- + 將會把所選表單的資料都下載,並完全覆蓋本地資料。 + 此動作無法復原。 + exist: + label: 確認表單名稱 + hint: 從試算表中取得所有表單的名稱,並進行匯入 + empty: + label: 選擇試算表 + hint: 選擇要匯入的試算表後,就能開始匯入資料 + confirm: 此動作將會{hint} + selectionHint: + - _: 輸入試算表網址或試算表 ID + other: 原試算表為「{name}」 + model: + defaultName: POS System 資料 + export: + divider: 選擇欲匯出的種類 + import: + divider: 選擇欲匯入表單 + order: + defaultName: 訂單資料 + snackbarAction: 開啟表單 + progressStatus: + addSpreadsheet: 新增試算表中.. + addSheets: 新增表單中.. + verifyUser: 驗證身份中 + fetchLocalOrders: 取得本地資料.. + overwriteOrders: 覆寫訂單資料.. + appendOrders: 附加進 {name} + model: + status: + - menu: 更新菜單中.. + stock: 更新庫存中.. + quantities: 更新份量中.. + replenisher: 更新補貨中.. + orderAttr: 更新顧客設定中.. + order: 匯出訂單中.. + orderDetailsAttr: 匯出顧客設定中.. + orderDetailsProduct: 匯出產品細項中.. + orderDetailsIngredient: 匯出成分細項中.. + product: + ingredient: + title: 成分資訊 + note: |- + 產品全部成分的資訊,格式如下: + - 成分1,預設使用量 + + 份量a,額外使用量,額外價格,額外成本 + + 份量b,額外使用量,額外價格,額外成本 + - 成分2,預設使用量 + replenishment: + title: 補貨量 + note: |- + 每次補貨時特定成分的量,格式如下: + - 成分1,補貨量 + - 成分2,補貨量 + attributeOption: + title: 顧客設定選項 + header: + ts: 時間戳記 + mode: 類型 + options: 選項 + note: |- + 「選項值」會根據顧客設定種類不同而有不同意義,格式如下: + - 選項1,是否為預設,選項值 + - 選項2,是否為預設,選項值 + order: + setting: + title: 訂單匯出設定 + overwrite: + label: 是否覆寫表單 + hint: 覆寫表單之後,將會從第一行開始匯出 + titlePrefix: + label: 加上日期前綴 + hint: 表單名稱前面加上日期前綴,例如:「0101 - 0131 訂單資料」 + recommendCombination: 不覆寫而改用附加的時候,建議表單名稱「不要」加上日期前綴 + name: + label: 表單名稱 + helper: |- + 拆分表單可以讓你更彈性的去分析資料, + 例如可以到訂單成份細項查詢:今天某個成分總共用了多少。 + meta: + overwrite: + - 'true': 會覆寫 + 'false': 不會覆寫 + titlePrefix: + - 'true': 有日期前綴 + 'false': 沒有日期前綴 + memoryWarning: |- + 這裡的容量代表網路傳輸所消耗的量,實際佔用的雲端記憶體可能是此值的百分之一而已。 + 詳細容量限制說明可以參考[本文件](https://developers.google.com/sheets/api/limits#quota)。 + header: + ts: 時間戳記 + time: 時間 + price: 總價 + productPrice: 產品總價 + paid: 付額 + cost: 成本 + profit: 收入 + itemCount: 產品份數 + typeCount: 產品類數 + attribute: + title: 訂單顧客設定 + header: + ts: 時間戳記 + name: 設定類別 + option: 選項 + product: + title: 訂單產品細項 + header: + ts: 時間戳記 + name: 產品 + catalog: 種類 + count: 數量 + price: 單一售價 + cost: 單一成本 + origin: 單一原價 + ingredient: + title: 訂單成分細項 + header: + ts: 時間戳記 + name: 成分 + quantity: 份量 + amount: 數量 + expandable: # 資料放在不只一個資料表,透過多個資料表來分類 + hint: 詳見下欄 + error: + createSpreadsheet: 無法建立試算表 + createSpreadsheetHelper: |- + 別擔心,通常都可以簡單解決! + 可能的原因有: + • 網路狀況不穩; + • 尚未授權 POS 系統進行表單的編輯。 + spreadsheetEmpty: 請先選擇試算表 + spreadsheetId: + empty: 不能為空 + invalid: |- + 不合法的文字,必須包含: + • /spreadsheets/d// + • 或者直接給 ID(英文+數字+底線+減號的組合) + createSheet: 無法在試算表中建立表單 + createSheetHelper: |- + 別擔心,通常都可以簡單解決! + 可能的原因有: + • 網路狀況不穩; + • 尚未授權 POS 系統進行表單的建立; + • 試算表 ID 打錯了,請嘗試複製整個網址後貼上; + • 該試算表被刪除了。 + sheetRepeat: 表單名稱重複 + sheetEmpty: 請選擇至少一個表單來匯出 + nonExistName: 找不到試算表,是否已被刪除? + import: + emptySpreadsheet: 必須選擇試算表來匯入 + emptySheet: 必須選擇指定的表單來匯入 + emptyData: 在表單中沒找到任何值 + notFoundSpreadsheet: 找不到試算表 + notFoundSheets: 找不到表單「{name}」的資料 + notFoundHelper: |- + 別擔心,通常都可以簡單解決! + 可能的原因有: + • 網路狀況不穩; + • 尚未授權 POS 系統進行表單的讀取; + • 試算表 ID 打錯了,請嘗試複製整個網址後貼上; + • 該試算表被刪除了。 +PT: + description: 快速檢查、快速分享。 + copy: + btn: 複製文字 + success: 複製成功 + warning: 複製過大的文字可能會造成系統的崩潰 + import: + hint: 請貼上複製而來的文字 + helper: 貼上文字後,會分析文字並決定匯入的是什麼種類的資訊。 + error: + notFound: 這段文字無法匹配相應的服務,請參考匯出時的文字內容。 + format: + order: + price: + - =0: 共 {price} 元。 + other: 共 {price} 元,其中的 {productsPrice} 元是產品價錢。 + money: 付額 {paid} 元、成分 {cost} 元。 + productCount: + - =0: 沒有任何餐點。 + =1: |- + 餐點有 {count} 份,內容為: + {products}。 + other: |- + 餐點有 {count} 份({setCount} 種組合)包括: + {products}。 + product: + - =0: '{product}({catalog}){count} 份共 {price} 元,沒有設定成分' + other: '{product}({catalog}){count} 份共 {price} 元,成份包括 {ingredients}' + ingredient: + - =0: "{ingredient}({quantity})" + other: "{ingredient}({quantity}),使用 {amount} 個" + noQuantity: 預設份量 + orderAttribute: 顧客的 {options} + orderAttributeItem: '{name} 為 {option}' + model: + menu: + meta: + catalog: '{count} 個產品種類' + product: '{count} 個產品' + header: 本菜單共有 {catalogs} 個產品種類、{products} 個產品。 + headerPrefix: 本菜單 + catalog: 第{index}個種類叫做 {catalog},{details}。 + catalogDetails: + - =0: 沒有設定產品 + other: 共有 {count} 個產品 + product: 第{index}個產品叫做 {name},其售價為 {price} 元,成本為 {cost} 元,{details} + productDetails: + - =0: 它沒有設定任何成份。 + other: |- + 它的成份有 {count} 種:{names}。 + 每份產品預設需要使用 {details}。 + ingredient: '{amount} 個 {name},{details}' + ingredientDetails: + - =0: 無法做份量調整 + other: 它還有 {count} 個不同份量 {quantities} + quantity: '每份產品改成使用 {amount} 個並調整產品售價 {price} 元和成本 {cost} 元' + stock: + meta: + ingredient: '{count} 種成分' + header: 本庫存共有 {count} 種成分。 + headerPrefix: 本庫存 + ingredient: 第{index}個成分叫做 {name},庫存現有 {amount} 個{details}。 + ingredientDetails: + - =0: '' + other: ,最大量有 {max} 個 + quantities: + meta: + quantity: '{count} 種份量' + header: 共設定 {count} 種份量。 + headerSuffix: 種份量。 + quantity: 第{index}種份量叫做 {name},預設會讓成分的份量乘以 {prop} 倍。 + replenisher: + meta: + replenishment: '{count} 種補貨方式' + header: 共設定 {count} 種補貨方式。 + headerSuffix: 種補貨方式。 + replenishment: 第{index}個成分叫做 {name},{details}。 + replenishmentDetails: + - =0: '它並不會調整庫存' + other: 它會調整{count}種成份的庫存 + oa: # order attribute + meta: + oa: '{count} 種顧客屬性' + header: 共設定 {count} 種顧客屬性。 + headerSuffix: 種顧客屬性。 + oa: 第{index}種屬性叫做 {name},屬於 {mode} 類型,{details}。 + oaDetails: + - =0: 它並沒有設定選項 + other: 它有 {count} 個選項 + defaultOption: 預設 + modeValue: 選項的值為 {value} + diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md index 32efc9ca..c957aa1f 100644 --- a/docs/CODE_OF_CONDUCT.md +++ b/docs/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -- The use of sexualized language or imagery and unwelcome sexual attention or advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@github.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/docs/PRIVACY_POLICY.md b/docs/PRIVACY_POLICY.md index 8a8c082a..c8b2ff34 100644 --- a/docs/PRIVACY_POLICY.md +++ b/docs/PRIVACY_POLICY.md @@ -22,13 +22,13 @@ When you register for an Account, we may ask for your contact information, inclu We use the information we collect in various ways, including to: -- Provide, operate, and maintain our apps -- Improve, personalize, and expand our apps -- Understand and analyze how you use our apps -- Develop new products, services, features, and functionality -- Communicate with you, either directly or through one of our partners, including for customer service, to provide you with updates and other information relating to the apps, and for marketing and promotional purposes -- Send you emails -- Find and prevent fraud +- Provide, operate, and maintain our apps +- Improve, personalize, and expand our apps +- Understand and analyze how you use our apps +- Develop new products, services, features, and functionality +- Communicate with you, either directly or through one of our partners, including for customer service, to provide you with updates and other information relating to the apps, and for marketing and promotional purposes +- Send you emails +- Find and prevent fraud ## Log Files diff --git a/docs/README.md b/docs/README.md index 75e01cc5..000892e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,18 +4,18 @@ 本 POS 系統的特色。 -- 完全允許離線使用 -- 本系統不會遠端紀錄個資,只會存在你的手機裡,所以可以安心使用 -- 庫存系統幫助你紀錄現有成份庫存 -- 設定顧客資訊 -- 收銀機方便做每日結餘 -- 訂單、菜單等資訊的匯出與備份 -- 客製化折線圖、圓餅圖的分析 +- 完全允許離線使用 +- 本系統不會遠端紀錄個資,只會存在你的手機裡,所以可以安心使用 +- 庫存系統幫助你紀錄現有成份庫存 +- 設定顧客資訊 +- 收銀機方便做每日結餘 +- 訂單、菜單等資訊的匯出與備份 +- 客製化折線圖、圓餅圖的分析 ## 下載 -- Android 可以至 [Google Play](https://play.google.com/store/apps/details?id=com.evanlu.possystem) 下載。 -- iOS 要再等等,已排程準備。 +- Android 可以至 [Google Play](https://play.google.com/store/apps/details?id=com.evanlu.possystem) 下載。 +- iOS 要再等等,已排程準備。 ## 貢獻 diff --git a/docs/about/contribute.md b/docs/about/contribute.md index 0e8f15a6..6658413d 100644 --- a/docs/about/contribute.md +++ b/docs/about/contribute.md @@ -4,12 +4,12 @@ 本 POS 系統是一個開源的專案,並且由各方貢獻者一點一點把這產品建構起來。我們很高興有你的加入。無論你有多少時間和能力,你的付出我們都給予高度感謝。有很多種貢獻方式: -- [改善文件](#改善文件) -- [回報程式害蟲](#如何回報程式害蟲) -- [提出新功能或改善功能](#提出新功能或改善功能) -- [設計外觀和整體架構](#調整使用者介面) -- 透過留言回應其他人的 [issue]({{ site.github.repository_url }}/issues) -- [直接撰寫程式碼改善 POS 系統](#怎麼提出程式碼上的異動) +- [改善文件](#改善文件) +- [回報程式害蟲](#如何回報程式害蟲) +- [提出新功能或改善功能](#提出新功能或改善功能) +- [設計外觀和整體架構](#調整使用者介面) +- 透過留言回應其他人的 [issue]({{ site.github.repository_url }}/issues) +- [直接撰寫程式碼改善 POS 系統](#怎麼提出程式碼上的異動) 遵守以下的準則,會加速整個系統開發的流程和進度。當然,我們也會提供相應的幫助,如:確認 issue 的定位、確認改善和幫助完成最終的 PR。 @@ -25,11 +25,11 @@ 除此之外,在某些情況你可能也需要改善文件: -- 有錯字。 -- 需要增加一些圖,來幫助理解。 -- 補充文件或外站連結。 -- 當你添加新的功能時,也請記得補上相關的文件。 -- 當你想要找某些資訊時,卻沒辦法在第一個尋找的地方找到,那我們就應該在那個地方補上這類文件資訊。 +- 有錯字。 +- 需要增加一些圖,來幫助理解。 +- 補充文件或外站連結。 +- 當你添加新的功能時,也請記得補上相關的文件。 +- 當你想要找某些資訊時,卻沒辦法在第一個尋找的地方找到,那我們就應該在那個地方補上這類文件資訊。 在進行[程式異動](#怎麼提出程式碼上的異動)時要注意,在執行 `git checkout -b my-branch-name` 之前,應將主分支先切到 `gh-pages`,請執行 `git checkout gh-pages` 後在執行上述指令。並且,當你完成程式異動時,請記得在合併時同樣選擇 `gh-pages` 作為要求合併的分支。 @@ -39,14 +39,14 @@ 這裡有幾個小技巧幫助你撰寫出一個好的害蟲通報文件: -- 說明明確的問題(例如,「出現錯誤」和「製作產品菜單時,若設定相同名字仍可以建立成功」)。 -- 你怎麼產生這個問題的? -- 你預期應該要有什麼結果卻得到什麼結果。 -- 確保你已經使用最新版本的應用程式。 -- 說明你使用的手機型號和版本。 -- 一隻害蟲一個 issue,若你發現兩個問題,請發兩個 issue。 -- 就算你不知道該怎麼解決這些問題,幫助其他人重現問題可以加速問題的發現。 -- 若你發現任何安全性的問題,請不要發 issue,相對的,請發信到 ,會有人來專門處理。 +- 說明明確的問題(例如,「出現錯誤」和「製作產品菜單時,若設定相同名字仍可以建立成功」)。 +- 你怎麼產生這個問題的? +- 你預期應該要有什麼結果卻得到什麼結果。 +- 確保你已經使用最新版本的應用程式。 +- 說明你使用的手機型號和版本。 +- 一隻害蟲一個 issue,若你發現兩個問題,請發兩個 issue。 +- 就算你不知道該怎麼解決這些問題,幫助其他人重現問題可以加速問題的發現。 +- 若你發現任何安全性的問題,請不要發 issue,相對的,請發信到 ,會有人來專門處理。 ## 提出新功能或改善功能 @@ -62,16 +62,16 @@ 這裡也提供幾點應注意的事項: -- 由於外觀和顏色是較為主觀的東西,每次調整,可能都會需要部分使用者的支持,例如按讚,才會考慮。 -- 請提供前後變更的相關截圖,幫助 PR 流程的進展。 -- 調整顏色應設定於定值,而非在特定元件上的變數。 +- 由於外觀和顏色是較為主觀的東西,每次調整,可能都會需要部分使用者的支持,例如按讚,才會考慮。 +- 請提供前後變更的相關截圖,幫助 PR 流程的進展。 +- 調整顏色應設定於定值,而非在特定元件上的變數。 ## 第一次嘗試貢獻 我們很開心你願意貢獻本專案。若你不確定如何開始做任何幫忙,建議你可以看看關於[good first issue]({{ site.github.repository_url }}/issues?q=is%3Aissue+label%3A%22good+first+issue%22),來看看什麼是好的 issue。除此之外[help wanted]({{ site.github.repository_url }}/issues?q=is%3Aissue+label%3A%22help+wanted%22)也是一個對於不知如何幫忙的人下手的好地方。 -- Good first issues - 應該只會包含少數幾行程式碼的修正和一組單元測試。 -- Help wanted issues - 可能會需要一些能力和經驗,但卻是一個特別需要大家幫忙的地方。 +- Good first issues - 應該只會包含少數幾行程式碼的修正和一組單元測試。 +- Help wanted issues - 可能會需要一些能力和經驗,但卻是一個特別需要大家幫忙的地方。 > 歡迎你透過 issue 或信箱提出任何問題,大家都是從初學者開始的唷 😺 @@ -79,23 +79,23 @@ 如要提出程式碼上的異動這裡有幾個建議方針去執行。 -- 若你是製作外觀上的改變,請提供截圖說明改善前後的差異。 -- 遵循 Flutter 程式碼[指南](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo)。 -- 若你是改動使用者會接觸到的功能,請記得更新相關的文件。 -- 每個 PR 應該執行一個功能或處理一個害蟲。若你有多個功能或害蟲,請提交多個 PR。 -- 不要改變和你要做的事情沒關的檔案。 -- [撰寫好的 commit 訊息](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)。 +- 若你是製作外觀上的改變,請提供截圖說明改善前後的差異。 +- 遵循 Flutter 程式碼[指南](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo)。 +- 若你是改動使用者會接觸到的功能,請記得更新相關的文件。 +- 每個 PR 應該執行一個功能或處理一個害蟲。若你有多個功能或害蟲,請提交多個 PR。 +- 不要改變和你要做的事情沒關的檔案。 +- [撰寫好的 commit 訊息](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)。 以下是執行程式碼改動的順序。 -- [Fork]({{ site.github.repository_url }}/fork) 並且複製本專案。 -- 安裝必要檔案:`flutter pub get`。 -- 安裝輔助工具:`flutter run build_runner build`。 -- 確保你本地端可以正確執行:`flutter test`。 -- 建立新的分支:`git checkout -b my-branch-name` -- 改動你要改的地方,並建立測試。 -- 推到你 fork 的專案後提交 PR:`git push -u origin my-branch-name` -- 你可以休息一下了 😆,會有人來處理你的 PR 並把他合併進主要分支。 +- [Fork]({{ site.github.repository_url }}/fork) 並且複製本專案。 +- 安裝必要檔案:`flutter pub get`。 +- 安裝輔助工具:`flutter run build_runner build`。 +- 確保你本地端可以正確執行:`flutter test`。 +- 建立新的分支:`git checkout -b my-branch-name` +- 改動你要改的地方,並建立測試。 +- 推到你 fork 的專案後提交 PR:`git push -u origin my-branch-name` +- 你可以休息一下了 😆,會有人來處理你的 PR 並把他合併進主要分支。 等不及想試試看提交你的第一個 PR 了嗎?你可以閱讀 GitHub 官方文件關於[如何貢獻 Open Source](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github)。 @@ -113,6 +113,6 @@ This project is governed by [the Contributor Covenant Code of Conduct](../CODE_O ## 其他有用資源 -- [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) -- [Using Pull Requests](https://help.github.com/articles/using-pull-requests/) -- [GitHub Help](https://help.github.com) +- [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) +- [Using Pull Requests](https://help.github.com/articles/using-pull-requests/) +- [GitHub Help](https://help.github.com) diff --git a/docs/maintenance/bump-dependencies.md b/docs/maintenance/bump-dependencies.md index 9fc80b9e..394dd14b 100644 --- a/docs/maintenance/bump-dependencies.md +++ b/docs/maintenance/bump-dependencies.md @@ -2,9 +2,9 @@ 分三種: -- dependencies,直接依賴的套件 -- dev_dependencies,開發環境依賴的套件 -- transitive,依賴套件的依賴套件 +- dependencies,直接依賴的套件 +- dev_dependencies,開發環境依賴的套件 +- transitive,依賴套件的依賴套件 ## 如何查找哪些套件需要更新 @@ -24,10 +24,10 @@ dev_package *1.0.0 *1.0.0 *1.1.0 1.1.0 但要注意幾件事: -- `Current` 代表現在的版本 -- `Upgradable` 代表依據[版本限制](https://dart.dev/tools/pub/dependencies#version-constraints)所能升級的最高版本 -- `Resolvable` 代表在和現有環境(主要是 dart/flutter 版本)不衝突的情況下可升級的最高版本 -- `Latest` 代表這個套件目前最新的版本 +- `Current` 代表現在的版本 +- `Upgradable` 代表依據[版本限制](https://dart.dev/tools/pub/dependencies#version-constraints)所能升級的最高版本 +- `Resolvable` 代表在和現有環境(主要是 dart/flutter 版本)不衝突的情況下可升級的最高版本 +- `Latest` 代表這個套件目前最新的版本 ## 如何升級 diff --git a/docs/maintenance/deployment.md b/docs/maintenance/deployment.md index 55059f62..9836fc08 100644 --- a/docs/maintenance/deployment.md +++ b/docs/maintenance/deployment.md @@ -2,24 +2,16 @@ 分為三個環境: -- `internal`:內部測試用。 -- `beta`:對外的測試,同樣的檔案會推展到 `promote_to_production`。 -- `promote_to_production`:把 `beta` 的版本推到線上。 +- `internal`:內部測試用。 +- `beta`:對外的測試,同樣的檔案會推展到 `promote_to_production`。 +- `promote_to_production`:把 `beta` 的版本推到線上。 分別的部署方式如下: -- `internal` - 1. 執行 `make bump` 後,根據想要更新的版本輸入。 -- `beta` - 1. 執行 `make bump-beta`。 -- `promote_to_production` - 1. 透過 `git pull` 把最新的 tag 拉下來; - 2. 把 GitHub 的 [draft release](https://github.com/evan361425/flutter-pos-system/releases) publish 出來。 - -確認都沒問題後,可以把舊的 tag 清掉: - -```shell -# 先清掉遠端的,再清掉本地端的,否則會沒辦法執行後續的 `xargs` -git tag | grep "$(git describe --tag --abbrev=0 | cut -d'-' -f1)-" | xargs git push --delete origin -git tag | grep "$(git describe --tag --abbrev=0 | cut -d'-' -f1)-" | xargs git tag -d -``` +- `internal` + 1. 執行 `make bump` 後,根據想要更新的版本輸入。 +- `beta` + 1. 執行 `make bump-beta`。 +- `promote_to_production` + 1. 把 GitHub 的 [draft release](https://github.com/evan361425/flutter-pos-system/releases) publish 出來。 + 2. 確認都沒問題後,可以把舊的 tag 清掉:`make clean-version` diff --git a/docs/maintenance/development.md b/docs/maintenance/development.md index 8affac9c..8b2608ce 100644 --- a/docs/maintenance/development.md +++ b/docs/maintenance/development.md @@ -6,54 +6,51 @@ 就可以安裝你需要的東西了,但如果你想要建置應用程式,你需要三個東西: -- `/android/app/.jks`,這是用來存放你的鑰匙的,確保你就是這個應用程式的擁有者,你可以這樣產生: +- `/android/.jks`,這是用來存放你的鑰匙的,確保你就是這個應用程式的擁有者,你可以這樣產生: ```bash -$ keytool -genkey -v -keystore android/my-jks.jks -alias alias_name -keyalg RSA -keysize 2048 -validity 10000 -Enter keystore password: <輸入你的密碼> -Re-enter new password: <輸入你的密碼> -What is your first and last name? - [Unknown]: -What is the name of your organizational unit? - [Unknown]: -What is the name of your organization? - [Unknown]: -What is the name of your City or Locality? - [Unknown]: -What is the name of your State or Province? - [Unknown]: -What is the two-letter country code for this unit? - [Unknown]: -Is CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown correct? - [no]: y - -Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 10,000 days - for: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown -[Storing android/app/my-jks.jks] +# 假設 為 my-jks +keytool -genkey -v -keystore android/app/my-jks.jks \ + -alias possystem \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -storepass possystem \ + -dname 'CN=possystem, OU=possystem, O=possystem, L=Unknown, ST=Unknown, C=Unknown' ``` -!!! info "位置" +- `/android/key.properties`,用來告訴建置應用程式時,你的鑰匙放哪裡,他裡面需要這些東西(依照上述產生範例): + - `keyAlias=possystem` + - `keyPassword=possystem`,keyPassword 如果你沒特別設定,預設和 storePassword 一樣 + - `storeFile=my-jks.jks` + - `storePassword=possystem` - 產生的 keystore 請放在 `/android/app/` 資料夾底下! +```bash +printf "keyAlias=%s\nkeyPassword=%s\nstoreFile=%s\nstorePassword=%s" \ + 'possystem' \ + 'possystem' \ + 'my-jks.jks' \ + 'possystem' > android/key.properties +``` -- `/android/key.properties`,用來告訴建置應用程式時,你的鑰匙放哪裡,他裡面需要這些東西(依照上述產生範例): - - `keyAlias=alias_name` - - `keyPassword=<輸入你的密碼>`,keyPassword 如果你沒特別設定,預設和 storePassword 一樣 - - `storeFile=my-jks.jks` - - `storePassword=<輸入你的密碼>` -- `/android/app/google-services.json`,你可以到 [Firebase Console](https://console.firebase.google.com/) 去產生, - 但是記得要在 *專案設定* 裡面去設定剛剛產生的金鑰 SHA 憑證指紋,你可以這樣輸出: +- `/android/app/google-services.json`, + 你可以到 [Firebase Console](https://console.firebase.google.com/) 去產生, + 但是記得要在 *專案設定* 裡面去設定剛剛產生的金鑰 SHA 憑證指紋,你可以這樣輸出: ```bash -$ keytool -list -keystore android/my-jks.jks -alias alias_name -Enter keystore password: -alias_name, Dec 18, 2022, PrivateKeyEntry, -Certificate fingerprint (SHA-256): B4:D1:3E:F5:8A:4C:20:07:30:16:4A:01:59:4A:4F:01:39:2C:62:C7:6B:EB:2B:89:3D:48:63:4D:59:D8:A1:9C +$ keytool -list -keystore android/app/my-jks.jks --storepass possystem +Keystore type: PKCS12 +Keystore provider: SUN + +Your keystore contains 1 entry + +mykey, May 18, 2024, PrivateKeyEntry, +Certificate fingerprint (SHA-256): 6F:14:57:54:CC:26:0A:4C:70:E3:28:1D:CE:D0:73:3F:72:19:49:96:8F:9A:1B:31:A5:E2:96:E4:44:14:E1:A1 ``` -- 最後請更改 `/lib/firebase_compatible_options.dart` 裡面最下面的 `androidDebug` ID。 - 你可以 `dart pub global activate flutterfire_cli` 安裝指令套件後 `flutterfire configure`, - 然後把產生的檔案的設定資訊複製到 `firebase_compatible_options.dart` 中。 +- 最後請更改 `/lib/firebase_compatible_options.dart` 裡面最下面的 `androidDebug` 設定。 + 你可以 `dart pub global activate flutterfire_cli` 安裝指令套件後 `flutterfire configure`, + 然後把產生的檔案的設定資訊複製到 `firebase_compatible_options.dart` 中。 ## 測試 @@ -62,6 +59,7 @@ Certificate fingerprint (SHA-256): B4:D1:3E:F5:8A:4C:20:07:30:16:4A:01:59:4A:4F: make mock 當想要開始測試,你可以: + make test 如果想要測試加上 coverage,你可以: diff --git a/docs/untranslated.json b/docs/untranslated.json index bc9dcdea..9e26dfee 100644 --- a/docs/untranslated.json +++ b/docs/untranslated.json @@ -1,1040 +1 @@ -{ - "en": [ - "actSuccess", - "actError", - "btnImport", - "btnExport", - "singleChoice", - "multiChoices", - "totalCount", - "searchCount", - "dialogDeletionContent", - "emptyBodyContent", - "invalidNumberType", - "invalidIntegerType", - "invalidPositiveNumber", - "invalidNumberMaximum", - "invalidNumberMinimum", - "invalidEmptyString", - "invalidStringMaximum", - "analysisCalendarMonth", - "analysisCalendarWeek", - "analysisCalendarTwoWeek", - "analysisOrderListItemMetaPrice", - "analysisOrderListItemMetaPaid", - "analysisOrderListItemMetaIncome", - "orderListEmpty", - "orderListMetaPrice", - "orderListMetaCount", - "menuSearchProductHint", - "menuSearchProductNotFound", - "menuCatalogListEmptyProduct", - "menuCatalogCreate", - "menuCatalogUpdate", - "menuCatalogReorder", - "menuCatalogMetaCreatedAt", - "menuCatalogEmptyBody", - "menuCatalogDialogDeletionContent", - "menuCatalogNameLabel", - "menuCatalogNameHint", - "menuCatalogNameRepeatError", - "menuProductMetaPrice", - "menuProductMetaCost", - "menuProductListEmptyIngredient", - "menuProductCreate", - "menuProductUpdate", - "menuProductReorder", - "menuProductEmptyBody", - "menuProductNameLabel", - "menuProductNameHint", - "menuProductNameRepeatError", - "menuProductPriceLabel", - "menuProductPriceHint", - "menuProductCostLabel", - "menuProductCostHint", - "menuIngredientMetaAmount", - "menuIngredientCreate", - "menuIngredientUpdate", - "menuIngredientSearchLabel", - "menuIngredientSearchAdd", - "menuIngredientSearchHint", - "menuIngredientSearchEmptyError", - "menuIngredientSearchHelper", - "menuIngredientRepeatError", - "menuIngredientAmountLabel", - "menuIngredientAmountHelper", - "menuQuantityMetaAmount", - "menuQuantityMetaPrice", - "menuQuantityMetaCost", - "menuQuantityCreate", - "menuQuantitySearchLabel", - "menuQuantitySearchAdd", - "menuQuantitySearchHint", - "menuQuantitySearchEmptyError", - "menuQuantitySearchHelper", - "menuQuantityRepeatError", - "menuQuantityAmountLabel", - "menuQuantityAdditionalPriceLabel", - "menuQuantityAdditionalPriceHelper", - "menuQuantityAdditionalCostLabel", - "menuQuantityAdditionalCostHelper", - "stockHasNotReplenishEver", - "stockUpdatedAt", - "stockIngredientCreate", - "stockIngredientDialogDeletionContent", - "stockIngredientConnectedProductsCount", - "stockIngredientAddTutorialTitle", - "stockIngredientAddTutorialMessage", - "stockIngredientNameLabel", - "stockIngredientNameHint", - "stockIngredientNameRepeatError", - "stockIngredientAmountLabel", - "stockIngredientTotalAmountLabel", - "stockIngredientTotalAmountHelper", - "stockReplenishmentSubtitle", - "stockReplenishmentButton", - "stockReplenishmentTutorialTitle", - "stockReplenishmentTutorialMessage", - "stockReplenishmentApplyConfirmContent", - "stockReplenishmentCreate", - "stockReplenishmentNameLabel", - "stockReplenishmentNameHint", - "stockReplenishmentNameRepeatError", - "stockReplenishmentIngredientAmountHint", - "quantityMetaProportion", - "quantityDialogDeletionContent", - "quantityCreate", - "quantityNameLabel", - "quantityNameHint", - "quantityNameRepeatError", - "quantityProportionLabel", - "quantityProportionHelper", - "settingCheckoutWarningTitle", - "settingCheckoutWarningTypes", - "settingOrderOutlookTypes", - "orderCartToggleSelection", - "orderCartActionsBtn", - "orderCartActionsDiscount", - "orderCartActionsDiscountLabel", - "orderCartActionsDiscountHint", - "orderCartActionsDiscountHelper", - "orderCartActionsDiscountSuffix", - "orderCartActionsChangePrice", - "orderCartActionsChangePriceLabel", - "orderCartActionsChangePriceHint", - "orderCartActionsChangePriceSuffix", - "orderCartActionsChangeCount", - "orderCartActionsChangeCountLabel", - "orderCartActionsChangeCountHint", - "orderCartActionsChangeCountSuffix", - "orderCartActionsFree", - "orderCartActionsDelete", - "orderCartEmptyCatalog", - "orderCartItemPrice", - "orderCartIngredientStatus", - "orderCartQuantityNotAble", - "orderCartQuantityDefault", - "orderCashierDefaultButton", - "orderCashierDefaultTutorialTitle", - "orderCashierDefaultTutorialMessage", - "orderCashierChangeButton", - "orderCashierChangeTutorialTitle", - "orderCashierChangeTutorialMessage", - "orderCashierSurplusButton", - "orderCashierSurplusTutorialTitle", - "orderCashierSurplusTutorialMessage", - "orderCashierPaidFailed", - "orderCashierPaidNotEnough", - "orderCashierPaidUsingSmallMoney", - "orderCashierPaidUsingSmallMoneyAction", - "orderCashierPaidUsingSmallMoneyHint", - "orderCashierPaidConfirmLeaveHistoryMode", - "orderCashierPaidLabel", - "orderCashierChangeLabel", - "orderCashierCalculatorChangeNotEnough", - "orderCashierSnapshotChangeField", - "orderProductIngredientDefaultName", - "orderProductIngredientName", - "orderObjectChange", - "orderObjectTotalPrice", - "orderObjectProductsPrice", - "orderObjectAttributesPrice", - "orderObjectProductsCost", - "orderObjectRevenue", - "orderObjectPaid", - "orderObjectProductPrice", - "orderObjectProductCost", - "orderObjectProductCount", - "orderObjectProductSinglePrice", - "orderObjectProductOriginalPrice", - "orderObjectProductCatalog", - "orderObjectProductIngredient", - "transitDescription", - "transitMethod", - "transitPreviewExportTitle", - "transitBasicTitle", - "transitOrderTitle", - "transitType", - "transitGSDescription", - "transitGSErrors", - "transitGSSpreadsheetLabel", - "transitGSSheetLabel", - "transitGSProgressStatus", - "transitGSUpdateModelStatus", - "transitProductIngredientInfoTitle", - "transitReplenishmentTitle", - "transitOrderAttributeOptionTitle", - "transitProductIngredientInfoGSNote", - "transitReplenishmentGSNote", - "transitOrderAttributeOptionGSNote", - "transitGSImportError", - "transitPreviewImportTitle", - "transitImportColumnsCountError", - "transitImportColumnStatus", - "analysisChartCreate", - "analysisChartNameLabel", - "analysisChartIgnoreEmptyLabel", - "analysisChartIgnoreEmptyHelper", - "analysisChartTypeLabel", - "analysisChartType", - "analysisChartDataPropertiesDivider", - "analysisChartMetricLabel", - "analysisChartMetricHelper", - "analysisChartMetric", - "analysisChartTargetLabel", - "analysisChartTargetHelper", - "analysisChartTargetError", - "analysisChartTarget", - "analysisChartTargetItemLabel", - "analysisChartTargetItemHelper", - "analysisChartTargetItemSelectAll" - ], - - "zh_Hant": [ - "appTitle", - "actSuccess", - "actError", - "btnImport", - "btnExport", - "singleChoice", - "multiChoices", - "totalCount", - "searchCount", - "dialogDeletionTitle", - "dialogDeletionContent", - "emptyBodyContent", - "invalidNumberType", - "invalidIntegerType", - "invalidPositiveNumber", - "invalidNumberMaximum", - "invalidNumberMinimum", - "invalidEmptyString", - "invalidStringMaximum", - "homeTabAnalysis", - "homeTabStock", - "homeTabCashier", - "homeTabSetting", - "analysisCalendarMonth", - "analysisCalendarWeek", - "analysisCalendarTwoWeek", - "analysisOrderListItemMetaPrice", - "analysisOrderListItemMetaPaid", - "analysisOrderListItemMetaIncome", - "orderListEmpty", - "orderListMetaPrice", - "orderListMetaCount", - "menuTitle", - "menuSearchProductHint", - "menuSearchProductNotFound", - "menuCatalogListEmptyProduct", - "menuCatalogCreate", - "menuCatalogUpdate", - "menuCatalogReorder", - "menuCatalogMetaTitle", - "menuCatalogMetaCreatedAt", - "menuCatalogEmptyBody", - "menuCatalogDialogDeletionContent", - "menuCatalogNameLabel", - "menuCatalogNameHint", - "menuCatalogNameRepeatError", - "menuProductMetaTitle", - "menuProductMetaPrice", - "menuProductMetaCost", - "menuProductListEmptyIngredient", - "menuProductCreate", - "menuProductUpdate", - "menuProductReorder", - "menuProductEmptyBody", - "menuProductNameLabel", - "menuProductNameHint", - "menuProductNameRepeatError", - "menuProductPriceLabel", - "menuProductPriceHint", - "menuProductCostLabel", - "menuProductCostHint", - "menuIngredientMetaAmount", - "menuIngredientCreate", - "menuIngredientUpdate", - "menuIngredientSearchLabel", - "menuIngredientSearchAdd", - "menuIngredientSearchHint", - "menuIngredientSearchEmptyError", - "menuIngredientSearchHelper", - "menuIngredientRepeatError", - "menuIngredientAmountLabel", - "menuIngredientAmountHelper", - "menuQuantityMetaAmount", - "menuQuantityMetaPrice", - "menuQuantityMetaCost", - "menuQuantityCreate", - "menuQuantitySearchLabel", - "menuQuantitySearchAdd", - "menuQuantitySearchHint", - "menuQuantitySearchEmptyError", - "menuQuantitySearchHelper", - "menuQuantityRepeatError", - "menuQuantityAmountLabel", - "menuQuantityAdditionalPriceLabel", - "menuQuantityAdditionalPriceHelper", - "menuQuantityAdditionalCostLabel", - "menuQuantityAdditionalCostHelper", - "stockHasNotReplenishEver", - "stockUpdatedAt", - "stockIngredientCreate", - "stockIngredientDialogDeletionContent", - "stockIngredientConnectedProductsCount", - "stockIngredientAddTutorialTitle", - "stockIngredientAddTutorialMessage", - "stockIngredientNameLabel", - "stockIngredientNameHint", - "stockIngredientNameRepeatError", - "stockIngredientAmountLabel", - "stockIngredientTotalAmountLabel", - "stockIngredientTotalAmountHelper", - "stockReplenishmentTitle", - "stockReplenishmentSubtitle", - "stockReplenishmentButton", - "stockReplenishmentTutorialTitle", - "stockReplenishmentTutorialMessage", - "stockReplenishmentApplyConfirmTitle", - "stockReplenishmentApplyConfirmContent", - "stockReplenishmentIngredientListTitle", - "stockReplenishmentCreate", - "stockReplenishmentNameLabel", - "stockReplenishmentNameHint", - "stockReplenishmentNameRepeatError", - "stockReplenishmentIngredientAmountHint", - "quantityTitle", - "quantityMetaProportion", - "quantityDialogDeletionContent", - "quantityCreate", - "quantityNameLabel", - "quantityNameHint", - "quantityNameRepeatError", - "quantityProportionLabel", - "quantityProportionHelper", - "featureRequestTitle", - "settingTitle", - "settingThemeTitle", - "settingThemeTypes", - "settingLanguageTitle", - "settingCheckoutWarningTitle", - "settingCheckoutWarningTypes", - "settingOrderOutlookTitle", - "settingOrderOutlookTypes", - "settingOrderAwakeningTitle", - "orderAttributeTitle", - "orderAttributeHint", - "orderAttributeCreate", - "orderAttributeUpdate", - "orderAttributeReorder", - "orderAttributeMetaMode", - "orderAttributeMetaNoDefault", - "orderAttributeMetaDefault", - "orderAttributeModeNames", - "orderAttributeModeDescriptions", - "orderAttributeNameLabel", - "orderAttributeNameHint", - "orderAttributeNameRepeatError", - "orderAttributeModeTitle", - "orderAttributeOptionCreate", - "orderAttributeOptionIsDefault", - "orderAttributeOptionReorder", - "orderAttributeOptionCreateTitle", - "orderAttributeOptionNameLabel", - "orderAttributeOptionNameHelper", - "orderAttributeOptionNameRepeatError", - "orderAttributeOptionsModeHelper", - "orderAttributeOptionsModeHint", - "orderAttributeOptionSetToDefault", - "orderAttributeOptionConfirmChangeDefaultTitle", - "orderAttributeOptionConfirmChangeDefaultContent", - "orderMetaTotalPrice", - "orderMetaTotalCount", - "orderActionsCheckout", - "orderActionsOpenChanger", - "orderActionsLeaveHistoryMode", - "orderActionsShowLastOrder", - "orderActionsShowLastOrderNotFound", - "orderActionsStash", - "orderActionsLeave", - "orderCartSnapshotTutorialTitle", - "orderCartSnapshotTutorialMessage", - "orderCartSnapshotEmpty", - "orderCartToggleSelection", - "orderCartActionsBtn", - "orderCartActionsDiscount", - "orderCartActionsDiscountLabel", - "orderCartActionsDiscountHint", - "orderCartActionsDiscountHelper", - "orderCartActionsDiscountSuffix", - "orderCartActionsChangePrice", - "orderCartActionsChangePriceLabel", - "orderCartActionsChangePriceHint", - "orderCartActionsChangePriceSuffix", - "orderCartActionsChangeCount", - "orderCartActionsChangeCountLabel", - "orderCartActionsChangeCountHint", - "orderCartActionsChangeCountSuffix", - "orderCartActionsFree", - "orderCartActionsDelete", - "orderCartEmptyCatalog", - "orderCartItemPrice", - "orderCartIngredientStatus", - "orderCartQuantityNotAble", - "orderCartQuantityDefault", - "orderSetAttributeTitle", - "orderCashierDefaultButton", - "orderCashierDefaultTutorialTitle", - "orderCashierDefaultTutorialMessage", - "orderCashierChangeButton", - "orderCashierChangeTutorialTitle", - "orderCashierChangeTutorialMessage", - "orderCashierSurplusButton", - "orderCashierSurplusTutorialTitle", - "orderCashierSurplusTutorialMessage", - "orderCashierTitle", - "orderCashierPaidFailed", - "orderCashierPaidNotEnough", - "orderCashierPaidUsingSmallMoney", - "orderCashierPaidUsingSmallMoneyAction", - "orderCashierPaidUsingSmallMoneyHint", - "orderCashierPaidConfirmLeaveHistoryMode", - "orderCashierPaidLabel", - "orderCashierChangeLabel", - "orderCashierCalculatorChangeNotEnough", - "orderCashierSnapshotChangeField", - "orderProductIngredientDefaultName", - "orderProductIngredientName", - "orderObjectChange", - "orderObjectTotalPrice", - "orderObjectProductsPrice", - "orderObjectAttributesPrice", - "orderObjectProductsCost", - "orderObjectRevenue", - "orderObjectPaid", - "orderObjectAttributeTitle", - "orderObjectAttributeCount", - "orderObjectProductsCount", - "orderObjectProductTitle", - "orderObjectProductPrice", - "orderObjectProductCost", - "orderObjectProductCount", - "orderObjectProductSinglePrice", - "orderObjectProductOriginalPrice", - "orderObjectProductCatalog", - "orderObjectProductIngredient", - "transitTitle", - "transitDescription", - "transitMethod", - "transitPreviewExportTitle", - "transitBasicTitle", - "transitOrderTitle", - "transitType", - "transitGSDescription", - "transitGSErrors", - "transitGSSpreadsheetLabel", - "transitGSSheetLabel", - "transitGSProgressStatus", - "transitGSUpdateModelStatus", - "transitProductIngredientInfoTitle", - "transitReplenishmentTitle", - "transitOrderAttributeOptionTitle", - "transitProductIngredientInfoGSNote", - "transitReplenishmentGSNote", - "transitOrderAttributeOptionGSNote", - "transitGSImportError", - "transitPreviewImportTitle", - "transitImportColumnsCountError", - "transitImportColumnStatus", - "analysisChartCreate", - "analysisChartNameLabel", - "analysisChartIgnoreEmptyLabel", - "analysisChartIgnoreEmptyHelper", - "analysisChartTypeLabel", - "analysisChartType", - "analysisChartDataPropertiesDivider", - "analysisChartMetricLabel", - "analysisChartMetricHelper", - "analysisChartMetric", - "analysisChartTargetLabel", - "analysisChartTargetHelper", - "analysisChartTargetError", - "analysisChartTarget", - "analysisChartTargetItemLabel", - "analysisChartTargetItemHelper", - "analysisChartTargetItemSelectAll" - ], - - "zh_Hant_TW": [ - "appTitle", - "actSuccess", - "actError", - "btnImport", - "btnExport", - "singleChoice", - "multiChoices", - "totalCount", - "searchCount", - "dialogDeletionTitle", - "dialogDeletionContent", - "emptyBodyContent", - "invalidNumberType", - "invalidIntegerType", - "invalidPositiveNumber", - "invalidNumberMaximum", - "invalidNumberMinimum", - "invalidEmptyString", - "invalidStringMaximum", - "homeTabAnalysis", - "homeTabStock", - "homeTabCashier", - "homeTabSetting", - "analysisCalendarMonth", - "analysisCalendarWeek", - "analysisCalendarTwoWeek", - "analysisOrderListItemMetaPrice", - "analysisOrderListItemMetaPaid", - "analysisOrderListItemMetaIncome", - "orderListEmpty", - "orderListMetaPrice", - "orderListMetaCount", - "menuTitle", - "menuSearchProductHint", - "menuSearchProductNotFound", - "menuCatalogListEmptyProduct", - "menuCatalogCreate", - "menuCatalogUpdate", - "menuCatalogReorder", - "menuCatalogMetaTitle", - "menuCatalogMetaCreatedAt", - "menuCatalogEmptyBody", - "menuCatalogDialogDeletionContent", - "menuCatalogNameLabel", - "menuCatalogNameHint", - "menuCatalogNameRepeatError", - "menuProductMetaTitle", - "menuProductMetaPrice", - "menuProductMetaCost", - "menuProductListEmptyIngredient", - "menuProductCreate", - "menuProductUpdate", - "menuProductReorder", - "menuProductEmptyBody", - "menuProductNameLabel", - "menuProductNameHint", - "menuProductNameRepeatError", - "menuProductPriceLabel", - "menuProductPriceHint", - "menuProductCostLabel", - "menuProductCostHint", - "menuIngredientMetaAmount", - "menuIngredientCreate", - "menuIngredientUpdate", - "menuIngredientSearchLabel", - "menuIngredientSearchAdd", - "menuIngredientSearchHint", - "menuIngredientSearchEmptyError", - "menuIngredientSearchHelper", - "menuIngredientRepeatError", - "menuIngredientAmountLabel", - "menuIngredientAmountHelper", - "menuQuantityMetaAmount", - "menuQuantityMetaPrice", - "menuQuantityMetaCost", - "menuQuantityCreate", - "menuQuantitySearchLabel", - "menuQuantitySearchAdd", - "menuQuantitySearchHint", - "menuQuantitySearchEmptyError", - "menuQuantitySearchHelper", - "menuQuantityRepeatError", - "menuQuantityAmountLabel", - "menuQuantityAdditionalPriceLabel", - "menuQuantityAdditionalPriceHelper", - "menuQuantityAdditionalCostLabel", - "menuQuantityAdditionalCostHelper", - "stockHasNotReplenishEver", - "stockUpdatedAt", - "stockIngredientCreate", - "stockIngredientDialogDeletionContent", - "stockIngredientConnectedProductsCount", - "stockIngredientAddTutorialTitle", - "stockIngredientAddTutorialMessage", - "stockIngredientNameLabel", - "stockIngredientNameHint", - "stockIngredientNameRepeatError", - "stockIngredientAmountLabel", - "stockIngredientTotalAmountLabel", - "stockIngredientTotalAmountHelper", - "stockReplenishmentTitle", - "stockReplenishmentSubtitle", - "stockReplenishmentButton", - "stockReplenishmentTutorialTitle", - "stockReplenishmentTutorialMessage", - "stockReplenishmentApplyConfirmTitle", - "stockReplenishmentApplyConfirmContent", - "stockReplenishmentIngredientListTitle", - "stockReplenishmentCreate", - "stockReplenishmentNameLabel", - "stockReplenishmentNameHint", - "stockReplenishmentNameRepeatError", - "stockReplenishmentIngredientAmountHint", - "quantityTitle", - "quantityMetaProportion", - "quantityDialogDeletionContent", - "quantityCreate", - "quantityNameLabel", - "quantityNameHint", - "quantityNameRepeatError", - "quantityProportionLabel", - "quantityProportionHelper", - "featureRequestTitle", - "settingTitle", - "settingThemeTitle", - "settingThemeTypes", - "settingLanguageTitle", - "settingCheckoutWarningTitle", - "settingCheckoutWarningTypes", - "settingOrderOutlookTitle", - "settingOrderOutlookTypes", - "settingOrderAwakeningTitle", - "orderAttributeTitle", - "orderAttributeHint", - "orderAttributeCreate", - "orderAttributeUpdate", - "orderAttributeReorder", - "orderAttributeMetaMode", - "orderAttributeMetaNoDefault", - "orderAttributeMetaDefault", - "orderAttributeModeNames", - "orderAttributeModeDescriptions", - "orderAttributeNameLabel", - "orderAttributeNameHint", - "orderAttributeNameRepeatError", - "orderAttributeModeTitle", - "orderAttributeOptionCreate", - "orderAttributeOptionIsDefault", - "orderAttributeOptionReorder", - "orderAttributeOptionCreateTitle", - "orderAttributeOptionNameLabel", - "orderAttributeOptionNameHelper", - "orderAttributeOptionNameRepeatError", - "orderAttributeOptionsModeHelper", - "orderAttributeOptionsModeHint", - "orderAttributeOptionSetToDefault", - "orderAttributeOptionConfirmChangeDefaultTitle", - "orderAttributeOptionConfirmChangeDefaultContent", - "orderMetaTotalPrice", - "orderMetaTotalCount", - "orderActionsCheckout", - "orderActionsOpenChanger", - "orderActionsLeaveHistoryMode", - "orderActionsShowLastOrder", - "orderActionsShowLastOrderNotFound", - "orderActionsStash", - "orderActionsLeave", - "orderCartSnapshotTutorialTitle", - "orderCartSnapshotTutorialMessage", - "orderCartSnapshotEmpty", - "orderCartToggleSelection", - "orderCartActionsBtn", - "orderCartActionsDiscount", - "orderCartActionsDiscountLabel", - "orderCartActionsDiscountHint", - "orderCartActionsDiscountHelper", - "orderCartActionsDiscountSuffix", - "orderCartActionsChangePrice", - "orderCartActionsChangePriceLabel", - "orderCartActionsChangePriceHint", - "orderCartActionsChangePriceSuffix", - "orderCartActionsChangeCount", - "orderCartActionsChangeCountLabel", - "orderCartActionsChangeCountHint", - "orderCartActionsChangeCountSuffix", - "orderCartActionsFree", - "orderCartActionsDelete", - "orderCartEmptyCatalog", - "orderCartItemPrice", - "orderCartIngredientStatus", - "orderCartQuantityNotAble", - "orderCartQuantityDefault", - "orderSetAttributeTitle", - "orderCashierDefaultButton", - "orderCashierDefaultTutorialTitle", - "orderCashierDefaultTutorialMessage", - "orderCashierChangeButton", - "orderCashierChangeTutorialTitle", - "orderCashierChangeTutorialMessage", - "orderCashierSurplusButton", - "orderCashierSurplusTutorialTitle", - "orderCashierSurplusTutorialMessage", - "orderCashierTitle", - "orderCashierPaidFailed", - "orderCashierPaidNotEnough", - "orderCashierPaidUsingSmallMoney", - "orderCashierPaidUsingSmallMoneyAction", - "orderCashierPaidUsingSmallMoneyHint", - "orderCashierPaidConfirmLeaveHistoryMode", - "orderCashierPaidLabel", - "orderCashierChangeLabel", - "orderCashierCalculatorChangeNotEnough", - "orderCashierSnapshotChangeField", - "orderProductIngredientDefaultName", - "orderProductIngredientName", - "orderObjectChange", - "orderObjectTotalPrice", - "orderObjectProductsPrice", - "orderObjectAttributesPrice", - "orderObjectProductsCost", - "orderObjectRevenue", - "orderObjectPaid", - "orderObjectAttributeTitle", - "orderObjectAttributeCount", - "orderObjectProductsCount", - "orderObjectProductTitle", - "orderObjectProductPrice", - "orderObjectProductCost", - "orderObjectProductCount", - "orderObjectProductSinglePrice", - "orderObjectProductOriginalPrice", - "orderObjectProductCatalog", - "orderObjectProductIngredient", - "transitTitle", - "transitDescription", - "transitMethod", - "transitPreviewExportTitle", - "transitBasicTitle", - "transitOrderTitle", - "transitType", - "transitGSDescription", - "transitGSErrors", - "transitGSSpreadsheetLabel", - "transitGSSheetLabel", - "transitGSProgressStatus", - "transitGSUpdateModelStatus", - "transitProductIngredientInfoTitle", - "transitReplenishmentTitle", - "transitOrderAttributeOptionTitle", - "transitProductIngredientInfoGSNote", - "transitReplenishmentGSNote", - "transitOrderAttributeOptionGSNote", - "transitGSImportError", - "transitPreviewImportTitle", - "transitImportColumnsCountError", - "transitImportColumnStatus", - "analysisChartCreate", - "analysisChartNameLabel", - "analysisChartIgnoreEmptyLabel", - "analysisChartIgnoreEmptyHelper", - "analysisChartTypeLabel", - "analysisChartType", - "analysisChartDataPropertiesDivider", - "analysisChartMetricLabel", - "analysisChartMetricHelper", - "analysisChartMetric", - "analysisChartTargetLabel", - "analysisChartTargetHelper", - "analysisChartTargetError", - "analysisChartTarget", - "analysisChartTargetItemLabel", - "analysisChartTargetItemHelper", - "analysisChartTargetItemSelectAll" - ], - - "zh_TW": [ - "appTitle", - "actSuccess", - "actError", - "btnImport", - "btnExport", - "singleChoice", - "multiChoices", - "totalCount", - "searchCount", - "dialogDeletionTitle", - "dialogDeletionContent", - "emptyBodyContent", - "invalidNumberType", - "invalidIntegerType", - "invalidPositiveNumber", - "invalidNumberMaximum", - "invalidNumberMinimum", - "invalidEmptyString", - "invalidStringMaximum", - "homeTabAnalysis", - "homeTabStock", - "homeTabCashier", - "homeTabSetting", - "analysisCalendarMonth", - "analysisCalendarWeek", - "analysisCalendarTwoWeek", - "analysisOrderListItemMetaPrice", - "analysisOrderListItemMetaPaid", - "analysisOrderListItemMetaIncome", - "orderListEmpty", - "orderListMetaPrice", - "orderListMetaCount", - "menuTitle", - "menuSearchProductHint", - "menuSearchProductNotFound", - "menuCatalogListEmptyProduct", - "menuCatalogCreate", - "menuCatalogUpdate", - "menuCatalogReorder", - "menuCatalogMetaTitle", - "menuCatalogMetaCreatedAt", - "menuCatalogEmptyBody", - "menuCatalogDialogDeletionContent", - "menuCatalogNameLabel", - "menuCatalogNameHint", - "menuCatalogNameRepeatError", - "menuProductMetaTitle", - "menuProductMetaPrice", - "menuProductMetaCost", - "menuProductListEmptyIngredient", - "menuProductCreate", - "menuProductUpdate", - "menuProductReorder", - "menuProductEmptyBody", - "menuProductNameLabel", - "menuProductNameHint", - "menuProductNameRepeatError", - "menuProductPriceLabel", - "menuProductPriceHint", - "menuProductCostLabel", - "menuProductCostHint", - "menuIngredientMetaAmount", - "menuIngredientCreate", - "menuIngredientUpdate", - "menuIngredientSearchLabel", - "menuIngredientSearchAdd", - "menuIngredientSearchHint", - "menuIngredientSearchEmptyError", - "menuIngredientSearchHelper", - "menuIngredientRepeatError", - "menuIngredientAmountLabel", - "menuIngredientAmountHelper", - "menuQuantityMetaAmount", - "menuQuantityMetaPrice", - "menuQuantityMetaCost", - "menuQuantityCreate", - "menuQuantitySearchLabel", - "menuQuantitySearchAdd", - "menuQuantitySearchHint", - "menuQuantitySearchEmptyError", - "menuQuantitySearchHelper", - "menuQuantityRepeatError", - "menuQuantityAmountLabel", - "menuQuantityAdditionalPriceLabel", - "menuQuantityAdditionalPriceHelper", - "menuQuantityAdditionalCostLabel", - "menuQuantityAdditionalCostHelper", - "stockHasNotReplenishEver", - "stockUpdatedAt", - "stockIngredientCreate", - "stockIngredientDialogDeletionContent", - "stockIngredientConnectedProductsCount", - "stockIngredientAddTutorialTitle", - "stockIngredientAddTutorialMessage", - "stockIngredientNameLabel", - "stockIngredientNameHint", - "stockIngredientNameRepeatError", - "stockIngredientAmountLabel", - "stockIngredientTotalAmountLabel", - "stockIngredientTotalAmountHelper", - "stockReplenishmentTitle", - "stockReplenishmentSubtitle", - "stockReplenishmentButton", - "stockReplenishmentTutorialTitle", - "stockReplenishmentTutorialMessage", - "stockReplenishmentApplyConfirmTitle", - "stockReplenishmentApplyConfirmContent", - "stockReplenishmentIngredientListTitle", - "stockReplenishmentCreate", - "stockReplenishmentNameLabel", - "stockReplenishmentNameHint", - "stockReplenishmentNameRepeatError", - "stockReplenishmentIngredientAmountHint", - "quantityTitle", - "quantityMetaProportion", - "quantityDialogDeletionContent", - "quantityCreate", - "quantityNameLabel", - "quantityNameHint", - "quantityNameRepeatError", - "quantityProportionLabel", - "quantityProportionHelper", - "featureRequestTitle", - "settingTitle", - "settingThemeTitle", - "settingThemeTypes", - "settingLanguageTitle", - "settingCheckoutWarningTitle", - "settingCheckoutWarningTypes", - "settingOrderOutlookTitle", - "settingOrderOutlookTypes", - "settingOrderAwakeningTitle", - "orderAttributeTitle", - "orderAttributeHint", - "orderAttributeCreate", - "orderAttributeUpdate", - "orderAttributeReorder", - "orderAttributeMetaMode", - "orderAttributeMetaNoDefault", - "orderAttributeMetaDefault", - "orderAttributeModeNames", - "orderAttributeModeDescriptions", - "orderAttributeNameLabel", - "orderAttributeNameHint", - "orderAttributeNameRepeatError", - "orderAttributeModeTitle", - "orderAttributeOptionCreate", - "orderAttributeOptionIsDefault", - "orderAttributeOptionReorder", - "orderAttributeOptionCreateTitle", - "orderAttributeOptionNameLabel", - "orderAttributeOptionNameHelper", - "orderAttributeOptionNameRepeatError", - "orderAttributeOptionsModeHelper", - "orderAttributeOptionsModeHint", - "orderAttributeOptionSetToDefault", - "orderAttributeOptionConfirmChangeDefaultTitle", - "orderAttributeOptionConfirmChangeDefaultContent", - "orderMetaTotalPrice", - "orderMetaTotalCount", - "orderActionsCheckout", - "orderActionsOpenChanger", - "orderActionsLeaveHistoryMode", - "orderActionsShowLastOrder", - "orderActionsShowLastOrderNotFound", - "orderActionsStash", - "orderActionsLeave", - "orderCartSnapshotTutorialTitle", - "orderCartSnapshotTutorialMessage", - "orderCartSnapshotEmpty", - "orderCartToggleSelection", - "orderCartActionsBtn", - "orderCartActionsDiscount", - "orderCartActionsDiscountLabel", - "orderCartActionsDiscountHint", - "orderCartActionsDiscountHelper", - "orderCartActionsDiscountSuffix", - "orderCartActionsChangePrice", - "orderCartActionsChangePriceLabel", - "orderCartActionsChangePriceHint", - "orderCartActionsChangePriceSuffix", - "orderCartActionsChangeCount", - "orderCartActionsChangeCountLabel", - "orderCartActionsChangeCountHint", - "orderCartActionsChangeCountSuffix", - "orderCartActionsFree", - "orderCartActionsDelete", - "orderCartEmptyCatalog", - "orderCartItemPrice", - "orderCartIngredientStatus", - "orderCartQuantityNotAble", - "orderCartQuantityDefault", - "orderSetAttributeTitle", - "orderCashierDefaultButton", - "orderCashierDefaultTutorialTitle", - "orderCashierDefaultTutorialMessage", - "orderCashierChangeButton", - "orderCashierChangeTutorialTitle", - "orderCashierChangeTutorialMessage", - "orderCashierSurplusButton", - "orderCashierSurplusTutorialTitle", - "orderCashierSurplusTutorialMessage", - "orderCashierTitle", - "orderCashierPaidFailed", - "orderCashierPaidNotEnough", - "orderCashierPaidUsingSmallMoney", - "orderCashierPaidUsingSmallMoneyAction", - "orderCashierPaidUsingSmallMoneyHint", - "orderCashierPaidConfirmLeaveHistoryMode", - "orderCashierPaidLabel", - "orderCashierChangeLabel", - "orderCashierCalculatorChangeNotEnough", - "orderCashierSnapshotChangeField", - "orderProductIngredientDefaultName", - "orderProductIngredientName", - "orderObjectChange", - "orderObjectTotalPrice", - "orderObjectProductsPrice", - "orderObjectAttributesPrice", - "orderObjectProductsCost", - "orderObjectRevenue", - "orderObjectPaid", - "orderObjectAttributeTitle", - "orderObjectAttributeCount", - "orderObjectProductsCount", - "orderObjectProductTitle", - "orderObjectProductPrice", - "orderObjectProductCost", - "orderObjectProductCount", - "orderObjectProductSinglePrice", - "orderObjectProductOriginalPrice", - "orderObjectProductCatalog", - "orderObjectProductIngredient", - "transitTitle", - "transitDescription", - "transitMethod", - "transitPreviewExportTitle", - "transitBasicTitle", - "transitOrderTitle", - "transitType", - "transitGSDescription", - "transitGSErrors", - "transitGSSpreadsheetLabel", - "transitGSSheetLabel", - "transitGSProgressStatus", - "transitGSUpdateModelStatus", - "transitProductIngredientInfoTitle", - "transitReplenishmentTitle", - "transitOrderAttributeOptionTitle", - "transitProductIngredientInfoGSNote", - "transitReplenishmentGSNote", - "transitOrderAttributeOptionGSNote", - "transitGSImportError", - "transitPreviewImportTitle", - "transitImportColumnsCountError", - "transitImportColumnStatus", - "analysisChartCreate", - "analysisChartNameLabel", - "analysisChartIgnoreEmptyLabel", - "analysisChartIgnoreEmptyHelper", - "analysisChartTypeLabel", - "analysisChartType", - "analysisChartDataPropertiesDivider", - "analysisChartMetricLabel", - "analysisChartMetricHelper", - "analysisChartMetric", - "analysisChartTargetLabel", - "analysisChartTargetHelper", - "analysisChartTargetError", - "analysisChartTarget", - "analysisChartTargetItemLabel", - "analysisChartTargetItemHelper", - "analysisChartTargetItemSelectAll" - ] -} +{} \ No newline at end of file diff --git a/l10n.yaml b/l10n.yaml index fb8f8fd9..304c5fb7 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -3,10 +3,10 @@ arb-dir: lib/l10n template-arb-file: app_zh.arb output-localization-file: app_localizations.dart preferred-supported-locales: - - "zh_Hant_TW" - - "zh_Hant" - - "zh_TW" - - "zh" - - "en" +- "en" +- "zh" +# - "zh_TW" +# - "zh_Hant" +# - "zh_Hant_TW" use-deferred-loading: true untranslated-messages-file: docs/untranslated.json diff --git a/lib/components/dialog/delete_dialog.dart b/lib/components/dialog/delete_dialog.dart index c62053da..015bcff0 100644 --- a/lib/components/dialog/delete_dialog.dart +++ b/lib/components/dialog/delete_dialog.dart @@ -20,10 +20,7 @@ class DeleteDialog extends StatelessWidget { FilledButton( key: const Key('delete_dialog.confirm'), onPressed: () => Navigator.of(context).pop(true), - style: FilledButton.styleFrom( - backgroundColor: const Color(0xFFC62828), - foregroundColor: Colors.white, - ), + style: FilledButton.styleFrom(backgroundColor: Colors.redAccent), child: Text(local.deleteButtonTooltip), ), ], diff --git a/lib/components/models/order_attribute_value_widget.dart b/lib/components/models/order_attribute_value_widget.dart index 3570dd97..21180b3a 100644 --- a/lib/components/models/order_attribute_value_widget.dart +++ b/lib/components/models/order_attribute_value_widget.dart @@ -2,43 +2,34 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/style/hint_text.dart'; import 'package:possystem/models/objects/order_attribute_object.dart'; import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; -class OrderAttributeValueWidget extends StatelessWidget { - final OrderAttributeMode? mode; - final num? value; - - const OrderAttributeValueWidget( - this.mode, - this.value, { - super.key, - }); - - @override - Widget build(BuildContext context) { - final name = getValueName(mode, value); - return name == '' ? const HintText('不影響價錢') : Text(name); - } - - static String getValueName(OrderAttributeMode? mode, num? value) { +class OrderAttributeValueWidget { + static Widget? build(OrderAttributeMode? mode, num? value) { if (value == null || mode == null || mode == OrderAttributeMode.statOnly) { - return ''; + return null; } + final name = _name(mode, value); + return name == '' ? HintText(S.orderAttributeValueEmpty) : Text(name); + } + + static String _name(OrderAttributeMode mode, num value) { final modeValue = value; if (mode == OrderAttributeMode.changeDiscount) { - final value = modeValue.toInt(); + final value = modeValue.toInt() / 100; return value == 0 - ? '免費' - : value >= 100 - ? '增加 ${(value / 100).toStringAsFixed(2)} 倍' - : '打 ${(value % 10) == 0 ? (value / 10).toStringAsFixed(0) : value} 折'; + ? S.orderAttributeValueFree + : value >= 1 + ? S.orderAttributeValueDiscountIncrease(value) + : S.orderAttributeValueDiscountDecrease(value); } else { final value = modeValue.toCurrency(); return modeValue == 0 ? '' : modeValue > 0 - ? '增加 $value 元' - : '減少 $value 元'; + ? S.orderAttributeValuePriceIncrease(value) + : S.orderAttributeValuePriceDecrease(value); } } } diff --git a/lib/components/models/order_loader.dart b/lib/components/models/order_loader.dart index 9d9b9ca2..fd1b5caa 100644 --- a/lib/components/models/order_loader.dart +++ b/lib/components/models/order_loader.dart @@ -42,16 +42,16 @@ class _OrderLoaderState extends State { builder: widget.builder, metricsBuilder: (metrics) { final meta = MetaBlock.withString(context, [ - S.orderListMetaPrice(metrics.price), - '總成本:${metrics.cost.toCurrency()}', - S.orderListMetaCount(metrics.count), + S.orderLoaderMetaTotalRevenue(metrics.revenue.toCurrency()), + S.orderLoaderMetaTotalCost(metrics.cost.toCurrency()), + S.orderLoaderMetaTotalCount(metrics.count), ])!; return Row(children: [ Expanded(child: Center(child: meta)), if (widget.trailingBuilder != null) buildTrailing(metrics), ]); }, - emptyChild: HintText(S.orderListEmpty), + emptyChild: HintText(S.orderLoaderEmpty), ); } diff --git a/lib/components/scaffold/item_list_scaffold.dart b/lib/components/scaffold/item_list_scaffold.dart deleted file mode 100644 index 79429227..00000000 --- a/lib/components/scaffold/item_list_scaffold.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:possystem/components/style/pop_button.dart'; -import 'package:possystem/constants/icons.dart'; - -class ItemListScaffold extends StatelessWidget { - final String title; - - final List items; - - /// It will use hint color - final List? tips; - - final int selected; - - const ItemListScaffold({ - super.key, - required this.title, - required this.items, - required this.selected, - this.tips, - }); - - @override - Widget build(BuildContext context) { - final hintStyle = TextStyle(color: Theme.of(context).hintColor); - - return Scaffold( - appBar: AppBar( - title: Text(title), - leading: const PopButton(), - ), - body: ListView.builder( - itemBuilder: (context, index) { - if (tips != null) { - tips![index]; - } - return ListTile( - title: Text(items[index]), - trailing: selected == index ? const Icon(KIcons.check) : null, - subtitle: tips != null && tips![index] != null ? Text(tips![index]!, style: hintStyle) : null, - onTap: () { - if (selected != index) { - Navigator.of(context).pop(index); - } - }, - ); - }, - itemCount: items.length, - ), - ); - } -} diff --git a/lib/components/mixin/item_modal.dart b/lib/components/scaffold/item_modal.dart similarity index 100% rename from lib/components/mixin/item_modal.dart rename to lib/components/scaffold/item_modal.dart diff --git a/lib/components/search_bar_wrapper.dart b/lib/components/search_bar_wrapper.dart index a61e702b..1f70217f 100644 --- a/lib/components/search_bar_wrapper.dart +++ b/lib/components/search_bar_wrapper.dart @@ -43,6 +43,7 @@ class _SearchBarWrapperState extends State> { @override Widget build(BuildContext context) { + // TODO: highlight the match string return SearchAnchor( searchController: searchController, // default using [MaterialTapTargetSize.shrinkWrap] button, which has bad diff --git a/lib/components/sign_in_button.dart b/lib/components/sign_in_button.dart index 254a1e0f..1bcf5d0a 100644 --- a/lib/components/sign_in_button.dart +++ b/lib/components/sign_in_button.dart @@ -1,9 +1,9 @@ import 'package:firebase_auth/firebase_auth.dart' show User, FirebaseAuthException; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:possystem/constants/constant.dart'; import 'package:possystem/helpers/logger.dart'; import 'package:possystem/services/auth.dart'; +import 'package:possystem/translator.dart'; const _googleBlue = Color(0xff4285f4); const _googleWhite = Color(0xffffffff); @@ -19,7 +19,7 @@ class SignInButton extends StatelessWidget { super.key, this.signedInWidget, this.signedInWidgetBuilder, - }); + }) : assert(signedInWidget != null || signedInWidgetBuilder != null); @override Widget build(BuildContext context) { @@ -28,7 +28,7 @@ class SignInButton extends StatelessWidget { builder: (context, snapshot) { final user = snapshot.data; // User is not signed in - if (user == null && !isLocalTest) { + if (user == null) { return const _GoogleSignInButton(key: Key('google_sign_in')); } @@ -99,7 +99,7 @@ class _GoogleSignInButtonState extends State<_GoogleSignInButton> { ), Expanded( child: Text( - '使用 Google 登入', + S.btnSignInWithGoogle, textAlign: TextAlign.center, style: TextStyle( height: 1.1, @@ -119,7 +119,7 @@ class _GoogleSignInButtonState extends State<_GoogleSignInButton> { color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(borderRadius), - onTap: signIn, + onTap: isLoading ? null : signIn, ), ), ), @@ -152,20 +152,18 @@ class _GoogleSignInButtonState extends State<_GoogleSignInButton> { } Future signIn() async { - if (!isLoading) { - setState(() => isLoading = true); + setState(() => isLoading = true); - bool success = false; - try { - success = await Auth.instance.signIn(); - } catch (e, stack) { - Log.err(e, 'auth_signin', stack); - setState(() { - error = e is FirebaseAuthException ? e.message : e.toString(); - }); - } - // if success this widget will disposed and should not fire setState - if (!success) setState(() => isLoading = false); + bool success = false; + try { + success = await Auth.instance.signIn(); + } catch (e, stack) { + Log.err(e, 'auth_signin', stack); + setState(() { + error = e is FirebaseAuthException ? e.message : e.toString(); + }); + } finally { + if (mounted && !success) setState(() => isLoading = false); } } } diff --git a/lib/components/style/more_button.dart b/lib/components/style/buttons.dart similarity index 90% rename from lib/components/style/more_button.dart rename to lib/components/style/buttons.dart index 3db5ec16..0fbf11b6 100644 --- a/lib/components/style/more_button.dart +++ b/lib/components/style/buttons.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:possystem/constants/icons.dart'; +import 'package:possystem/translator.dart'; class MoreButton extends StatelessWidget { final VoidCallback onPressed; @@ -51,8 +52,8 @@ class NavToButton extends StatelessWidget { Widget build(BuildContext context) { return IconButton( onPressed: onPressed, - tooltip: '前往', - icon: const Icon(KIcons.navTo), + tooltip: S.btnNavTo, + icon: const Icon(Icons.open_in_new_sharp), ); } } diff --git a/lib/components/style/date_range_picker.dart b/lib/components/style/date_range_picker.dart new file mode 100644 index 00000000..07fc4e40 --- /dev/null +++ b/lib/components/style/date_range_picker.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:possystem/settings/language_setting.dart'; + +/// Show a date range picker dialog but with a slightly different design. +/// +/// Human usually think 5/1~5/2 is two days. +/// Machine usually think 5/1~5/2 is one day (5/1 0:0 ~ 5/2 0:0). +/// So we need to convert between human and machine by adding a day to the end. +Future showMyDateRangePicker(BuildContext context, DateTimeRange range) async { + final result = await showDateRangePicker( + context: context, + initialDateRange: DateTimeRange( + start: range.start, + end: range.end.subtract(const Duration(days: 1)), + ), + initialEntryMode: DatePickerEntryMode.calendarOnly, + firstDate: DateTime(2021, 1), + lastDate: DateTime.now(), + locale: LanguageSetting.instance.value.locale, + + /// TODO: should fix this bug + /// Wrapping the design, because the background will use a slightly + /// transparent primary color when selecting a date, which will reduce + /// the expected contrast, making it difficult to see, so adjust the color + /// of onPrimary. + builder: (context, dialog) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme.copyWith( + onPrimary: theme.textTheme.bodyMedium?.color, + ); + return Theme( + data: theme.copyWith(colorScheme: colorScheme), + child: dialog ?? const SizedBox.shrink(), + ); + }); + + if (result != null) { + return DateTimeRange( + start: result.start, + end: result.end.add(const Duration(days: 1)), + ); + } + + return null; +} diff --git a/lib/components/style/empty_body.dart b/lib/components/style/empty_body.dart index f411cbd7..60d6076f 100644 --- a/lib/components/style/empty_body.dart +++ b/lib/components/style/empty_body.dart @@ -24,18 +24,18 @@ class EmptyBody extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - title ?? S.emptyBodyContent, + title ?? S.emptyBodyTitle, style: Theme.of(context).textTheme.titleLarge, ), if (helperText != null) Padding( - padding: const EdgeInsets.all(8.0), - child: Text(helperText!, textAlign: TextAlign.center), + padding: const EdgeInsets.fromLTRB(16, 8.0, 16.0, 8.0), + child: Text(helperText!), ), TextButton( key: const Key('empty_body'), onPressed: onPressed, - child: const Text('立即設定'), + child: Text(S.emptyBodyAction), ), ], ), diff --git a/lib/components/style/image_holder.dart b/lib/components/style/image_holder.dart index 305181d1..c152ee46 100644 --- a/lib/components/style/image_holder.dart +++ b/lib/components/style/image_holder.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:possystem/helpers/logger.dart'; import 'package:possystem/models/xfile.dart'; import 'package:possystem/routes.dart'; +import 'package:possystem/translator.dart'; class ImageHolder extends StatelessWidget { final ImageProvider image; @@ -102,7 +103,7 @@ class EditImageHolder extends StatelessWidget { return ImageHolder( key: const Key('image_holder.edit'), image: image, - title: path == null ? '點選以新增圖片' : '點擊以更新圖片', + title: path == null ? S.imageHolderCreate : S.imageHolderUpdate, onPressed: () async { final file = await context.pushNamed(Routes.imageGallery); if (file != null && file is String) onSelected(file); diff --git a/lib/components/style/percentile_bar.dart b/lib/components/style/percentile_bar.dart index adfc81c5..ff7a5fca 100644 --- a/lib/components/style/percentile_bar.dart +++ b/lib/components/style/percentile_bar.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:possystem/helpers/util.dart'; +import 'package:intl/intl.dart'; +import 'package:possystem/translator.dart'; class PercentileBar extends StatefulWidget { final num total; @@ -20,6 +21,7 @@ class _PercentileBarState extends State with SingleTickerProvider late AnimationController _controller; late Animation _colorAnimation; late Animation _curveAnimation; + final nf = NumberFormat.compact(locale: S.localeName); @override Widget build(BuildContext context) { @@ -28,7 +30,7 @@ class _PercentileBarState extends State with SingleTickerProvider Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Text('${widget.at.prettyString()}/${widget.total.prettyString()}'), + Text('${nf.format(widget.at)}/${nf.format(widget.total)}'), ], ), AnimatedBuilder( @@ -38,7 +40,7 @@ class _PercentileBarState extends State with SingleTickerProvider value: _controller.value, valueColor: _colorAnimation, backgroundColor: _colorAnimation.value?.withOpacity(0.2), - semanticsLabel: '目前佔總數的 ${_curveAnimation.value}', + semanticsLabel: S.semanticsPercentileBar(_curveAnimation.value), ); }, ), @@ -55,14 +57,8 @@ class _PercentileBarState extends State with SingleTickerProvider vsync: this, ); + // TODO: use single color final colorTween = TweenSequence([ - TweenSequenceItem( - tween: ColorTween( - begin: const Color(0xffff834c), - end: const Color(0xffeebc01), - ), - weight: 1, - ), TweenSequenceItem( tween: ColorTween( begin: const Color(0xff7fca2b), @@ -72,8 +68,8 @@ class _PercentileBarState extends State with SingleTickerProvider ), TweenSequenceItem( tween: ColorTween( - begin: const Color(0xff3d88df), - end: const Color(0xff8b6abc), + begin: const Color(0xff81c9de), + end: const Color(0xff3d88df), ), weight: 1, ), diff --git a/lib/components/style/route_circular_button.dart b/lib/components/style/route_circular_button.dart index fdaeb0d0..2306b917 100644 --- a/lib/components/style/route_circular_button.dart +++ b/lib/components/style/route_circular_button.dart @@ -25,41 +25,35 @@ class RouteCircularButton extends StatelessWidget { @override Widget build(BuildContext context) { - final color = Theme.of(context).colorScheme.primary; - return InkWell( - borderRadius: const BorderRadius.all(Radius.circular(48)), - splashColor: Colors.transparent, - onTap: onTap ?? - () async { - final result = await context.pushNamed(route!); - if (result == true) { - if (context.mounted) { - showSnackBar(context, S.actSuccess); - } - } - }, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 96, maxWidth: 96), - child: AspectRatio( - aspectRatio: 1, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - border: Border.all(color: color), - color: Colors.transparent, - shape: BoxShape.circle, - ), - child: Icon(icon, color: color), - ), - const SizedBox(height: 4), - Text(text, style: TextStyle(color: color)), - ], + return Tooltip( + message: text, // text will be ellipsis, so show full text in tooltip + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(14), + maximumSize: const Size(96, 96), + ), + onPressed: onTap ?? + () async { + final result = await context.pushNamed(route!); + if (result == true) { + if (context.mounted) { + showSnackBar(context, S.actSuccess); + } + } + }, + child: Icon(icon, size: 32), ), - ), + const SizedBox(height: 4), + Text( + text, + style: const TextStyle(overflow: TextOverflow.ellipsis), + ), + ], ), ); } diff --git a/lib/components/style/search_bar_inline.dart b/lib/components/style/search_bar_inline.dart index fb11c39a..37a1f566 100644 --- a/lib/components/style/search_bar_inline.dart +++ b/lib/components/style/search_bar_inline.dart @@ -36,6 +36,7 @@ class SearchBarInline extends StatelessWidget { labelText: labelText, hintText: hintText, focusedBorder: Theme.of(context).inputDecorationTheme.focusedBorder, + errorMaxLines: 2, prefixIcon: const Icon(KIcons.search), ), ), diff --git a/lib/components/style/snackbar.dart b/lib/components/style/snackbar.dart index 8dbca013..b6943fc5 100644 --- a/lib/components/style/snackbar.dart +++ b/lib/components/style/snackbar.dart @@ -32,19 +32,14 @@ Future showSnackbarWhenFailed( }); } -void showMoreInfoSnackBar( - BuildContext context, - String message, - Widget content, { - String label = '說明', -}) { +void showMoreInfoSnackBar(BuildContext context, String message, Widget content) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar(SnackBar( // make floating button below behavior: SnackBarBehavior.floating, content: Text(message), action: SnackBarAction( - label: label, + label: S.actMoreInfo, onPressed: () { showDialog( context: context, diff --git a/lib/components/style/launcher_snackbar_action.dart b/lib/components/style/snackbar_actions.dart similarity index 100% rename from lib/components/style/launcher_snackbar_action.dart rename to lib/components/style/snackbar_actions.dart index 2774040e..1c4d5e0c 100644 --- a/lib/components/style/launcher_snackbar_action.dart +++ b/lib/components/style/snackbar_actions.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:possystem/helpers/logger.dart'; import 'package:possystem/helpers/launcher.dart'; +import 'package:possystem/helpers/logger.dart'; class LauncherSnackbarAction extends SnackBarAction { LauncherSnackbarAction({ diff --git a/lib/components/tutorial.dart b/lib/components/tutorial.dart index 98ece25f..74e3c792 100644 --- a/lib/components/tutorial.dart +++ b/lib/components/tutorial.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:possystem/services/cache.dart'; import 'package:spotlight_ant/spotlight_ant.dart'; @@ -49,6 +50,8 @@ class Tutorial extends StatelessWidget { final SpotlightDurationConfig duration; + final String? route; + const Tutorial({ super.key, required this.id, @@ -59,6 +62,7 @@ class Tutorial extends StatelessWidget { this.padding = const EdgeInsets.all(8), this.disable = false, this.monitorVisibility = false, + this.route, required this.child, this.duration = const SpotlightDurationConfig( bump: Duration(milliseconds: 500), @@ -80,10 +84,13 @@ class Tutorial extends StatelessWidget { duration: duration, monitorId: monitorVisibility ? 'tutorial.$id' : null, onDismiss: _onDismiss, + onDismissed: route != null ? () => context.goNamed(route!) : null, spotlight: SpotlightConfig( builder: spotlightBuilder, padding: padding, + onTap: route != null ? () async => SpotlightAntAction.skip : null, ), + backdrop: SpotlightBackdropConfig(silent: route != null), action: const SpotlightActionConfig( enabled: [SpotlightAntAction.prev, SpotlightAntAction.next], ), @@ -92,10 +99,10 @@ class Tutorial extends StatelessWidget { if (title != null) Text( title!, - style: const TextStyle(fontSize: 24), + style: Theme.of(context).textTheme.headlineMedium?.copyWith(color: Colors.white), ), const SizedBox(height: 16), - Text(message, style: const TextStyle(fontSize: 18)), + Text(message), ]), ), child: child, diff --git a/lib/constants/constant.dart b/lib/constants/constant.dart index cd9b882b..95adacef 100644 --- a/lib/constants/constant.dart +++ b/lib/constants/constant.dart @@ -5,3 +5,5 @@ const double kSpacing3 = 18.0; const double kSpacing4 = 22.0; const double kSpacing5 = 24.0; const bool isLocalTest = String.fromEnvironment('appFlavor') == 'debug'; +const bool isInternalTest = String.fromEnvironment('appFlavor') == 'dev'; +const bool isProd = String.fromEnvironment('appFlavor') == 'prod'; diff --git a/lib/constants/icons.dart b/lib/constants/icons.dart index 3272ed19..9a6f2905 100644 --- a/lib/constants/icons.dart +++ b/lib/constants/icons.dart @@ -13,11 +13,9 @@ class KIcons { static const entryAdd = Icons.add_circle_outline_sharp; static const more = Icons.more_horiz_sharp; - static const check = Icons.check_sharp; static const edit = Icons.edit_sharp; static const search = Icons.search_sharp; static const preview = Icons.remove_red_eye_sharp; - static const navTo = Icons.open_in_new_sharp; static const warn = Icons.warning_amber_sharp; } diff --git a/lib/debug/debug_page.dart b/lib/debug/debug_page.dart new file mode 100644 index 00000000..011ffb50 --- /dev/null +++ b/lib/debug/debug_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:possystem/components/style/pop_button.dart'; +import 'package:possystem/debug/random_gen_order.dart'; +import 'package:possystem/debug/rerun_migration.dart'; +import 'package:possystem/services/cache.dart'; + +class DebugPage extends StatelessWidget { + const DebugPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Debug'), leading: const PopButton()), + body: ListView( + key: const Key('debug.list'), + children: [ + ListTile( + title: const Text('Generate orders'), + trailing: const Icon(Icons.add_sharp), + onTap: goGenerateRandomOrders(context), + ), + ListTile( + title: const Text('Cache Reset'), + trailing: const Icon(Icons.clear_all_sharp), + onTap: Cache.instance.reset, + ), + const ListTile( + title: Text('Migrate DB Again'), + trailing: Icon(Icons.refresh_sharp), + onTap: rerunMigration, + ) + ], + ), + ); + } +} diff --git a/lib/debug/random_gen_order.dart b/lib/debug/random_gen_order.dart index 13034ec1..d53ffc9f 100644 --- a/lib/debug/random_gen_order.dart +++ b/lib/debug/random_gen_order.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:possystem/components/style/date_range_picker.dart'; import 'package:possystem/components/style/pop_button.dart'; import 'package:possystem/components/style/snackbar.dart'; import 'package:possystem/helpers/util.dart'; @@ -14,10 +15,19 @@ import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/settings/currency_setting.dart'; import 'package:provider/provider.dart'; -/// Generate order records each record have total 1~10 products. For example, -/// 1 product might have 10 count or 10 different products or 2 same product but -/// each have different ingredients. -List generateOrder({ +void Function() goGenerateRandomOrders(BuildContext context) { + return () => Navigator.of(context).push(MaterialPageRoute(builder: (context) { + return const _SettingPage(); + })); +} + +/// Generate random order records with random products, +/// random count, random price, random ingredients, random attributes. +/// +/// Each record might have 1~10 products, for example, +/// 1 product might have 10 count or 10 different products +/// or 2 same product but each have different ingredients. +List generateOrders({ required int orderCount, required DateTime startFrom, required DateTime endTo, @@ -116,19 +126,6 @@ List _selectExistedProduct(List data, String id) { return result; } -class RandomGenerateOrderButton extends StatelessWidget { - const RandomGenerateOrderButton({super.key}); - - @override - Widget build(BuildContext context) { - return ElevatedButton.icon( - onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (_) => const _SettingPage())), - label: const Text('產生隨機餐點'), - icon: const Icon(Icons.developer_mode_sharp), - ); - } -} - class _SettingPage extends StatefulWidget { const _SettingPage(); @@ -140,6 +137,7 @@ class _SettingPageState extends State<_SettingPage> { final DateFormat dateFormat = DateFormat("yyyy-MM-dd"); final _countController = TextEditingController(text: '1'); + bool generating = false; late DateTime startFrom; late DateTime endTo; @@ -150,7 +148,7 @@ class _SettingPageState extends State<_SettingPage> { leading: const PopButton(), actions: [ TextButton( - onPressed: () => submit(context.read()), + onPressed: generating ? null : () => submit(context.read()), child: const Text('OK'), ) ], @@ -164,11 +162,11 @@ class _SettingPageState extends State<_SettingPage> { textInputAction: TextInputAction.done, keyboardType: TextInputType.number, decoration: const InputDecoration( - labelText: '訂單數量', - hintText: '平均分配於時間區間', + labelText: 'Count', + hintText: 'It will be distributed in the time interval.', ), maxLength: 5, - validator: Validator.positiveInt('訂單數量', maximum: 9999, minimum: 1), + validator: Validator.positiveInt('Count', maximum: 9999, minimum: 1), ), const SizedBox(height: 8.0), InkWell( @@ -192,28 +190,24 @@ class _SettingPageState extends State<_SettingPage> { } Future selectDates() async { - const oneDay = Duration(days: 1); - final selected = await showDateRangePicker( - context: context, - firstDate: DateTime(2020), - lastDate: DateUtils.dateOnly(DateTime.now()), - initialDateRange: DateTimeRange( - start: startFrom, - end: endTo.subtract(oneDay), - ), + final selected = await showMyDateRangePicker( + context, + DateTimeRange(start: startFrom, end: endTo), ); if (selected != null) { setState(() { startFrom = selected.start; - endTo = selected.end.add(oneDay); + endTo = selected.end; }); } } void submit(Seller seller) async { + setState(() => generating = true); + final count = int.tryParse(_countController.text); - final result = generateOrder( + final result = generateOrders( orderCount: count ?? 0, startFrom: startFrom, endTo: endTo, @@ -221,7 +215,7 @@ class _SettingPageState extends State<_SettingPage> { await Future.forEach(result, (e) => seller.push(e)); if (mounted) { - showSnackBar(context, '成功產生 ${result.length} 個訂單'); + showSnackBar(context, 'Generate ${result.length} orders successfully'); Navigator.of(context).pop(); } diff --git a/lib/debug/rerun_migration.dart b/lib/debug/rerun_migration.dart index 5aa21934..0a755504 100644 --- a/lib/debug/rerun_migration.dart +++ b/lib/debug/rerun_migration.dart @@ -1,20 +1,8 @@ -import 'package:flutter/material.dart'; import 'package:possystem/services/database.dart'; -class RerunMigration extends StatelessWidget { - const RerunMigration({super.key}); - - @override - Widget build(BuildContext context) { - return ElevatedButton.icon( - onPressed: () async { - await Database.execMigrationAction( - Database.instance.db, - Database.latestVersion, - ); - }, - label: const Text('重新執行 Migration'), - icon: const Icon(Icons.clear_all_sharp), - ); - } +void rerunMigration() async { + await Database.execMigrationAction( + Database.instance.db, + Database.latestVersion, + ); } diff --git a/lib/firebase_compatible_options.dart b/lib/firebase_compatible_options.dart index add2c8ce..013c7833 100644 --- a/lib/firebase_compatible_options.dart +++ b/lib/firebase_compatible_options.dart @@ -1,7 +1,8 @@ // File generated by FlutterFire CLI. // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' show defaultTargetPlatform, kDebugMode, kIsWeb, TargetPlatform; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:possystem/constants/constant.dart'; /// Default [FirebaseOptions] for use with your Firebase apps. /// @@ -23,7 +24,7 @@ class DefaultFirebaseOptions { } switch (defaultTargetPlatform) { case TargetPlatform.android: - return kDebugMode ? androidDebug : android; + return isProd ? android : androidDebug; case TargetPlatform.iOS: throw UnsupportedError( 'DefaultFirebaseOptions have not been configured for ios - ' diff --git a/lib/helpers/exporter/google_sheet_exporter.dart b/lib/helpers/exporter/google_sheet_exporter.dart index 005efb62..f5f684ca 100644 --- a/lib/helpers/exporter/google_sheet_exporter.dart +++ b/lib/helpers/exporter/google_sheet_exporter.dart @@ -126,7 +126,7 @@ class GoogleSheetExporter extends DataExporter { return GoogleSheetProperties.fromSheet(res?.sheets); } - /// 更新表單 + /// Update the sheet with the given data. Future updateSheet( GoogleSpreadsheet spreadsheet, Iterable sheets, @@ -169,7 +169,7 @@ class GoogleSheetExporter extends DataExporter { ); } - /// 值的修改,批次做 + /// Update the sheet with the given data in batch. Future updateSheetValues( GoogleSpreadsheet spreadsheet, Iterable sheets, @@ -202,7 +202,7 @@ class GoogleSheetExporter extends DataExporter { ); } - /// 僅作值的附加 + /// Update the sheet by appending the given data. Future appendSheetValues( GoogleSpreadsheet spreadsheet, GoogleSheetProperties sheet, @@ -349,10 +349,12 @@ class GoogleSheetProperties { String toCacheValue() => '$title $id'; @override - // ignore: hash_and_equals bool operator ==(Object other) { return other is GoogleSheetProperties && other.id == id && other.title == title; } + + @override + int get hashCode => id.hashCode ^ title.hashCode; } class GoogleSheetCellData { @@ -362,7 +364,7 @@ class GoogleSheetCellData { final String? note; - // 讓資料變成選擇性的欄位,例如顧客設定的類別只能有哪幾種。 + /// If this is not null, the cell will be a dropdown list. final List? options; GoogleSheetCellData({ diff --git a/lib/helpers/formatter/formatter.dart b/lib/helpers/formatter/formatter.dart index cfdc0149..ff34641e 100644 --- a/lib/helpers/formatter/formatter.dart +++ b/lib/helpers/formatter/formatter.dart @@ -1,9 +1,9 @@ -import 'package:possystem/models/model.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/models/menu/product.dart'; import 'package:possystem/models/menu/product_ingredient.dart'; import 'package:possystem/models/menu/product_quantity.dart'; +import 'package:possystem/models/model.dart'; import 'package:possystem/models/objects/order_attribute_object.dart'; import 'package:possystem/models/order/order_attribute.dart'; import 'package:possystem/models/order/order_attribute_option.dart'; @@ -46,7 +46,8 @@ abstract class Formatter { int counter = 1; for (var row in parsed) { final r = row.map((e) => e.trim()).toList(); - final msg = formatter.validate(r) ?? (existInResult(r[formatter.nameIndex]) ? '將忽略本行,相同的項目已於前面出現' : null); + final msg = + formatter.validate(r) ?? (existInResult(r[formatter.nameIndex]) ? S.transitImportErrorDuplicate : null); if (msg != null) { result.add( @@ -168,7 +169,7 @@ class _MenuFormatter extends ModelFormatter { @override String? validate(List row) { - if (row.length < 4) return S.transitImportColumnsCountError(4); + if (row.length < 4) return S.transitImportErrorColumnCount(4); final errorMsg = Validator.textLimit(S.menuCatalogNameLabel, 30)(row[0]) ?? Validator.textLimit(S.menuProductNameLabel, 30)(row[1]) ?? @@ -178,7 +179,7 @@ class _MenuFormatter extends ModelFormatter { if (errorMsg != null || row.length == 4) return errorMsg; final vIng = Validator.textLimit(S.stockIngredientNameLabel, 30); - final vQua = Validator.textLimit(S.quantityNameLabel, 30); + final vQua = Validator.textLimit(S.stockQuantityNameLabel, 30); final vAmount = Validator.positiveNumber( S.stockIngredientAmountLabel, allowNull: true, @@ -257,7 +258,7 @@ class _StockFormatter extends ModelFormatter { @override String? validate(List row) { - if (row.isEmpty) return S.transitImportColumnsCountError(1); + if (row.isEmpty) return S.transitImportErrorColumnCount(1); return Validator.textLimit(S.stockIngredientNameLabel, 30)(row[0]) ?? Validator.positiveNumber( @@ -280,11 +281,11 @@ class _QuantitiesFormatter extends ModelFormatter { @override String? validate(List row) { - if (row.isEmpty) return S.transitImportColumnsCountError(1); + if (row.isEmpty) return S.transitImportErrorColumnCount(1); - return Validator.textLimit(S.quantityNameLabel, 30)(row[0]) ?? + return Validator.textLimit(S.stockQuantityNameLabel, 30)(row[0]) ?? Validator.positiveNumber( - S.quantityProportionLabel, + S.stockQuantityProportionLabel, maximum: 100, allowNull: true, )(row.length > 1 ? row[1] : null); @@ -304,7 +305,7 @@ class _ReplenisherFormatter extends ModelFormatter { @override String? validate(List row) { - if (row.isEmpty) return S.transitImportColumnsCountError(1); + if (row.isEmpty) return S.transitImportErrorColumnCount(1); final errorMsg = Validator.textLimit(S.stockReplenishmentNameLabel, 30)(row[0]); if (errorMsg != null || row.length == 1) return errorMsg; @@ -368,7 +369,7 @@ class _OAFormatter extends ModelFormatter { @override String? validate(List row) { - if (row.length < 2) return S.transitImportColumnsCountError(2); + if (row.length < 2) return S.transitImportErrorColumnCount(2); final msg = Validator.textLimit(S.orderAttributeNameLabel, 30)(row[0]); if (msg != null || row.length == 2) return msg; @@ -430,7 +431,7 @@ class _OAFormatter extends ModelFormatter { OrderAttributeMode _str2mode(String key) { for (final e in OrderAttributeMode.values) { - if (S.orderAttributeModeNames(e.name) == key) { + if (S.orderAttributeModeName(e.name) == key) { return e; } } diff --git a/lib/helpers/formatter/google_sheet_formatter.dart b/lib/helpers/formatter/google_sheet_formatter.dart index 8cb1ef2e..0ed0d578 100644 --- a/lib/helpers/formatter/google_sheet_formatter.dart +++ b/lib/helpers/formatter/google_sheet_formatter.dart @@ -37,7 +37,7 @@ class _MenuTransformer extends ModelTransformer { _toCD(S.menuProductNameLabel), _toCD(S.menuProductPriceLabel), _toCD(S.menuProductCostLabel), - _toCD(S.transitProductIngredientInfoTitle, S.transitProductIngredientInfoGSNote), + _toCD(S.transitGSModelProductIngredientTitle, S.transitGSModelProductIngredientNote), ]; @override @@ -71,7 +71,7 @@ class _StockTransformer extends ModelTransformer { List getHeader() => [ _toCD(S.stockIngredientNameLabel), _toCD(S.stockIngredientAmountLabel), - _toCD(S.stockIngredientTotalAmountLabel), + _toCD(S.stockIngredientAmountMaxLabel), ]; @override @@ -89,8 +89,8 @@ class _QuantitiesTransformer extends ModelTransformer { @override List getHeader() => [ - _toCD(S.quantityNameLabel), - _toCD(S.quantityProportionLabel, S.quantityProportionHelper), + _toCD(S.stockQuantityNameLabel), + _toCD(S.stockQuantityProportionLabel, S.stockQuantityProportionHelper), ]; @override @@ -108,7 +108,7 @@ class _ReplenisherTransformer extends ModelTransformer { @override List getHeader() => [ _toCD(S.stockReplenishmentNameLabel), - _toCD(S.transitReplenishmentTitle, S.transitReplenishmentGSNote) + _toCD(S.transitGSModelReplenishmentTitle, S.transitGSModelReplenishmentNote) ]; @override @@ -129,18 +129,18 @@ class _OATransformer extends ModelTransformer { @override List getHeader() { final note = OrderAttributeMode.values - .map((e) => '${S.orderAttributeModeNames(e.name)} - ${S.orderAttributeModeDescriptions(e.name)}') + .map((e) => '${S.orderAttributeModeName(e.name)} - ${S.orderAttributeModeHelper(e.name)}') .join('\n'); return [ _toCD(S.orderAttributeNameLabel), - _toCD(S.orderAttributeModeTitle, note), - _toCD(S.transitOrderAttributeOptionTitle, S.transitOrderAttributeOptionGSNote), + _toCD(S.orderAttributeModeDivider, note), + _toCD(S.transitGSModelAttributeOptionTitle, S.transitGSModelAttributeOptionNote), ]; } @override List> getRows() { - final options = OrderAttributeMode.values.map((e) => S.orderAttributeModeNames(e.name)).toList(); + final options = OrderAttributeMode.values.map((e) => S.orderAttributeModeName(e.name)).toList(); return target.itemList.map((e) { final info = [ @@ -150,7 +150,7 @@ class _OATransformer extends ModelTransformer { GoogleSheetCellData(stringValue: e.name), GoogleSheetCellData( options: options, - stringValue: S.orderAttributeModeNames(e.mode.name), + stringValue: S.orderAttributeModeName(e.mode.name), ), GoogleSheetCellData(stringValue: info), ]; diff --git a/lib/helpers/formatter/plain_text_formatter.dart b/lib/helpers/formatter/plain_text_formatter.dart index bb7b6da3..108140c7 100644 --- a/lib/helpers/formatter/plain_text_formatter.dart +++ b/lib/helpers/formatter/plain_text_formatter.dart @@ -1,14 +1,16 @@ +import 'package:intl/intl.dart'; import 'package:possystem/helpers/formatter/formatter.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/replenisher.dart'; import 'package:possystem/models/repository/stock.dart'; +import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/translator.dart'; -const _reDig = r'-?\d+\.?\d*'; +const _reDig = r' *-?\d+\.?\d*'; +const _reInt = r'[0-9 ]+'; const _rePre = r'^'; -const _rePost = r'$'; class PlainTextFormatter extends Formatter { const PlainTextFormatter(); @@ -30,15 +32,15 @@ class PlainTextFormatter extends Formatter { } Formattable? whichFormattable(String line) { - if (line.startsWith('本菜單')) { + if (line.startsWith(S.transitPTFormatModelMenuHeaderPrefix)) { return Formattable.menu; - } else if (line.startsWith('本庫存')) { + } else if (line.startsWith(S.transitPTFormatModelStockHeaderPrefix)) { return Formattable.stock; - } else if (line.endsWith('種份量')) { + } else if (line.endsWith(S.transitPTFormatModelQuantitiesHeaderSuffix)) { return Formattable.quantities; - } else if (line.endsWith('種補貨方式')) { + } else if (line.endsWith(S.transitPTFormatModelReplenisherHeaderSuffix)) { return Formattable.replenisher; - } else if (line.endsWith('種顧客屬性')) { + } else if (line.endsWith(S.transitPTFormatModelOaHeaderSuffix)) { return Formattable.orderAttr; } @@ -49,62 +51,60 @@ class PlainTextFormatter extends Formatter { class _MenuTransformer extends ModelTransformer { const _MenuTransformer(super.target); - static const catalogTmp = r'第%num個種類叫做 %name'; - static const productTmp = r'第%num個產品叫做 %name,其售價為 %price 元,成本為 %cost 元'; - static const ingredientTmp = r'每份產品預設需要使用 %amount 個 %name'; - static const quantityTmp = r'%name(' - '每份產品改成使用 %amount 個' - '並調整產品售價 %price 元和' - '成本 %cost 元)'; + static const ingredientDelimiter = ';'; + static const quantityPrefix = ':'; + static const quantityDelimiter = '、'; @override - List getHeader() => ['${target.length} 個產品種類', '${target.products.length} 個產品']; + List getHeader() => [ + S.transitPTFormatModelMenuMetaCatalog(target.length), + S.transitPTFormatModelMenuMetaProduct(target.products.length) + ]; @override List> getRows() { int catalogCount = 1; return [ - ['本菜單共有 ${target.length} 個產品種類、${target.products.length} 個產品。'], + [S.transitPTFormatModelMenuHeader(target.length, target.products.length)], ...target.itemList.map>((catalog) { int productCount = 1; - final v = catalog.isEmpty ? '沒有設定產品' : '共有 ${catalog.length} 個產品'; + final nf = NumberFormat.decimalPattern(S.localeName); return [ - '${catalogTmp.f({'num': catalogCount++, 'name': catalog.name})},$v。', - ...catalog.itemList.map((product) { - String base = productTmp.f({ - 'num': productCount++, - 'name': product.name, - 'price': product.price, - 'cost': product.cost, - }); - if (product.isEmpty) { - return '$base,它沒有設定任何成份。'; - } - - base = '$base,它的成份有 ${product.items.length} 種:' - '${product.items.map((e) => e.name).join('、')}'; - - final ingredients = product.items.map((ingredient) { - final ing = ingredientTmp.f({ - 'amount': ingredient.amount, - 'name': ingredient.name, - }); - if (ingredient.isEmpty) { - return '$ing,無法做份量調整'; - } - - final quantities = ingredient.items - .map((quantity) => quantityTmp.f({ - 'name': quantity.name, - 'amount': quantity.amount, - 'price': quantity.additionalPrice, - 'cost': quantity.additionalCost, - })) - .join('、'); - return '$ing,它還有 ${ingredient.items.length} 個不同份量:$quantities'; - }).join(';'); - return '$base。$ingredients。'; - }), + S.transitPTFormatModelMenuCatalog( + (catalogCount++).toString(), + catalog.name, + S.transitPTFormatModelMenuCatalogDetails(catalog.length), + ), + for (final product in catalog.itemList) + S.transitPTFormatModelMenuProduct( + (productCount++).toString(), + product.name, + product.price.toCurrency(), + product.cost.toCurrency(), + S.transitPTFormatModelMenuProductDetails( + product.items.length, + product.items.map((e) => e.name).join('、'), + product.items + .map( + (ingredient) => S.transitPTFormatModelMenuIngredient( + nf.format(ingredient.amount), + ingredient.name, + S.transitPTFormatModelMenuIngredientDetails( + ingredient.items.length, + quantityPrefix + + ingredient.items + .map((quantity) => '${quantity.name}(${S.transitPTFormatModelMenuQuantity( + nf.format(quantity.amount), + quantity.additionalPrice.toCurrency(), + quantity.additionalCost.toCurrency(), + )})') + .join(quantityDelimiter), + ), + ), + ) + .join(ingredientDelimiter), + ), + ), ]; }) ]; @@ -112,32 +112,50 @@ class _MenuTransformer extends ModelTransformer { @override List> parseRows(List> rows) { - final reCatalog = RegExp(_rePre + - catalogTmp.f({ - 'num': r'\d+', - 'name': r'(?[^,]+?),', - })); - final reProduct = RegExp(_rePre + - productTmp.f({ - 'num': r'\d+', - 'name': r'(?[^,]+?)', - 'price': '(?$_reDig)', - 'cost': '(?$_reDig)', - })); - final reIngredient = RegExp(_rePre + - ingredientTmp.f({ - 'amount': '(?$_reDig)', - 'name': r'(?[^,]+?),', - })); - final reQuantity = RegExp(_rePre + - quantityTmp.f({ - 'name': r'(?[^(]+?)', - 'amount': '(?$_reDig)', - 'price': '(?$_reDig)', - 'cost': '(?$_reDig)', - })); - - final lines = rows[0].expand((e) => e.toString().split('。').map((e) => e.trim())).where((e) => e.isNotEmpty); + final reCatalog = RegExp( + _rePre + + S.transitPTFormatModelMenuCatalog( + _reInt, + r'(?.+)', + r'.*', + ), + ); + final reProduct = RegExp( + _rePre + + S + .transitPTFormatModelMenuProduct( + _reInt, + r'(?.+)', + '(?$_reDig)', + '(?$_reDig)', + r'.*', + ) + .replaceAll(r'$', r'\$'), + ); + final reIngredient = RegExp( + S.transitPTFormatModelMenuIngredient( + '(?$_reDig)', + r'(?.+?)', + r'.*', + ), + ); + final reQuantity = RegExp( + _rePre + + r'(?.+)(' + // hard coded naming pattern + S + .transitPTFormatModelMenuQuantity( + '(?$_reDig)', + '(?$_reDig)', + '(?$_reDig)', + ) + .replaceAll(r'$', r'\$'), + ); + + final lines = rows[0] + .expand( + (e) => e.toString().split('\n').map((e) => e.trim()), + ) + .where((e) => e.isNotEmpty); final result = >[]; String catalog = '', product = '', price = '', cost = ''; bool foundProduct = false; @@ -166,21 +184,21 @@ class _MenuTransformer extends ModelTransformer { continue; } - final ingSplit = line.split(';'); + final ingSplit = line.split(ingredientDelimiter); String ingredients = ''; foundProduct = false; for (final ing in ingSplit) { - final ingSplit = ing.split(':'); + int quaStartIndex = ing.indexOf(quantityPrefix); + if (quaStartIndex == -1) quaStartIndex = ing.length; - match = reIngredient.firstMatch(ingSplit[0]); + match = reIngredient.firstMatch(ing.substring(0, quaStartIndex)); if (match != null) { ingredients = '$ingredients\n- ${match.namedGroup('name')!},' '${match.namedGroup('amount')!}'; } + if (quaStartIndex == ing.length) continue; - if (ingSplit.length == 1) continue; - final quaSplit = ingSplit[1].split('、'); - + final quaSplit = ing.substring(quaStartIndex + 1).split(quantityDelimiter); for (final qua in quaSplit) { match = reQuantity.firstMatch(qua); if (match != null) { @@ -203,42 +221,40 @@ class _MenuTransformer extends ModelTransformer { class _StockTransformer extends ModelTransformer { const _StockTransformer(super.target); - static const baseTmp = r'第%num個成份叫做 %name,庫存現有 %amount 個'; - static const maxTmp = r'最大量有 %max 個。'; - @override - List getHeader() => ['${target.length} 種成份']; + List getHeader() => [S.transitPTFormatModelStockMetaIngredient(target.length)]; @override List> getRows() { int counter = 1; + final nf = NumberFormat.decimalPattern(S.localeName); return [ - ['本庫存共有 ${target.length} 種成份'], - if (target.isNotEmpty) - [ - ...target.itemList.map((ingredient) { - final max = ingredient.totalAmount; - final maxStr = max == null ? '' : ',${maxTmp.f({'max': max})}'; - return baseTmp.f({ - 'num': counter++, - 'name': ingredient.name, - 'amount': ingredient.currentAmount, - }) + - maxStr; - }), - ], + [S.transitPTFormatModelStockHeader(target.length)], + [ + for (final ingredient in target.itemList) + S.transitPTFormatModelStockIngredient( + (counter++).toString(), + ingredient.name, + nf.format(ingredient.currentAmount), + S.transitPTFormatModelStockIngredientDetails( + ingredient.totalAmount == null ? 0 : 1, + nf.format(ingredient.totalAmount ?? 0), + ), + ), + ], ]; } @override List> parseRows(List> rows) { final reBase = RegExp(_rePre + - baseTmp.f({ - 'num': r'\d+', - 'name': r'(?[^,]+?)', - 'amount': '(?$_reDig)', - })); - final reMax = RegExp(maxTmp.f({'max': '(?$_reDig)'}) + _rePost); + S.transitPTFormatModelStockIngredient( + _reInt, + r'(?[^,]+?)', + '(?$_reDig)', + '', + )); + final reMax = RegExp(S.transitPTFormatModelStockIngredientDetails(1, '(?$_reDig)')); final result = >[]; for (final line in rows[0]) { @@ -260,38 +276,36 @@ class _StockTransformer extends ModelTransformer { class _QuantitiesTransformer extends ModelTransformer { const _QuantitiesTransformer(super.target); - static const baseTmp = r'第%num種份量叫做 %name,' - '預設會讓成分的份量乘以 %prop 倍。'; - @override - List getHeader() => ['${target.length} 種份量']; + List getHeader() => [S.transitPTFormatModelQuantitiesMetaQuantity(target.length)]; @override List> getRows() { int counter = 1; + final nf = NumberFormat.decimalPattern(S.localeName); return [ - ['共設定 ${target.length} 種份量'], - if (target.isNotEmpty) - [ - ...target.itemList.map((quantity) { - return baseTmp.f({ - 'num': counter++, - 'name': quantity.name, - 'prop': quantity.defaultProportion, - }); - }), - ], + [S.transitPTFormatModelQuantitiesHeader(target.length)], + [ + for (final quantity in target.itemList) + S.transitPTFormatModelQuantitiesQuantity( + (counter++).toString(), + quantity.name, + nf.format(quantity.defaultProportion), + ), + ] ]; } @override List> parseRows(List> rows) { - final re = RegExp(_rePre + - baseTmp.f({ - 'num': r'\d+', - 'name': r'(?[^,]+?)', - 'prop': '(?$_reDig)', - })); + final re = RegExp( + _rePre + + S.transitPTFormatModelQuantitiesQuantity( + _reInt, + r'(?[^,]+?)', + '(?$_reDig)', + ), + ); final result = >[]; for (final line in rows[0]) { @@ -311,56 +325,49 @@ class _QuantitiesTransformer extends ModelTransformer { class _ReplenisherTransformer extends ModelTransformer { const _ReplenisherTransformer(super.target); - static const baseTmp = r'第%num種方式叫做 %name,'; - static const ingredientTmp = r'%name(%amount 個)'; + static const ingredientDelimiter = ':'; @override - List getHeader() => ['${target.length} 種補貨方式']; + List getHeader() => [S.transitPTFormatModelReplenisherMetaReplenishment(target.length)]; @override List> getRows() { int counter = 1; + final nf = NumberFormat.decimalPattern(S.localeName); return [ - ['共設定 ${target.length} 種補貨方式'], - ...target.itemList.map((repl) { - final data = repl.ingredientData; - final base = baseTmp.f({'num': counter++, 'name': repl.name}); - - if (data.isEmpty) { - return ['$base它並不會調整庫存。']; - } - final ing = data.entries - .map((e) => ingredientTmp.f({ - 'name': e.key.name, - 'amount': e.value, - })) - .join('、'); - return ['$base它會調整${data.length}種成份的庫存:$ing。']; - }) + [S.transitPTFormatModelReplenisherHeader(target.length)], + target.itemList.map((repl) { + String d = repl.ingredientData.entries.map((e) => '${e.key.name}(${nf.format(e.value)})').join('、'); + d = d.isEmpty ? '' : ingredientDelimiter + d; + return S.transitPTFormatModelReplenisherReplenishment( + (counter++).toString(), + repl.name, + S.transitPTFormatModelReplenisherReplenishmentDetails(repl.ingredientData.length) + d, + ); + }).toList(), ]; } @override List> parseRows(List> rows) { - final reBase = RegExp(_rePre + - baseTmp.f({ - 'num': r'\d+', - 'name': r'(?[^,]+?)', - })); - final reIngredient = RegExp(_rePre + - ingredientTmp.f({ - 'amount': '(?$_reDig)', - 'name': r'(?[^(]+?)', - })); + final reBase = RegExp( + _rePre + + S.transitPTFormatModelReplenisherReplenishment( + _reInt, + r'(?[^,]+?)', + '.*', + ), + ); + final reIngredient = RegExp('$_rePre(?.*)((?$_reDig))'); final result = >[]; for (final line in rows[0]) { - final lineSplit = line.toString().split(':'); + final lineSplit = line.toString().split(ingredientDelimiter); final baseMatch = reBase.firstMatch(lineSplit[0]); if (baseMatch != null) { String ingredients = ''; if (lineSplit.length > 1) { - final lineIng = lineSplit[1].replaceFirst(RegExp(r'。?$'), ''); + final lineIng = lineSplit[1].replaceFirst(RegExp(r'[^)]*$'), ''); for (final ing in lineIng.split('、')) { final match = reIngredient.firstMatch(ing); if (match != null) { @@ -381,47 +388,43 @@ class _ReplenisherTransformer extends ModelTransformer { class _OATransformer extends ModelTransformer { const _OATransformer(super.target); - static const baseTmp = r'第%num種屬性叫做 %name,屬於 %mode 類型。'; - @override - List getHeader() => ['${target.length} 種顧客屬性']; + List getHeader() => [S.transitPTFormatModelOaMetaOa(target.length)]; @override List> getRows() { int counter = 1; return [ - ['共設定 ${target.length} 種顧客屬性'], - ...target.itemList.map((attr) { - final base = baseTmp.f({ - 'num': counter++, - 'name': attr.name, - 'mode': S.orderAttributeModeNames(attr.mode.name), - }); - if (attr.isEmpty) { - return ['$base它並沒有設定選項。']; - } - - final attrs = attr.itemList.map((e) { - final info = [ - e.isDefault ? '預設' : '', - e.modeValue == null ? '' : '選項的值為 ${e.modeValue}', + [S.transitPTFormatModelOaHeader(target.length)], + target.itemList.map((attr) { + String details = attr.itemList.map((e) { + final details = [ + e.isDefault ? S.transitPTFormatModelOaDefaultOption : '', + e.modeValue == null ? '' : S.transitPTFormatModelOaModeValue(e.modeValue!), ].where((e) => e.isNotEmpty).join(','); - return info.isEmpty ? e.name : '${e.name}($info)'; + return details.isEmpty ? e.name : '${e.name}($details)'; }).join('、'); - - return ['$base它有 ${attr.length} 個選項:$attrs']; - }) + details = details.isEmpty ? '' : ':$details'; + + return S.transitPTFormatModelOaOa( + (counter++).toString(), + attr.name, + S.orderAttributeModeName(attr.mode.name), + S.transitPTFormatModelOaOaDetails(attr.length) + details, + ); + }).toList(), ]; } @override List> parseRows(List> rows) { final reOA = RegExp(_rePre + - baseTmp.f({ - 'num': r'\d+', - 'name': r'(?[^,]+?)', - 'mode': r'(?[^ ]+?)', - })); + S.transitPTFormatModelOaOa( + _reInt, + r'(?.+?)', + r'(?.+)', + '.*', + )); final result = >[]; for (final line in rows[0]) { @@ -436,7 +439,7 @@ class _OATransformer extends ModelTransformer { String info = 'false'; if (infoIdx != -1) { final infoStr = opt.substring(infoIdx + 1, opt.length - 1); - if (infoStr.startsWith('預設')) { + if (infoStr.startsWith(S.transitPTFormatModelOaDefaultOption)) { info = 'true'; } final v = RegExp(_reDig).firstMatch(infoStr)?.group(0) ?? ''; @@ -457,13 +460,3 @@ class _OATransformer extends ModelTransformer { return result; } } - -extension _StringExtension on String { - String f(Map params) { - String result = this; - for (final entry in params.entries) { - result = result.replaceFirst('%${entry.key}', entry.value.toString()); - } - return result; - } -} diff --git a/lib/helpers/util.dart b/lib/helpers/util.dart index 3102f47d..3aa8607a 100644 --- a/lib/helpers/util.dart +++ b/lib/helpers/util.dart @@ -46,7 +46,7 @@ class Util { return Center(child: Text(error.toString())); } - if (!snapshot.hasData) { + if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: SizedBox( height: 20, @@ -63,28 +63,26 @@ class Util { } } -extension PrettyNum on num { - static final _format = NumberFormat.compact(locale: 'zh_TW'); - - /// TODO: After currency is implemented, we need to change this to use currency formatter. - /// 4.444 -> 4.44 - /// 44.44 -> 44.4 - /// 444.4 -> 444 - /// 4444 -> 4444 - /// 44444 -> 4.44萬 - /// 444444 -> 44.4萬 - /// 4444444 -> 444萬 - /// 44444444 -> 4444萬 - /// 444444444 -> 4.44億 - String prettyString() { - return _format.format(this); +extension RangeFormat on DateTimeRange { + String format(String local) { + final thisYear = DateTime.now().year; + final fs = start.year == thisYear ? DateFormat.MMMd(local) : DateFormat.yMMMd(local); + if (duration.inDays == 1) { + return fs.format(start); + } + + final fe = end.year == thisYear ? DateFormat.MMMd(local) : DateFormat.yMMMd(local); + return '${fs.format(start)} - ${fe.format(end.subtract(const Duration(days: 1)))}'; } -} -extension RangeFormat on DateTimeRange { - String format(DateFormat f) { - return duration.inDays == 1 - ? f.format(start) - : '${f.format(start)} - ${f.format(end.subtract(const Duration(days: 1)))}'; + String formatCompact(String local) { + final thisYear = DateTime.now().year; + final fs = start.year == thisYear ? DateFormat('MMdd', local) : DateFormat('yMMdd', local); + if (duration.inDays == 1) { + return fs.format(start); + } + + final fe = end.year == thisYear ? DateFormat('MMdd', local) : DateFormat('yMMdd', local); + return '${fs.format(start)} - ${fe.format(end.subtract(const Duration(days: 1)))}'; } } diff --git a/lib/helpers/validator.dart b/lib/helpers/validator.dart index ef7ec5e5..3be1e412 100644 --- a/lib/helpers/validator.dart +++ b/lib/helpers/validator.dart @@ -17,7 +17,7 @@ class Validator { error = S.invalidNumberType(fieldName); } } else if (number < 0) { - error = S.invalidPositiveNumber(fieldName); + error = S.invalidNumberPositive(fieldName); } else if (maximum != null && maximum < number) { error = S.invalidNumberMaximum(fieldName, maximum); } @@ -46,7 +46,7 @@ class Validator { error = S.invalidIntegerType(fieldName); } } else if (number < 0) { - error = S.invalidPositiveNumber(fieldName); + error = S.invalidNumberPositive(fieldName); } else if (maximum != null && maximum < number) { error = S.invalidNumberMaximum(fieldName, maximum); } else if (minimum != null && minimum > number) { @@ -94,7 +94,7 @@ class Validator { String? error; if (value == null || value.isEmpty) { - error = S.invalidEmptyString(fieldName); + error = S.invalidStringEmpty(fieldName); } else if (value.characters.length > limit) { error = S.invalidStringMaximum(fieldName, limit); } else if (validator != null) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a492b588..f3879804 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,72 +1,1859 @@ { - "appTitle": "POS System", - "settingThemeTitle": "Platte", - "settingThemeTypes": "{type, select, dark{Dark Mode} light{Light Mode} system{Follow System} other{UNKNOWN}}", + "@@locale": "en", + "@@last_modified": "2024-05-18T08:05:55.647664Z", + "@@author": "Lu Shueh Chou", + "settingTab": "Settings", + "settingVersion": "Version: {version}", + "@settingVersion": { + "description": "Display the app version", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "settingWelcome": "Hi, {name}", + "@settingWelcome": { + "description": "Display user's name", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingLogoutBtn": "Log Out", + "settingElfTitle": "Suggestions", + "settingElfDescription": "Provide feedback using Google Forms", + "settingElfContent": "Feel like something's missing here?\nFeel free to [give suggestions](https://forms.gle/s8V5SXuqhA1u3zmt7).\nYou can also check out [upcoming features](https://github.com/evan361425/flutter-pos-system/milestones).", + "settingFeatureTitle": "Other Settings", + "settingFeatureDescription": "Appearance, Language, Tips", + "settingThemeTitle": "Theme", + "settingThemeName": "{name, select, dark{Dark Mode} light{Light Mode} system{Follow System} other{UNKNOWN}}", + "@settingThemeName": { + "description": "Appearance of the app", + "placeholders": { + "name": { + "type": "String" + } + } + }, "settingLanguageTitle": "Language", - "dialogDeletionTitle": "Deletion Confirm", - "menuTitle": "Menu", - "menuCatalogMetaTitle": "Catalog", - "menuProductMetaTitle": "Product", - "stockReplenishmentTitle": "Replenishment list", - "stockReplenishmentApplyConfirmTitle": "Apply this replenishment?", - "stockReplenishmentIngredientListTitle": "Set different ingredients for replenishment", - "quantityTitle": "Quantity", - "featureRequestTitle": "Feature Request", - "settingTitle": "Other Setting", - "settingCashierWarningTitle": "Cashier Warning", - "settingOrderOutlookTitle": "Order Outlook", - "settingOrderAwakeningTitle": "Always awake when ordering", - "orderSetAttributeTitle": "Customer Setting", - "orderCashierTitle": "Cashier", - "orderObjectAttributeTitle": "Customer Setting", - "orderObjectAttributeCount": "{count} entry", - "orderObjectProductTitle": "Products", - "orderObjectProductsCount": "{count} products", - "transitTitle": "Transit", - "importPreviewerTitle": "Preview result", - "homeTabAnalysis": "Analysis", - "homeTabStock": "Stock", - "homeTabCashier": "Cashier", - "homeTabSetting": "Setting", + "settingOrderOutlookTitle": "Ordering Outlook", + "settingOrderOutlookName": "{name, select, slidingPanel{Sliding Panel} singleView{Classic Mode} other{UNKNOWN}}", + "@settingOrderOutlookName": { + "description": "Appearance during ordering", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingOrderOutlookTip": "{name, select, slidingPanel{Panel slides up during ordering, suitable for small-screen phones} singleView{All info displayed on a single screen, suitable for large-screen tablets} other{UNKNOWN}}", + "@settingOrderOutlookTip": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingCheckoutWarningTitle": "Cash Registry Warnings", + "settingCheckoutWarningName": "{name, select, showAll{Show All} onlyNotEnough{Show Only When Not Enough} hideAll{Hide All} other{UNKNOWN}}", + "@settingCheckoutWarningName": { + "description": "Whether to display cash registry warnings", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingCheckoutWarningTip": "{name, select, showAll{Show warning when using smaller denominations to give change.\nFor example, if $5 is not enough, start using 5 $1 bills for change.} onlyNotEnough{Show warning when cash registry not enough money.} hideAll{Won't display any warnings during ordering.} other{UNKNOWN}}", + "@settingCheckoutWarningTip": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingOrderProductCountTitle": "Products per Row during Ordering", + "settingOrderProductCountHint": "Set to \"0\" to display only text during ordering", + "settingOrderProductCountMinLabel": "Text Only", + "settingOrderAwakeningTitle": "Keep Screen On During Ordering", + "@settingOrderAwakeningTitle": { + "description": "Keep the screen on during ordering, even when idle" + }, + "settingOrderAwakeningDescription": "If disabled, the screen will turn off based on system settings during ordering.", + "settingReportTitle": "Collect Error Messages and Events", + "settingReportDescription": "Send error messages when the app encounters issues, helping the app improve", + "stockTab": "Inventory", + "stockUpdatedAt": "Last Restock: {updatedAt}", + "@stockUpdatedAt": { + "placeholders": { + "updatedAt": { + "type": "DateTime", + "format": "MMMEd" + } + } + }, + "stockIngredientEmptyBody": "Once ingredients are added, you can start tracking their inventory!", + "stockIngredientTitleCreate": "Add Ingredient", + "stockIngredientTitleUpdate": "Edit Ingredient", + "stockIngredientTitleUpdateAmount": "Edit Inventory", + "stockIngredientTutorialTitle": "Add Ingredient", + "stockIngredientTutorialContent": "Ingredients help us track product inventory.\n\nYou can add ingredients in \"Menu\"\nand then manage inventory here.", + "stockIngredientDialogDeletionContent": "{count, plural, =0{No products currently use this ingredient} other{Deleting this ingredient will also remove it from {count} products}}", + "@stockIngredientDialogDeletionContent": { + "description": "Indicates how many products will be affected when deleting the ingredient", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "stockIngredientProductsCount": "{count} products using it", + "@stockIngredientProductsCount": { + "description": "When editing an ingredient, it indicates how many products are using it and allows for navigation to the product page", + "placeholders": { + "count": { + "type": "int", + "description": "Number of products" + } + } + }, + "stockIngredientNameLabel": "Ingredient Name", + "stockIngredientNameHint": "e.g., Cheese", + "stockIngredientNameErrorRepeat": "Ingredient name already exists", + "stockIngredientAmountLabel": "Current Amount", + "stockIngredientAmountMaxLabel": "Maximum Amount", + "stockIngredientAmountMaxHelper": "Setting this value helps you see how much of the ingredient is being used.\nLeave blank or don't fill it in, and the value will automatically be set each time inventory is increased.", + "stockIngredientAmountShortHelper": "If not set maximum amount, every time increase the amount will be considered as the maximum amount", + "@stockIngredientAmountShortHelper": { + "description": "Auxiliary text used for quickly increasing inventory" + }, + "stockReplenishmentButton": "Purchase", + "stockReplenishmentEmptyBody": "Purchasing helps you quickly adjust ingredient inventory", + "stockReplenishmentTitleList": "Purchase List", + "stockReplenishmentTitleCreate": "Add Purchase", + "stockReplenishmentTitleUpdate": "Edit Purchase", + "stockReplenishmentMetaAffect": "Affects {count} Ingredients", + "@stockReplenishmentMetaAffect": { + "description": "Indicates in the purchase list how many ingredients are affected", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "stockReplenishmentNever": "Never Restocked", + "@stockReplenishmentNever": { + "description": "The stock page displays the last restock time; if never restocked, this text is set" + }, + "stockReplenishmentApplyButton": "Apply Purchase", + "stockReplenishmentApplyConfirmButton": "Apply", + "stockReplenishmentApplyConfirmTitle": "Apply Purchase?", + "stockReplenishmentApplyConfirmColumn": "{value, select, name{Name} amount{Amount} other{UNKNOWN}}", + "@stockReplenishmentApplyConfirmColumn": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "stockReplenishmentApplyConfirmHint": "After apply, following ingredients will be adjusted", + "stockReplenishmentTutorialTitle": "Ingredient Purchases", + "stockReplenishmentTutorialContent": "Through purchases, you no longer need to set the inventory of each ingredient one by one.\nSet up purchases now and adjust multiple ingredients at once!", + "stockReplenishmentNameLabel": "Purchase Name", + "stockReplenishmentNameHint": "e.g., Costco Purchase", + "stockReplenishmentNameErrorRepeat": "Purchase name already exists", + "stockReplenishmentIngredientsDivider": "Ingredients", + "stockReplenishmentIngredientsHelper": "Click to set the quantity of different ingredients to be purchased", + "stockReplenishmentIngredientAmountHint": "Set the amount to increase/decrease", + "stockQuantityTitle": "Quantity", + "stockQuantityDescription": "Half Sugar, Low Sugar, etc.", + "stockQuantityTitleCreate": "Add Quantity", + "stockQuantityTitleUpdate": "Edit Quantity", + "stockQuantityEmptyBody": "Quantity allows for quick adjustments to the amount of ingredients, such as:\nHalf Sugar, Low Sugar.", + "stockQuantityMetaProportion": "Default Ratio: {proportion}", + "@stockQuantityMetaProportion": { + "description": "Text explaining default ratios in subheadings of quantity items", + "placeholders": { + "proportion": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "stockQuantityDialogDeletionContent": "{count, plural, =0{No product ingredients currently use this quantity} other{Deleting this quantity will also remove it from {count} product ingredients}}", + "@stockQuantityDialogDeletionContent": { + "description": "Indicates how many product ingredients will be affected when deleting the quantity", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "stockQuantityNameLabel": "Quantity Name", + "stockQuantityNameHint": "e.g., Small or Large", + "stockQuantityNameErrorRepeat": "Quantity name already exists", + "stockQuantityProportionLabel": "Default Ratio", + "stockQuantityProportionHelper": "Applied when this quantity is used for an ingredient.\n\nFor example:\nif this quantity is \"Large\" and the default ratio is \"1.5\",\nand there's a product \"Cheeseburger\" with the ingredient \"Cheese,\"\nwhich uses \"2\" units of cheese per burger,\nwhen adding this quantity,\nthe quantity of \"Cheese\" will automatically be set to \"3\" (2 * 1.5).\n\nIf set to \"1,\" there's no effect.\n\nIf set to \"0,\" the ingredient won't be used.", + "transitTitle": "Data Transfer", + "transitDescription": "Importing and Exporting Store Information and Orders", + "transitTutorialTitle": "Sync Multiple Devices", + "transitTutorialContent": "This is where you can import/export menu, inventory, order records, and other information.\n\nWe provide two methods: Google Sheets and plain text, making it convenient to sync data across different devices.", + "transitMethodTitle": "Please Select Transfer Method", + "transitMethodName": "{name, select, googleSheet{Google Sheets} plainText{Plain Text} other{UNKNOWN}}", + "@transitMethodName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitCatalogName": "{name, select, order{Order Records} model{Store Information} other{UNKNOWN}}", + "@transitCatalogName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitCatalogHelper": "{name, select, order{Export order info for detailed statistical analysis.} model{Store info is usually used to sync menu, inventory, etc., to third-party locations or to import to another device.} other{UNKNOWN}}", + "@transitCatalogHelper": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitModelName": "{name, select, menu{Menu} stock{Inventory} quantities{Quantities} replenisher{Replenisher} orderAttr{Customer Settings} order{Order} orderDetailsAttr{Order Customer Settings} orderDetailsProduct{Order Product Details} orderDetailsIngredient{Order Ingredient Details} other{UNKNOWN}}", + "@transitModelName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitOrderMetaRange": "Orders for {range}", + "@transitOrderMetaRange": { + "placeholders": { + "range": { + "type": "String", + "example": "01/01 - 01/31" + } + } + }, + "transitOrderMetaRangeDays": "Data for {days} Days", + "@transitOrderMetaRangeDays": { + "placeholders": { + "days": { + "type": "int" + } + } + }, + "transitOrderCapacityTitle": "Estimated Capacity: {size}", + "@transitOrderCapacityTitle": { + "placeholders": { + "size": { + "type": "String" + } + } + }, + "transitOrderCapacityContent": "High capacity may cause execution errors. It's recommended to perform in batches and not export too many records at once.", + "transitOrderCapacityOk": "Capacity Okay", + "transitOrderCapacityWarn": "Capacity Warning", + "transitOrderCapacityDanger": "Capacity Danger", + "transitOrderItemTitle": "{date}", + "@transitOrderItemTitle": { + "placeholders": { + "date": { + "type": "DateTime", + "format": "MMM d HH:mm:ss", + "isCustomDateFormat": "true" + } + } + }, + "transitOrderItemMetaProductCount": "Product Count: {count}", + "@transitOrderItemMetaProductCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitOrderItemMetaPrice": "Total Price: {price}", + "@transitOrderItemMetaPrice": { + "placeholders": { + "price": { + "type": "String" + } + } + }, + "transitOrderItemDialogTitle": "Order Details", + "transitExportPreviewBtn": "Preview", + "transitExportPreviewTitle": "Preview Output Result", + "transitExportBtn": "Import", + "transitImportPreviewBtn": "Preview", + "transitImportPreviewTitle": "Preview Import Result", + "transitImportPreviewHeader": "Note: Importing will remove the data not listed below. Please confirm before executing!", + "transitImportPreviewIngredientMetaAmount": "Amount: {amount}", + "@transitImportPreviewIngredientMetaAmount": { + "placeholders": { + "amount": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "transitImportPreviewIngredientMetaMaxAmount": "{exist, plural, =0{Not Set} other{Max Value: {value}}}", + "@transitImportPreviewIngredientMetaMaxAmount": { + "placeholders": { + "exist": { + "type": "int" + }, + "value": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "transitImportPreviewIngredientHeader": "After import, old ingredients won't be removed to avoid affecting the \"Menu\" status.", + "transitImportPreviewQuantityHeader": "After import, old quantities won't be removed to avoid affecting the \"Menu\" status.", + "transitImportBtn": "Export", + "transitImportErrorColumnCount": "Insufficient data, {columns} columns required", + "@transitImportErrorColumnCount": { + "placeholders": { + "columns": { + "type": "int" + } + } + }, + "transitImportErrorDuplicate": "This line will be ignored as the same item appeared earlier", + "transitImportColumnStatus": "{name, select, normal{(Normal)} staged{(New)} stagedIng{(New Ingredient)} stagedQua{(New Quantity)} updated{(Updated)} other{UNKNOWN}}", + "@transitImportColumnStatus": { + "description": "Additional status of the data displayed", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSDescription": "Google Sheets is a powerful mini-database. After exporting, it can be customized for various analyses!", + "transitGSSheetNameLabel": "Sheet Title of {name}", + "@transitGSSheetNameLabel": { + "description": "Label of title", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSSheetNameUpdate": "Modify Title", + "transitGSSpreadsheetLabel": "Spreadsheet", + "transitGSSpreadsheetActionSelect": "Select Spreadsheet", + "transitGSSpreadsheetActionClear": "Clear Selection", + "transitGSSpreadsheetExportEmptyLabel": "Create & Export", + "transitGSSpreadsheetExportEmptyHint": "Create a new spreadsheet \"{name}\" and export data to it.", + "@transitGSSpreadsheetExportEmptyHint": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSSpreadsheetExportExistLabel": "Specify & Export", + "@transitGSSpreadsheetExportExistLabel": { + "description": "Inform the user that data will be exported to the specified spreadsheet." + }, + "transitGSSpreadsheetExportExistHint": "Export to spreadsheet \"{name}\"", + "@transitGSSpreadsheetExportExistHint": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSSpreadsheetImportAllBtn": "Import All", + "transitGSSpreadsheetImportAllHint": "There will be no preview screen, directly overwrite all data.", + "transitGSSpreadsheetImportAllConfirmTitle": "Import All Data?", + "transitGSSpreadsheetImportAllConfirmContent": "All data from the selected sheets will be downloaded and completely overwrite local data.\nThis action cannot be undone.", + "transitGSSpreadsheetImportExistLabel": "Load Sheets Name", + "transitGSSpreadsheetImportExistHint": "Get all sheet names from the spreadsheet and ready to import.", + "transitGSSpreadsheetImportEmptyLabel": "Select Spreadsheet", + "transitGSSpreadsheetImportEmptyHint": "Once you choose the spreadsheet to import, you can start importing data.", + "transitGSSpreadsheetConfirm": "This action will {hint}", + "@transitGSSpreadsheetConfirm": { + "placeholders": { + "hint": { + "type": "String" + } + } + }, + "transitGSSpreadsheetSelectionHint": "{name, select, _{Enter the spreadsheet URL or spreadsheet ID} other{The current spreadsheet is \"{name}\"}}", + "@transitGSSpreadsheetSelectionHint": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSSpreadsheetModelDefaultName": "POS System Data", + "transitGSSpreadsheetModelExportDivider": "Select types to export", + "transitGSSpreadsheetModelImportDivider": "Select sheet to import", + "transitGSSpreadsheetOrderDefaultName": "POS System Orders", + "transitGSSpreadsheetSnackbarAction": "Open", + "transitGSProgressStatusAddSpreadsheet": "Adding Spreadsheet...", + "transitGSProgressStatusAddSheets": "Adding Sheets...", + "transitGSProgressStatusVerifyUser": "Verifying Identity", + "transitGSProgressStatusFetchLocalOrders": "Retrieving Local Data...", + "transitGSProgressStatusOverwriteOrders": "Overwriting Order Data...", + "transitGSProgressStatusAppendOrders": "Appended to {name}", + "@transitGSProgressStatusAppendOrders": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSModelStatus": "{model, select, menu{Updating Menu...} stock{Updating Inventory...} quantities{Updating Quantities...} replenisher{Updating Replenisher...} orderAttr{Exporting Customer Settings...} order{Exporting Orders...} orderDetailsAttr{Exporting Order Customer Settings...} orderDetailsProduct{Exporting Order Product Details...} orderDetailsIngredient{Exporting Order Ingredient Details...} other{UNKNOWN}}", + "@transitGSModelStatus": { + "placeholders": { + "model": { + "type": "String" + } + } + }, + "transitGSModelProductIngredientTitle": "Ingredient Information", + "transitGSModelProductIngredientNote": "Information of all product ingredients, format as follows:\n- Ingredient 1, Default usage amount\n + Quantity a, Additional usage amount, Additional price, Additional cost\n + Quantity b, Additional usage amount, Additional price, Additional cost\n- Ingredient 2, Default usage amount", + "transitGSModelReplenishmentTitle": "Replenishment Amount", + "transitGSModelReplenishmentNote": "The amount of specific ingredients during each replenishment, format as follows:\n- Ingredient 1, Replenishment amount\n- Ingredient 2, Replenishment amount", + "transitGSModelAttributeOptionTitle": "Customer Setting Options", + "transitGSModelAttributeOptionHeaderTs": "Timestamp", + "transitGSModelAttributeOptionHeaderMode": "Type", + "transitGSModelAttributeOptionHeaderOptions": "Options", + "transitGSModelAttributeOptionNote": "\"Options\" will have different meanings depending on the type of customer settings, format as follows:\n- Option 1, Is default, Option value\n- Option 2, Is default, Option value", + "transitGSOrderSettingTitle": "Order Export Settings", + "transitGSOrderSettingOverwriteLabel": "Overwrite Sheet", + "transitGSOrderSettingOverwriteHint": "Overwriting the sheet will start exporting from the first row.", + "transitGSOrderSettingTitlePrefixLabel": "Add Date Prefix", + "transitGSOrderSettingTitlePrefixHint": "Add a date prefix to the sheet name, for example, \"0101 - 0131 Order Data\".", + "transitGSOrderSettingRecommendCombination": "When not overwriting and using append instead, it's recommended not to add a date prefix to the form name.", + "transitGSOrderSettingNameLabel": "Sheet Name", + "transitGSOrderSettingNameHelper": "Splitting the sheet allows for more flexible data analysis,\nfor example, you can query the total usage of a certain ingredient in order details.", + "transitGSOrderMetaOverwrite": "{value, select, true{Will overwrite} false{Won't overwrite} other{UNKNOWN}}", + "@transitGSOrderMetaOverwrite": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "transitGSOrderMetaTitlePrefix": "{value, select, true{Has date prefix} false{No date prefix} other{UNKNOWN}}", + "@transitGSOrderMetaTitlePrefix": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "transitGSOrderMetaMemoryWarning": "The capacity here represents the amount consumed by network transmission, the actual cloud memory occupied may be only one percent of this value.\nFor detailed capacity limit explanations, please refer to [this document](https://developers.google.com/sheets/api/limits#quota).", + "transitGSOrderHeaderTs": "Timestamp", + "transitGSOrderHeaderTime": "Time", + "transitGSOrderHeaderPrice": "Price", + "transitGSOrderHeaderProductPrice": "Product Price", + "transitGSOrderHeaderPaid": "Paid", + "transitGSOrderHeaderCost": "Cost", + "transitGSOrderHeaderProfit": "Profit", + "transitGSOrderHeaderItemCount": "Item Count", + "@transitGSOrderHeaderItemCount": { + "description": "how many items in the order" + }, + "transitGSOrderHeaderTypeCount": "Type Count", + "@transitGSOrderHeaderTypeCount": { + "description": "how many types of products in the order" + }, + "transitGSOrderAttributeTitle": "Order Customer Settings", + "transitGSOrderAttributeHeaderTs": "Timestamp", + "transitGSOrderAttributeHeaderName": "Setting Category", + "transitGSOrderAttributeHeaderOption": "Option", + "transitGSOrderProductTitle": "Order Product Details", + "transitGSOrderProductHeaderTs": "Timestamp", + "transitGSOrderProductHeaderName": "Product", + "transitGSOrderProductHeaderCatalog": "Category", + "transitGSOrderProductHeaderCount": "Quantity", + "transitGSOrderProductHeaderPrice": "Single Price", + "transitGSOrderProductHeaderCost": "Single Cost", + "transitGSOrderProductHeaderOrigin": "Original Price", + "transitGSOrderIngredientTitle": "Order Ingredient Details", + "transitGSOrderIngredientHeaderTs": "Timestamp", + "transitGSOrderIngredientHeaderName": "Ingredient", + "transitGSOrderIngredientHeaderQuantity": "Quantity", + "transitGSOrderIngredientHeaderAmount": "Amount", + "transitGSOrderExpandableHint": "See next table", + "transitGSErrorCreateSpreadsheet": "Unable to Create Spreadsheet", + "transitGSErrorCreateSpreadsheetHelper": "Don't worry, it's usually easy to solve!\nPossible reasons include:\n• Unstable network conditions;\n• POS system not authorized to edit spreadsheets.", + "transitGSErrorSpreadsheetEmpty": "Please Select a Spreadsheet First", + "transitGSErrorSpreadsheetIdEmpty": "Cannot be Empty", + "transitGSErrorSpreadsheetIdInvalid": "Invalid text. It must include:\n/spreadsheets/d//\nOr provide the ID directly (combination of letters, numbers, underscores, and hyphens).", + "transitGSErrorCreateSheet": "Unable to Create Sheet in Spreadsheet", + "transitGSErrorCreateSheetHelper": "Don't worry, it's usually easy to solve!\nPossible reasons include:\n• Unstable network conditions;\n• POS system not authorized to create sheets;\n• Misspelled spreadsheet ID, try copying the entire URL and pasting it;\n• The spreadsheet has been deleted.", + "transitGSErrorSheetRepeat": "Sheet name duplicate", + "transitGSErrorSheetEmpty": "Please select at least one sheet to export", + "transitGSErrorNonExistName": "Spreadsheet not found, has it been deleted?", + "transitGSErrorImportEmptySpreadsheet": "Must select a spreadsheet to import", + "transitGSErrorImportEmptySheet": "Must select a specific sheet to import", + "transitGSErrorImportEmptyData": "No values found in sheet", + "transitGSErrorImportNotFoundSpreadsheet": "Spreadsheet not found", + "transitGSErrorImportNotFoundSheets": "No data found for sheet \"{name}\"", + "@transitGSErrorImportNotFoundSheets": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSErrorImportNotFoundHelper": "Don't worry, it's usually easy to solve!\nPossible reasons include:\n• Unstable network conditions;\n• POS system not authorized to read sheets;\n• Misspelled spreadsheet ID, try copying the entire URL and pasting it;\n• The spreadsheet has been deleted.", + "transitPTDescription": "Quick check, quick share.", + "transitPTCopyBtn": "Copy Text", + "transitPTCopySuccess": "Copied successfully", + "transitPTCopyWarning": "Copying too much text may cause system crash", + "transitPTImportHint": "Paste copied text here", + "transitPTImportHelper": "After pasting the text, it will analyze and determine the type of information to import.", + "transitPTImportErrorNotFound": "This text cannot match any corresponding service. Please refer to the exported text content.", + "transitPTFormatOrderPrice": "{hasProducts, plural, =0{Total price ${price}.} other{Total price ${price}, {productsPrice} of them are product price.}}", + "@transitPTFormatOrderPrice": { + "placeholders": { + "hasProducts": { + "type": "int" + }, + "price": { + "type": "String" + }, + "productsPrice": { + "type": "String" + } + } + }, + "transitPTFormatOrderMoney": "Paid ${paid}, cost ${cost}.", + "@transitPTFormatOrderMoney": { + "placeholders": { + "paid": { + "type": "String" + }, + "cost": { + "type": "String" + } + } + }, + "transitPTFormatOrderProductCount": "{count, plural, =0{There is no product.} =1{There is 1 product details are:\n{products}.} other{There are {count} products ({setCount} types of set) including:\n{products}.}}", + "@transitPTFormatOrderProductCount": { + "placeholders": { + "count": { + "type": "int" + }, + "setCount": { + "type": "int" + }, + "products": { + "type": "String" + } + } + }, + "transitPTFormatOrderProduct": "{hasIngredient, plural, =0{{count} of {product} ({catalog}), total price is ${price}, no ingredient settings} other{{count} of {product} ({catalog}), total price is ${price}, ingredients are {ingredients}}}", + "@transitPTFormatOrderProduct": { + "placeholders": { + "hasIngredient": { + "type": "int" + }, + "product": { + "type": "String" + }, + "catalog": { + "type": "String" + }, + "count": { + "type": "int" + }, + "price": { + "type": "String" + }, + "ingredients": { + "type": "String" + } + } + }, + "transitPTFormatOrderIngredient": "{amount, plural, =0{{ingredient} ({quantity})} other{{ingredient} ({quantity}), used {amount}}}", + "@transitPTFormatOrderIngredient": { + "description": "Details of ingredients and quantities for each product in the order list", + "placeholders": { + "amount": { + "type": "num", + "format": "decimalPattern" + }, + "ingredient": { + "type": "String" + }, + "quantity": { + "type": "String" + } + } + }, + "transitPTFormatOrderNoQuantity": "default quantity", + "transitPTFormatOrderOrderAttribute": "Customer's {options}.", + "@transitPTFormatOrderOrderAttribute": { + "placeholders": { + "options": { + "type": "String" + } + } + }, + "transitPTFormatOrderOrderAttributeItem": "{name} is {option}", + "@transitPTFormatOrderOrderAttributeItem": { + "placeholders": { + "name": { + "type": "String" + }, + "option": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuMetaCatalog": "{count} categories", + "@transitPTFormatModelMenuMetaCatalog": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelMenuMetaProduct": "{count} products", + "@transitPTFormatModelMenuMetaProduct": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelMenuHeader": "This menu has {catalogs} categories, {products} products.", + "@transitPTFormatModelMenuHeader": { + "placeholders": { + "catalogs": { + "type": "int" + }, + "products": { + "type": "int" + } + } + }, + "transitPTFormatModelMenuHeaderPrefix": "This menu has", + "@transitPTFormatModelMenuHeaderPrefix": { + "description": "This is used to check if this text is a menu" + }, + "transitPTFormatModelMenuCatalog": "Category {index} is called {catalog} and {details}.", + "@transitPTFormatModelMenuCatalog": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "catalog": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuCatalogDetails": "{count, plural, =0{it has no product} =1{it has one product} other{it has {count} products}}", + "@transitPTFormatModelMenuCatalogDetails": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelMenuProduct": "Product {index} is called {name}, with price at ${price}, cost ${cost} and {details}", + "@transitPTFormatModelMenuProduct": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "price": { + "type": "String" + }, + "cost": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuProductDetails": "{count, plural, =0{it has no ingredient.} =1{it has one ingredient: {names}.\nEach product requires {details}.} other{it has {count} ingredients: {names}.\nEach product requires {details}.}}", + "@transitPTFormatModelMenuProductDetails": { + "placeholders": { + "count": { + "type": "int" + }, + "names": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuIngredient": "{amount} of {name} and {details}", + "@transitPTFormatModelMenuIngredient": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "amount": { + "type": "String" + }, + "name": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuIngredientDetails": "{count, plural, =0{it is unable to adjust quantity} =1{it also has one different quantity {quantities}} other{it also has {count} different quantities {quantities}}}", + "@transitPTFormatModelMenuIngredientDetails": { + "placeholders": { + "count": { + "type": "int" + }, + "quantities": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuQuantity": "quantity {amount} with additional price ${price} and cost ${cost}", + "@transitPTFormatModelMenuQuantity": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "amount": { + "type": "String" + }, + "price": { + "type": "String" + }, + "cost": { + "type": "String" + } + } + }, + "transitPTFormatModelStockMetaIngredient": "{count} ingredients", + "@transitPTFormatModelStockMetaIngredient": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelStockHeader": "The inventory has {count} ingredients in total.", + "@transitPTFormatModelStockHeader": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelStockHeaderPrefix": "The inventory has", + "@transitPTFormatModelStockHeaderPrefix": { + "description": "This is used to check if this text is stock" + }, + "transitPTFormatModelStockIngredient": "Ingredient at {index} is called {name}, with {amount} amount{details}.", + "@transitPTFormatModelStockIngredient": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "amount": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelStockIngredientDetails": "{exist, plural, =0{} other{, with a maximum of {max} pieces}}", + "@transitPTFormatModelStockIngredientDetails": { + "description": "String(max) are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "exist": { + "type": "int" + }, + "max": { + "type": "String" + } + } + }, + "transitPTFormatModelQuantitiesMetaQuantity": "{count} quantities", + "@transitPTFormatModelQuantitiesMetaQuantity": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelQuantitiesHeader": "{count} quantities have been set.", + "@transitPTFormatModelQuantitiesHeader": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelQuantitiesHeaderSuffix": "quantities have been set.", + "@transitPTFormatModelQuantitiesHeaderSuffix": { + "description": "This is used to check if this text is quantities" + }, + "transitPTFormatModelQuantitiesQuantity": "Quantity at {index} is called {name}, which defaults to multiplying ingredient quantity by {prop}.", + "@transitPTFormatModelQuantitiesQuantity": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "prop": { + "type": "String" + } + } + }, + "transitPTFormatModelReplenisherMetaReplenishment": "{count} replenishment methods", + "@transitPTFormatModelReplenisherMetaReplenishment": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelReplenisherHeader": "{count} replenishment methods have been set.", + "@transitPTFormatModelReplenisherHeader": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelReplenisherHeaderSuffix": "replenishment methods have been set.", + "@transitPTFormatModelReplenisherHeaderSuffix": { + "description": "This is used to check if this text is replenishment quantity" + }, + "transitPTFormatModelReplenisherReplenishment": "Replenishment method at {index} is called {name}, {details}.", + "@transitPTFormatModelReplenisherReplenishment": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelReplenisherReplenishmentDetails": "{count, plural, =0{it will not adjust inventory} other{it will adjust the inventory of {count} ingredients}}", + "@transitPTFormatModelReplenisherReplenishmentDetails": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelOaMetaOa": "{count} customer attributes", + "@transitPTFormatModelOaMetaOa": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelOaHeader": "{count} customer attributes have been set.", + "@transitPTFormatModelOaHeader": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelOaHeaderSuffix": "customer attributes have been set.", + "@transitPTFormatModelOaHeaderSuffix": { + "description": "This is used to check if this text is customer settings" + }, + "transitPTFormatModelOaOa": "Attribute at {index} is called {name}, belongs to {mode} type, {details}.", + "@transitPTFormatModelOaOa": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "mode": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelOaOaDetails": "{count, plural, =0{it has no options} =1{it has one option} other{it has {count} options}}", + "@transitPTFormatModelOaOaDetails": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelOaDefaultOption": "default", + "transitPTFormatModelOaModeValue": "option value is {value}", + "@transitPTFormatModelOaModeValue": { + "placeholders": { + "value": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "appTitle": "POS System", + "actSuccess": "Successful!", + "@actSuccess": { + "description": "Action executed successfully and displayed on the Snackbar." + }, + "actError": "Unknown error occurred.", + "@actError": { + "description": "Error message displayed on the Snackbar when an error occurs." + }, + "actMoreInfo": "More", + "@actMoreInfo": { + "description": "Button on the Snackbar to show more details." + }, + "singleChoice": "Select One", + "@singleChoice": { + "description": "Reminder to the user that only one option can be selected at a time." + }, + "multiChoices": "Select Multiple", + "@multiChoices": { + "description": "Reminder to the user that multiple options can be selected." + }, + "totalCount": "{count, plural, =0{No Items} =1{{count} item} other{{count} items}}", + "@totalCount": { + "description": "Total count displayed on the ListView.", + "placeholders": { + "count": { + "type": "int", + "format": "compactLong" + } + } + }, + "searchCount": "Found {count} results", + "@searchCount": { + "description": "Total count displayed on the SearchScaffold.", + "placeholders": { + "count": { + "type": "int", + "format": "compact" + } + } + }, + "dialogDeletionTitle": "Delete Confirmation", + "@dialogDeletionTitle": { + "description": "Title displayed on the DeleteDialog." + }, + "dialogDeletionContent": "Are you sure you want to delete \"{name}\"?\n\n{more}This action cannot be undone!\n", + "@dialogDeletionContent": { + "description": "Content displayed on the DeleteDialog.", + "placeholders": { + "name": { + "type": "String", + "example": "Veggie Sandwich" + }, + "more": { + "type": "String", + "description": "More details about the side effects of deletion", + "example": "What other impacts deleting this item will have" + } + } + }, + "imageHolderCreate": "Tap to add image", + "imageHolderUpdate": "Click to update image", + "imageBtnCrop": "Crop", + "imageGalleryTitle": "Gallery", + "imageGalleryEmpty": "Start importing your first image!", + "imageGalleryActionCreate": "Add Image", + "imageGalleryActionDelete": "Delete", + "imageGallerySnackbarDeleteFailed": "One or more images failed to delete.", + "imageGallerySelectionTitle": "Select Images", + "imageGallerySelectionDeleteConfirm": "Will delete {count} image(s) permanently.\nAfter deletion, the connected product will not able to display the image.", + "@imageGallerySelectionDeleteConfirm": { + "placeholders": { + "count": { + "type": "int", + "format": "compact" + } + } + }, + "emptyBodyTitle": "Oops! It's empty here.", + "@emptyBodyTitle": { + "description": "Text displayed on EmptyBody, informing the user that there are no items yet. This is the default text." + }, + "emptyBodyAction": "Set Up Now", + "btnNavTo": "View", + "@btnNavTo": { + "description": "Button text to navigate to another screen in trailing." + }, + "btnSignInWithGoogle": "Sign in with Google", + "semanticsPercentileBar": "Currently {percent} of total", + "@semanticsPercentileBar": { + "placeholders": { + "percent": { + "type": "num", + "format": "percentPattern" + } + } + }, + "invalidIntegerType": "{field} must be an integer.", + "@invalidIntegerType": { + "description": "Warning message when the input is not an integer.", + "placeholders": { + "field": { + "type": "String" + } + } + }, + "invalidNumberType": "{field} must be a number.", + "@invalidNumberType": { + "description": "Warning message when the input is not a number.", + "placeholders": { + "field": { + "type": "String" + } + } + }, + "invalidNumberPositive": "{field} cannot be negative.", + "@invalidNumberPositive": { + "description": "Warning message when the input is not positive.", + "placeholders": { + "field": { + "type": "String" + } + } + }, + "invalidNumberMaximum": "{field} cannot exceed {maximum}.", + "@invalidNumberMaximum": { + "description": "Warning message when the input exceeds the maximum value.", + "placeholders": { + "field": { + "type": "String" + }, + "maximum": { + "type": "num", + "description": "Maximum value", + "format": "decimalPattern" + } + } + }, + "invalidNumberMinimum": "{field} cannot be less than {minimum}.", + "@invalidNumberMinimum": { + "description": "Warning message when the input is less than the minimum value.", + "placeholders": { + "field": { + "type": "String" + }, + "minimum": { + "type": "num", + "description": "Minimum value", + "format": "decimalPattern" + } + } + }, + "invalidStringEmpty": "{field} cannot be empty.", + "@invalidStringEmpty": { + "description": "Warning message when no text is entered.", + "placeholders": { + "field": { + "type": "String" + } + } + }, + "invalidStringMaximum": "{field} cannot exceed {maximum} characters.", + "@invalidStringMaximum": { + "description": "Warning message when the input exceeds the maximum character limit.", + "placeholders": { + "field": { + "type": "String" + }, + "maximum": { + "type": "int", + "description": "Maximum number of characters" + } + } + }, + "singleMonth": "Single Month", + "@singleMonth": { + "description": "One of the units for calendar period conversion." + }, + "singleWeek": "Single Week", + "@singleWeek": { + "description": "One of the units for calendar period conversion." + }, + "twoWeeks": "Two Weeks", + "@twoWeeks": { + "description": "One of the units for calendar period conversion." + }, "orderAttributeTitle": "Customer Settings", - "orderAttributeHint": "Customer setting can help organize the customer when analysis\nFor example:\nAge 20, Office worker, Take away.", - "orderAttributeCreate": "Create", - "orderAttributeUpdate": "Edit", - "orderAttributeReorder": "Reorder", - "orderAttributeMetaMode": "Mode:{mode}", + "orderAttributeDescription": "Information for analysis such as dine-in, takeout, etc.", + "orderAttributeTitleCreate": "Add Customer Setting", + "orderAttributeTitleUpdate": "Edit Customer Setting", + "orderAttributeTitleReorder": "Reorder Customer Settings", + "orderAttributeEmptyBody": "Customer settings help us track who comes to consume, such as:\n20-30 years old, takeout, office workers, etc.", + "orderAttributeHeaderInfo": "Customer Settings", + "@orderAttributeHeaderInfo": { + "description": "Displayed on the upper rectangle in homepage" + }, + "orderAttributeTutorialTitle": "Customer Settings", + "orderAttributeTutorialContent": "This is where you set customer information, such as dine-in, takeout, office worker, etc.\nThis information helps us track who comes to consume and make better business strategies.", + "orderAttributeMetaMode": "Mode: {name}", + "@orderAttributeMetaMode": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "orderAttributeMetaDefault": "Default: {name}", + "@orderAttributeMetaDefault": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "orderAttributeMetaNoDefault": "None", - "orderAttributeMetaDefault": "Default:{name}", - "orderAttributeModeNames": "{mode, select, statOnly{Normal} changePrice{Appraise} changeDiscount{Discount} other{UNKNOWN}}", - "orderAttributeModeDescriptions": "{mode, select, statOnly{Normal setting will not affect the final price of the cart} changePrice{This will increase or decrease the price。For example: Take away + 30, Reusable tableware - 5.} changeDiscount{This will take a discount. For example: Internal use + 10% for service charge, Social interaction - 10%。} other{UNKNOWN}}", - "orderAttributeNameLabel": "Setting name", - "orderAttributeNameHint": "For example: customer age", - "orderAttributeNameRepeatError": "Name repeated", - "orderAttributeModeTitle": "Customer Setting Mode", - "orderAttributeOptionCreate": "Create option", - "orderAttributeOptionIsDefault": "Default", - "orderAttributeOptionReorder": "Reorder", - "orderAttributeOptionCreateTitle": "Create option of {name}", - "orderAttributeOptionNameLabel": "Name", - "orderAttributeOptionNameHelper": "Take age as an example, possible options are:\n- Below 20\n- 20 to 30", - "orderAttributeOptionNameRepeatError": "Name repeated", - "orderAttributeOptionsModeHelper": "{mode, select, statOnly{Normal do not need any additional settings} changePrice{It will increase or decrease the price} changeDiscount{It will take a discount} other{UNKNOWN}}", - "orderAttributeOptionsModeHint": "{mode, select, statOnly{} changePrice{-30 means decrease 30 dollars} changeDiscount{80 means 20 percent off} other{UNKNOWN}}", - "orderAttributeOptionSetToDefault": "Set as default", - "orderAttributeOptionConfirmChangeDefaultTitle": "Overwrite the default option?", - "orderAttributeOptionConfirmChangeDefaultContent": "This will replace the original default option '{name}'", - "orderMetaTotalPrice": "Price: {price}", - "orderMetaTotalCount": "Count: {count}", - "orderActionsCheckout": "Checkout", - "orderActionsOpenChanger": "Change money", - "orderActionsLeaveHistoryMode": "Leave history mode", - "orderActionsShowLastOrder": "Show last order", - "orderActionsShowLastOrderNotFound": "Can not find the last order today\nYou can try analysis page", - "orderActionsStash": "Stash current order", - "orderActionsStashHitLimit": "Stash count has hit the limit", - "orderActionsDropStash": "Pull out stashed order", - "orderActionsDropStashNotFound": "There is no stashed orders", - "orderActionsLeave": "Pop out the page", - "orderCartSnapshotTutorialTitle": "Details interface", - "orderCartSnapshotTutorialMessage": "To make it more convenience,\nwe put the products details setting(ingredient and quantity) in side here.\nIf you are using a tablet you can choose to use the other template\nGo to setting > order outlook, for the details.", - "orderCartSnapshotEmpty": "Empty orders" -} + "orderAttributeModeDivider": "Customer Setting Mode", + "orderAttributeModeName": "{name, select, statOnly{Normal} changePrice{Price Change} changeDiscount{Discount} other{UNKNOWN}}", + "@orderAttributeModeName": { + "description": "name" + }, + "orderAttributeModeHelper": "{name, select, statOnly{Normal setting, selecting won't affect the order price.} changePrice{Selecting this setting may affect the order price.\nFor example: Takeout +$30, Eco Cup -$5.\n} changeDiscount{Selecting this setting will affect the total price based on the discount.\nFor example: Dine-in +10% service charge, Friends & Family Discount -10%.\n} other{UNKNOWN}}", + "@orderAttributeModeHelper": { + "description": "Explanation of customer setting categories", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "orderAttributeNameLabel": "Customer Setting Name", + "orderAttributeNameHint": "e.g., Age", + "orderAttributeNameErrorRepeat": "Name already exists", + "orderAttributeOptionTitleCreate": "Add Option", + "orderAttributeOptionTitleCreateWith": "Add option for {name}", + "@orderAttributeOptionTitleCreateWith": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "orderAttributeOptionTitleUpdate": "Edit Option", + "orderAttributeOptionTitleReorder": "Reorder Options", + "orderAttributeOptionMetaDefault": "Default", + "orderAttributeOptionNameLabel": "Option Name", + "orderAttributeOptionNameHelper": "For example, possible options for age include:\n- Under 20\n- 20 to 30", + "orderAttributeOptionNameErrorRepeat": "Name already exists", + "orderAttributeOptionModeTitle": "Option Mode", + "orderAttributeOptionModeHelper": "{name, select, statOnly{No need to set \"Discount\" or \"Price Change\" because this setting is \"Normal\"} changePrice{Selecting this option during ordering will apply this price change} changeDiscount{Selecting this option during ordering will apply this discount} other{UNKNOWN}}", + "@orderAttributeOptionModeHelper": { + "description": "Explanation of mode", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "orderAttributeOptionModeHint": "{name, select, statOnly{} changePrice{For example: -30 means decrease by thirty dollars} changeDiscount{For example: 80 means \"20% off\"} other{UNKNOWN}}", + "@orderAttributeOptionModeHint": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "orderAttributeOptionToDefaultLabel": "Set as Default", + "orderAttributeOptionToDefaultHelper": "Set this option as the default value, which will be used for each order by default.", + "orderAttributeOptionToDefaultConfirmChangeTitle": "Override Option Default?", + "orderAttributeOptionToDefaultConfirmChangeContent": "Doing this will make \"{name}\" no longer the default value", + "@orderAttributeOptionToDefaultConfirmChangeContent": { + "description": "Prompt to ensure the user knows what the original default value was", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "orderAttributeValueEmpty": "No price impact", + "orderAttributeValueFree": "Free", + "orderAttributeValueDiscountIncrease": "Increase to {value} times", + "@orderAttributeValueDiscountIncrease": { + "placeholders": { + "value": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "orderAttributeValueDiscountDecrease": "Decrease to {value} times", + "@orderAttributeValueDiscountDecrease": { + "placeholders": { + "value": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "orderAttributeValuePriceIncrease": "Increase by {value} dollars", + "@orderAttributeValuePriceIncrease": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "orderAttributeValuePriceDecrease": "Decrease by {value} dollars", + "@orderAttributeValuePriceDecrease": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "menuTitle": "Menu", + "menuSubtitle": "Categories, Products", + "menuTutorialTitle": "Create Your Menu", + "menuTutorialContent": "Let's start by creating a menu!", + "menuSearchHint": "Search for products, ingredients, quantities", + "menuSearchNotFound": "Couldn't find relevant information. Did you misspell something?", + "menuCatalogHeaderInfo": "Categories", + "@menuCatalogHeaderInfo": { + "description": "Displayed on the upper rectangle in homepage" + }, + "menuCatalogTutorialTitle": "Create First Catalog", + "menuCatalogEmptyBody": "Similar \"products\" will be grouped under \"categories\",\nmaking it convenient for ordering, such as:\n• \"Cheese Burger\", \"Veggie Burger\" > \"Burgers\"\n• \"Plastic Bag\", \"Eco Cup\" > \"Others\"", + "menuCatalogTitleCreate": "Add Category", + "@menuCatalogTitleCreate": { + "description": "FloatingActionButton description on the menu page" + }, + "menuCatalogTitleUpdate": "Edit Category", + "menuCatalogTitleReorder": "Reorder Categories", + "menuCatalogDialogDeletionContent": "{count, plural, =0{No products inside} other{Will delete {count} products together}}", + "@menuCatalogDialogDeletionContent": { + "description": "Warning message when deleting product categories on the menu page", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "menuCatalogNameLabel": "Category Name", + "menuCatalogNameHint": "e.g., Burgers", + "menuCatalogNameErrorRepeat": "Name already exists. Please choose a different name!", + "menuCatalogEmptyProducts": "No products set yet", + "menuProductHeaderInfo": "Products", + "@menuProductHeaderInfo": { + "description": "Displayed on the upper rectangle in homepage" + }, + "menuProductEmptyBody": "\"Products\" are the basic units in the menu, such as:\n\"Cheese Burger\", \"Cola\"", + "menuProductTitleCreate": "Add Product", + "menuProductTitleUpdate": "Edit Product", + "menuProductTitleReorder": "Reorder Products", + "menuProductTitleUpdateImage": "Update Photo", + "menuProductMetaTitle": "Product", + "@menuProductMetaTitle": { + "description": "Prefix for meta, so users know this is product meta info, not category" + }, + "menuProductMetaPrice": "Price: {price}", + "@menuProductMetaPrice": { + "description": "Price of the product", + "placeholders": { + "price": { + "type": "num", + "format": "compact" + } + } + }, + "menuProductMetaCost": "Cost: {cost}", + "@menuProductMetaCost": { + "description": "Cost of the product", + "placeholders": { + "cost": { + "type": "num", + "format": "compact" + } + } + }, + "menuProductMetaEmpty": "No ingredients set", + "@menuProductMetaEmpty": { + "description": "Text displayed in the subtitle in the product list" + }, + "menuProductNameLabel": "Product Name", + "menuProductNameHint": "e.g., Cheeseburger", + "menuProductNameErrorRepeat": "Product name already exists", + "menuProductPriceLabel": "Product Price", + "menuProductPriceHelper": "Price displayed on the order page", + "menuProductCostLabel": "Product Cost", + "menuProductCostHelper": "Used to calculate profit, should be less than the price", + "menuProductEmptyIngredients": "No ingredients set yet", + "menuIngredientEmptyBody": "You can set ingredients for the product, such as:\n\"Cheeseburger\" with \"Cheese\", \"Bun\" as ingredients", + "menuIngredientTitleCreate": "Add Ingredient", + "menuIngredientTitleUpdate": "Edit Ingredient", + "menuIngredientTitleReorder": "Reorder Ingredients", + "menuIngredientMetaAmount": "Amount: {amount}", + "@menuIngredientMetaAmount": { + "placeholders": { + "amount": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "menuIngredientSearchLabel": "Search Ingredients", + "menuIngredientSearchHelper": "After adding ingredient, you can set related information in \"Inventory\".", + "menuIngredientSearchHint": "e.g., Cheese", + "menuIngredientSearchAdd": "Add Ingredient \"{name}\"", + "@menuIngredientSearchAdd": { + "description": "Button to add ingredient if search result not found", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "menuIngredientSearchErrorEmpty": "Ingredient must be set, please click to set.", + "menuIngredientSearchErrorRepeat": "Product already has the same ingredient, cannot select repeatedly.", + "menuIngredientAmountLabel": "Amount Used", + "menuIngredientAmountHelper": "Default amount used.\nIf customers are able to adjust the amount,\nset different quantities in \"Quantity.\"\n", + "menuQuantityTitleCreate": "Add Quantity", + "menuQuantityTitleUpdate": "Edit Quantity", + "menuQuantityMetaAmount": "Amount: {amount}", + "@menuQuantityMetaAmount": { + "placeholders": { + "amount": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "menuQuantityMetaAdditionalPrice": "Price: {price}", + "@menuQuantityMetaAdditionalPrice": { + "placeholders": { + "price": { + "type": "String" + } + } + }, + "menuQuantityMetaAdditionalCost": "Cost: {cost}", + "@menuQuantityMetaAdditionalCost": { + "placeholders": { + "cost": { + "type": "String" + } + } + }, + "menuQuantitySearchLabel": "Search Quantity", + "menuQuantitySearchHelper": "After adding ingredient quantity, you can set related information in \"Quantity\".", + "menuQuantitySearchHint": "e.g., Large, Small", + "menuQuantitySearchAdd": "Add Quantity \"{name}\"", + "@menuQuantitySearchAdd": { + "description": "Button to add quantity if search result not found", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "menuQuantitySearchErrorEmpty": "Quantity must be set, please click to set.", + "menuQuantitySearchErrorRepeat": "Product already has the same quantity, cannot select repeatedly.", + "menuQuantityAmountLabel": "Amount Used", + "menuQuantityAdditionalPriceLabel": "Additional Price", + "menuQuantityAdditionalPriceHelper": "Set to 0 to indicate no additional charge for extra (or less) quantity.", + "menuQuantityAdditionalCostLabel": "Additional Cost", + "menuQuantityAdditionalCostHelper": "Additional cost can be negative, e.g., \"Less\" reduces ingredient usage, reducing cost accordingly.", + "cashierTab": "Cashier", + "cashierUnitLabel": "${unit}", + "@cashierUnitLabel": { + "placeholders": { + "unit": { + "type": "String" + } + } + }, + "cashierCounterLabel": "Quantity", + "@cashierCounterLabel": { + "description": "Label when setting currency quantity." + }, + "cashierToDefaultTitle": "Set as Default", + "cashierToDefaultTutorialTitle": "Cash Register Default Status", + "cashierToDefaultTutorialContent": "After setting the quantities of various currencies below,\nclick here to set the default status!\nThe set quantities will be the \"maximum\" for each currency status bar.", + "cashierToDefaultDialogTitle": "Adjust Cash Register Default?", + "cashierToDefaultDialogContent": "This will set the current cash register status as the default status.\nThis action will override previous settings.", + "cashierChangerTitle": "Changer", + "cashierChangerButton": "Apply", + "cashierChangerTutorialTitle": "Cash Register Money Changer", + "cashierChangerTutorialContent": "Exchange one hundred for 10 tens, for example.\nHelps to quickly adjust the cash register status.", + "cashierChangerErrorNoSelection": "Please select a combination to apply", + "cashierChangerErrorNotEnough": "Not enough ${unit}", + "@cashierChangerErrorNotEnough": { + "placeholders": { + "unit": { + "type": "String" + } + } + }, + "cashierChangerErrorInvalidHead": "Cannot exchange {count} of ${unit} to", + "@cashierChangerErrorInvalidHead": { + "placeholders": { + "count": { + "type": "int" + }, + "unit": { + "type": "String" + } + } + }, + "cashierChangerErrorInvalidBody": "{count} of ${unit}", + "@cashierChangerErrorInvalidBody": { + "description": "Concatenated multiple lines after `invalidHead` to form a complete sentence.", + "placeholders": { + "count": { + "type": "int" + }, + "unit": { + "type": "String" + } + } + }, + "cashierChangerFavoriteTab": "Favorites", + "cashierChangerFavoriteHint": "After selecting, please click \"Apply\" to use the combination.", + "cashierChangerFavoriteEmptyBody": "Here can help you quickly convert different currencies.", + "cashierChangerFavoriteItemFrom": "Exchange {count} of ${unit} to", + "@cashierChangerFavoriteItemFrom": { + "placeholders": { + "count": { + "type": "int" + }, + "unit": { + "type": "String" + } + } + }, + "cashierChangerFavoriteItemTo": "{count} of ${unit}", + "@cashierChangerFavoriteItemTo": { + "placeholders": { + "count": { + "type": "int" + }, + "unit": { + "type": "String" + } + } + }, + "cashierChangerCustomTab": "Custom", + "cashierChangerCustomAddBtn": "Add Favorite", + "cashierChangerCustomCountLabel": "Quantity", + "cashierChangerCustomUnitLabel": "Currency", + "cashierChangerCustomUnitAddBtn": "Add Currency", + "cashierChangerCustomDividerFrom": "Withdraw from Cash Register", + "cashierChangerCustomDividerTo": "Exchange", + "cashierSurplusTitle": "Surplus", + "cashierSurplusButton": "Surplus", + "cashierSurplusTutorialTitle": "Daily Surplus", + "cashierSurplusTutorialContent": "Surplus helps us at the end of each day,\ncalculate the difference between the current amount and the default amount.", + "cashierSurplusErrorEmptyDefault": "Default status not set yet", + "cashierSurplusTableHint": "Once you confirm that there are no issues with the cash register money, you can complete the surplus!", + "cashierSurplusColumnName": "{name, select, unit{Unit} currentCount{Current} diffCount{Difference} defaultCount{Default} other{UNKNOWN}}", + "cashierSurplusCounterLabel": "Quantity of ${unit}", + "@cashierSurplusCounterLabel": { + "description": "Allow users to customize currency when surplus.", + "placeholders": { + "unit": { + "type": "String" + } + } + }, + "cashierSurplusCounterShortLabel": "Quantity", + "@cashierSurplusCounterShortLabel": { + "description": "This is for display in error messages, e.g., \"Quantity cannot be 0\"." + }, + "cashierSurplusCurrentTotalLabel": "Current Total", + "cashierSurplusCurrentTotalHelper": "The total amount the cash register should have now.\nIf you find that the cash and this value don't match, think about whether you used the cash register to buy something today?", + "cashierSurplusDiffTotalLabel": "Difference", + "cashierSurplusDiffTotalHelper": "The difference from the total amount of the cash register at the very beginning.\nThis can quickly help you understand how much money the cash register has gained today.", + "orderTitle": "Ordering", + "orderBtn": "Order", + "orderSnackbarCashierNotEnough": "Insufficient cash in the cashier!", + "orderSnackbarCashierUsingSmallMoney": "Using smaller denominations to give change", + "orderSnackbarCashierUsingSmallMoneyHelper": "When giving change to customers, if the cashier doesn't have the appropriate denominations, this message will appear.\n\nFor example, if the total is $65 and the customer pays $100, the change should be $35.\nIf the cashier only has two $10 bills and more than three $5 bills, this message will appear.\n\nTo avoid this prompt:\n• Go to the changer page and top up various denominations.\n• Go to the [settings page]({link}) to disable related prompts from the cashier.", + "@orderSnackbarCashierUsingSmallMoneyHelper": { + "placeholders": { + "link": { + "type": "String" + } + } + }, + "orderActionCheckout": "Checkout", + "@orderActionCheckout": { + "description": "Proceed to the next step after confirming the items in your cart" + }, + "orderActionExchange": "Exchange", + "orderActionStash": "Stash", + "orderActionReview": "Order History", + "orderLoaderMetaTotalRevenue": "Revenue: {revenue}", + "@orderLoaderMetaTotalRevenue": { + "description": "Total revenue from orders in the order list", + "placeholders": { + "revenue": { + "type": "String" + } + } + }, + "orderLoaderMetaTotalCost": "Cost: {cost}", + "@orderLoaderMetaTotalCost": { + "description": "Total cost from orders in the order list", + "placeholders": { + "cost": { + "type": "String" + } + } + }, + "orderLoaderMetaTotalCount": "Count: {count}", + "@orderLoaderMetaTotalCount": { + "description": "Total number of orders in the order list", + "placeholders": { + "count": { + "type": "int", + "format": "compact" + } + } + }, + "orderLoaderEmpty": "No order records found", + "orderCatalogListEmpty": "No product categories set yet", + "orderProductListTutorialTitle": "Start Ordering!", + "orderProductListTutorialContent": "Ordering through images is more convenient!\nYou can go to \"Settings\" > \"[Items Per Row]({link})\" to adjust\nand allow text-only ordering here.", + "@orderProductListTutorialContent": { + "placeholders": { + "link": { + "type": "String" + } + } + }, + "orderCartActionBulkify": "Bulk Actions", + "orderCartActionToggle": "Toggle", + "orderCartActionSelectAll": "Select All", + "orderCartActionDiscount": "Discount", + "orderCartActionDiscountLabel": "Discount", + "orderCartActionDiscountHint": "e.g., 30 means 70% off", + "orderCartActionDiscountHelper": "The number here represents the \"percentage\" off, i.e., 85 means 15% off. For precise prices, use \"Price Change\".", + "orderCartActionDiscountSuffix": "%", + "orderCartActionChangePrice": "Price Change", + "orderCartActionChangePriceLabel": "Price", + "orderCartActionChangePriceHint": "Price per item", + "orderCartActionChangePricePrefix": "$", + "orderCartActionChangePriceSuffix": "", + "orderCartActionChangeCount": "Change Quantity", + "orderCartActionChangeCountLabel": "Quantity", + "orderCartActionChangeCountHint": "Quantity of items", + "orderCartActionChangeCountSuffix": "items", + "orderCartActionFree": "Free", + "orderCartActionDelete": "Delete", + "orderCartSnapshotTutorialTitle": "Cart", + "orderCartSnapshotTutorialContent": "To make selecting products more convenient,\nwe've placed the products you've ordered here.\nIf you need a layout that shows all information at once (suitable for large screens),\ngo to \"Settings\" > \"[Ordering Layout]({link})\" to adjust.", + "@orderCartSnapshotTutorialContent": { + "placeholders": { + "link": { + "type": "String" + } + } + }, + "orderCartSnapshotEmpty": "No items in cart", + "orderCartMetaTotalPrice": "Price: {price}", + "@orderCartMetaTotalPrice": { + "description": "Total price of items in the cart", + "placeholders": { + "price": { + "type": "String" + } + } + }, + "orderCartMetaTotalCount": "Count: {count}", + "@orderCartMetaTotalCount": { + "description": "Total number of items in the cart", + "placeholders": { + "count": { + "type": "int", + "format": "compact" + } + } + }, + "orderCartProductPrice": "{price, select, 0{Free} other{${price}}}", + "@orderCartProductPrice": { + "description": "Price of the product", + "placeholders": { + "price": { + "type": "String" + } + } + }, + "orderCartProductIncrease": "Increase Quantity", + "orderCartProductDefaultQuantity": "Default Quantity", + "orderCartProductIngredient": "{name} ({quantity})", + "@orderCartProductIngredient": { + "description": "Ingredients and quantities of each item in the product list when ordering", + "placeholders": { + "name": { + "type": "String" + }, + "quantity": { + "type": "String" + } + } + }, + "orderCartIngredientStatus": "{status, select, emptyCart{Please select a product to set its ingredients} differentProducts{Please select the same product to set its ingredients} noNeedIngredient{This product doesn't require ingredient settings} other{UNKNOWN}}", + "@orderCartIngredientStatus": { + "description": "Prompt to users during ordering if the selected product doesn't require ingredient settings", + "placeholders": { + "status": { + "type": "String" + } + } + }, + "orderCartQuantityNotAble": "Please select an ingredient to set quantity", + "@orderCartQuantityNotAble": { + "description": "During ordering, select the ingredient to set the quantity" + }, + "orderCartQuantityLabel": "{name} ({amount})", + "@orderCartQuantityLabel": { + "placeholders": { + "name": { + "type": "String" + }, + "amount": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "orderCartQuantityDefaultLabel": "Default ({amount})", + "@orderCartQuantityDefaultLabel": { + "description": "During ingredient setup, the quantity can be customized or set to default (no quantity used)", + "placeholders": { + "amount": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "orderCheckoutEmptyCart": "Please make an order first.", + "orderCheckoutActionStash": "Stash", + "orderCheckoutActionConfirm": "Confirm", + "orderCheckoutStashTab": "Stash", + "orderCheckoutStashEmpty": "No items currently stashed.", + "orderCheckoutStashNoProducts": "No products", + "orderCheckoutStashActionCheckout": "Checkout", + "orderCheckoutStashActionRestore": "Restore", + "orderCheckoutStashDialogCalculator": "Checkout Calculator", + "orderCheckoutStashDialogRestoreTitle": "Restore Stashed Order", + "orderCheckoutStashDialogRestoreContent": "This action will override the current cart contents.", + "orderCheckoutStashDialogDeleteName": "order", + "orderCheckoutAttributeTab": "Customer Settings", + "orderCheckoutCashierTab": "Cashier", + "orderCheckoutCashierCalculatorLabelPaid": "Paid", + "orderCheckoutCashierCalculatorLabelChange": "Change", + "orderCheckoutCashierSnapshotLabelChange": "Change: {change}", + "@orderCheckoutCashierSnapshotLabelChange": { + "description": "Change given by the cashier after the customer's payment", + "placeholders": { + "change": { + "type": "String" + } + } + }, + "orderCheckoutSnackbarPaidFailed": "Payment is less than the order amount.", + "orderObjectViewEmpty": "No order records found", + "orderObjectViewChange": "Change", + "orderObjectViewPriceTotal": "Total Price: {price}", + "@orderObjectViewPriceTotal": { + "description": "Total price information after ordering", + "placeholders": { + "price": { + "type": "String" + } + } + }, + "orderObjectViewPriceProducts": "Product Price", + "orderObjectViewPriceAttributes": "Customer Settings Price", + "orderObjectViewCost": "Cost", + "orderObjectViewProfit": "Profit", + "orderObjectViewPaid": "Paid", + "orderObjectViewDividerAttribute": "Customer Settings", + "orderObjectViewDividerProduct": "Product Information", + "orderObjectViewProductPrice": "Price", + "orderObjectViewProductCost": "Cost", + "orderObjectViewProductCount": "Count", + "orderObjectViewProductSinglePrice": "Unit Price", + "orderObjectViewProductOriginalPrice": "Original Unit Price", + "orderObjectViewProductCatalog": "Product Category", + "orderObjectViewProductIngredient": "Ingredients", + "orderObjectViewProductDefaultQuantity": "Default", + "analysisTab": "Stats", + "analysisHistoryBtn": "Records", + "analysisHistoryTitle": "Order Records", + "analysisHistoryTitleEmpty": "No Order History Found", + "analysisHistoryCalendarTutorialTitle": "Calendar", + "analysisHistoryCalendarTutorialContent": "Swipe up and down to adjust the time period, such as month or week.\nSwipe left and right to adjust the date range.", + "analysisHistoryExportBtn": "Export", + "analysisHistoryExportTutorialTitle": "Export Orders Data", + "analysisHistoryExportTutorialContent": "Export orders externally for further analysis or backup.\nYou can export multi-day orders in the \"Transit\" page.", + "analysisHistoryOrderListMetaPrice": "Price: {price}", + "@analysisHistoryOrderListMetaPrice": { + "description": "Price of specific orders in the order list.", + "placeholders": { + "price": { + "type": "num", + "format": "compactCurrency", + "optionalParameters": { + "symbol": "$" + } + } + } + }, + "analysisHistoryOrderListMetaPaid": "Paid: {paid}", + "@analysisHistoryOrderListMetaPaid": { + "description": "Payment amount for specific orders in the order list.", + "placeholders": { + "paid": { + "type": "num", + "format": "compactCurrency", + "optionalParameters": { + "symbol": "$" + } + } + } + }, + "analysisHistoryOrderListMetaProfit": "Profit: {profit}", + "@analysisHistoryOrderListMetaProfit": { + "description": "Net profit for specific orders in the order list.", + "placeholders": { + "profit": { + "type": "num", + "format": "compactCurrency", + "optionalParameters": { + "symbol": "$" + } + } + } + }, + "analysisHistoryOrderTitle": "Order Details", + "analysisHistoryOrderNotFound": "No relevant orders found", + "analysisHistoryOrderDeleteDialog": "Are you sure you want to delete the order for {name}?\nCash register and inventory data cannot be recovered.\nThis action cannot be undone.", + "@analysisHistoryOrderDeleteDialog": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "analysisGoalsTitle": "Today's Summary", + "analysisGoalsCountTitle": "Order Count", + "analysisGoalsCountDescription": "The order count reflects the attractiveness of products to customers.\nIt represents the demand for your products in the market and helps you understand which products or time periods are most popular.\nA high order count may indicate the success of your pricing strategy or marketing activities and is one of the indicators of business model effectiveness.\nHowever, it's essential to note that simply pursuing a high order count may overlook profitability.", + "analysisGoalsRevenueTitle": "Revenue", + "analysisGoalsRevenueDescription": "Revenue represents the total sales amount and is an indicator of business scale.\nHigh revenue may indicate that your products are popular and selling well, but revenue alone cannot reflect the sustainability and profitability of the business.\nSometimes, to increase revenue, companies may adopt strategies such as price reductions, which may affect profitability.", + "analysisGoalsProfitTitle": "Profit", + "analysisGoalsProfitDescription": "Profit is the balance after deducting operating costs from operating income and is crucial for the company's ongoing operations.\nProfit directly reflects operational efficiency and cost management capabilities.\nUnlike revenue, profit considers the business expenses, including raw material costs, labor, rent, etc.\nIt's a more practical indicator that helps you evaluate the effectiveness and sustainability of operations.", + "analysisGoalsCostTitle": "Cost", + "analysisGoalsAchievedRate": "Profit Achievement\n{rate}", + "@analysisGoalsAchievedRate": { + "placeholders": { + "rate": { + "type": "String" + } + } + }, + "analysisChartTitle": "Chart Analysis", + "analysisChartTitleCreate": "Create Chart", + "analysisChartTitleReorder": "Reorder Charts", + "analysisChartTutorialTitle": "Chart Analysis", + "analysisChartTutorialContent": "With charts, you can visualize data changes more intuitively.\nStart designing charts to track your sales performance now!", + "analysisChartCardEmptyData": "No Data", + "analysisChartCardTitleUpdate": "Edit Chart", + "analysisChartMetricName": "{name, select, revenue{Revenue} cost{Cost} profit{Profit} count{Quantity} other{UNKNOWN}}", + "@analysisChartMetricName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "analysisChartTargetName": "{name, select, order{Order} catalog{Category} product{Product} ingredient{Ingredient} attribute{Attribute} other{UNKNOWN}}", + "@analysisChartTargetName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "analysisChartRangeYesterday": "Yesterday", + "analysisChartRangeToday": "Today", + "analysisChartRangeLastWeek": "Last Week", + "analysisChartRangeThisWeek": "This Week", + "analysisChartRangeLast7Days": "Last 7 Days", + "analysisChartRangeLastMonth": "Last Month", + "analysisChartRangeThisMonth": "This Month", + "analysisChartRangeLast30Days": "Last 30 Days", + "analysisChartRangeTabName": "{name, select, day{Date} week{Week} month{Month} custom{Custom} other{UNKNOWN}}", + "@analysisChartRangeTabName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "analysisChartModalNameLabel": "Chart Name", + "analysisChartModalNameHint": "For example: Daily Revenue", + "analysisChartModalIgnoreEmptyLabel": "Ignore Empty Data", + "analysisChartModalIgnoreEmptyHelper": "Do not display if a product or metric has no data for that period.", + "analysisChartModalDivider": "Data Settings", + "analysisChartModalTypeLabel": "Chart Type", + "analysisChartModalTypeName": "{name, select, cartesian{Time Series Chart} circular{Pie Chart} other{UNKNOWN}}", + "@analysisChartModalTypeName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "analysisChartModalMetricLabel": "Metrics to View", + "analysisChartModalMetricHelper": "Choose different types of metrics based on your objectives.", + "analysisChartModalTargetLabel": "Item Category", + "analysisChartModalTargetHelper": "Select the information to analyze in the chart.", + "analysisChartModalTargetErrorEmpty": "Please select an item category", + "analysisChartModalTargetItemLabel": "Item Selection", + "analysisChartModalTargetItemHelper": "Choose the items you want to observe, such as the quantity of a specific product within a certain period.", + "analysisChartModalTargetItemSelectAll": "Select All" +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 33e98c2a..2162197d 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,748 +1,1642 @@ { "@@locale": "zh", + "@@last_modified": "2024-05-18T08:05:55.671229Z", + "@@author": "Lu Shueh Chou", + "settingTab": "設定", + "settingVersion": "版本:{version}", + "@settingVersion": { + "description": "Display the app version", + "placeholders": { + "version": { + "type": "String" + } + } + }, + "settingWelcome": "HI,{name}", + "@settingWelcome": { + "description": "Display user's name", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingLogoutBtn": "登出", + "settingElfTitle": "建議", + "settingElfDescription": "使用 Google 表單提供回饋", + "settingElfContent": "覺得這裡還少了什麼嗎?\n歡迎[提供建議](https://forms.gle/R1vZDk9ztQLScUdb9)。\n也可以來看看[排程中的功能](https://github.com/evan361425/flutter-pos-system/milestones)。", + "settingFeatureTitle": "其他設定", + "settingFeatureDescription": "外觀、語言、提示", + "settingThemeTitle": "調色盤", + "settingThemeName": "{name, select, dark{暗色模式} light{日光模式} system{跟隨系統} other{UNKNOWN}}", + "@settingThemeName": { + "description": "Appearance of the app", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingLanguageTitle": "語言", + "settingOrderOutlookTitle": "點餐的外觀", + "settingOrderOutlookName": "{name, select, slidingPanel{酷炫面板} singleView{經典模式} other{UNKNOWN}}", + "@settingOrderOutlookName": { + "description": "Appearance during ordering", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingOrderOutlookTip": "{name, select, slidingPanel{點餐時下方會有可拉動的面板,內含點餐中的資訊,適合小螢幕的手機} singleView{所有資訊顯示在單一螢幕中,適合大螢幕的平板} other{UNKNOWN}}", + "@settingOrderOutlookTip": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingCheckoutWarningTitle": "收銀機提示", + "settingCheckoutWarningName": "{name, select, showAll{全部顯示} onlyNotEnough{僅不夠時顯示} hideAll{全部隱藏} other{UNKNOWN}}", + "@settingCheckoutWarningName": { + "description": "Whether to display cash registry warnings", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingCheckoutWarningTip": "{name, select, showAll{若使用小錢去找,顯示提示。\n例如 5 塊錢不夠了,開始用 5 個 1 塊去找錢} onlyNotEnough{當零錢不夠找的時候,顯示提示。} hideAll{當點餐時,收銀機不會顯示任何提示} other{UNKNOWN}}", + "@settingCheckoutWarningTip": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingOrderProductCountTitle": "點餐時每行顯示幾個產品", + "settingOrderProductCountHint": "設定「零」則點餐時僅會以文字顯示", + "settingOrderProductCountMinLabel": "純文字顯示", + "settingOrderAwakeningTitle": "點餐時不關閉螢幕", + "@settingOrderAwakeningTitle": { + "description": "Keep the screen on during ordering, even when idle" + }, + "settingOrderAwakeningDescription": "若取消,則會根據系統設定時間關閉螢幕", + "settingReportTitle": "收集錯誤訊息和事件", + "settingReportDescription": "當應用程式發生錯誤時,寄送錯誤訊息,以幫助應用程式成長", + "stockTab": "庫存", + "stockUpdatedAt": "上次補貨時間:{updatedAt}", + "@stockUpdatedAt": { + "placeholders": { + "updatedAt": { + "type": "DateTime", + "format": "MMMEd" + } + } + }, + "stockIngredientEmptyBody": "新增成份後,就可以開始追蹤這些成份的庫存囉!", + "stockIngredientTitleCreate": "新增成分", + "stockIngredientTitleUpdate": "編輯成分", + "stockIngredientTitleUpdateAmount": "編輯庫存", + "stockIngredientTutorialTitle": "新增成分", + "stockIngredientTutorialContent": "成份可以幫助我們確認產品的庫存。\n你可以在「產品」中設定成分,然後在這裡設定庫存。", + "stockIngredientDialogDeletionContent": "{count, plural, =0{目前無任何產品有本成分} other{將會一同刪除掉 {count} 個產品的成分}}", + "@stockIngredientDialogDeletionContent": { + "description": "Indicates how many products will be affected when deleting the ingredient", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "stockIngredientProductsCount": "共有 {count} 個產品使用此成分", + "@stockIngredientProductsCount": { + "description": "When editing an ingredient, it indicates how many products are using it and allows for navigation to the product page", + "placeholders": { + "count": { + "type": "int", + "description": "Number of products" + } + } + }, + "stockIngredientNameLabel": "成分名稱", + "stockIngredientNameHint": "例如:起司", + "stockIngredientNameErrorRepeat": "成分名稱重複", + "stockIngredientAmountLabel": "現有庫存", + "stockIngredientAmountMaxLabel": "最大庫存", + "stockIngredientAmountMaxHelper": "設定這個值可以幫助你一眼看出用了多少成分。\n\n填空或不填寫則每次增加庫存,都會自動設定這值,\n這是假設每次補貨都會讓庫存達到最大值。", + "stockIngredientAmountShortHelper": "若沒有設定最大庫存量,增加庫存會重設最大值。", + "@stockIngredientAmountShortHelper": { + "description": "Auxiliary text used for quickly increasing inventory" + }, + "stockReplenishmentButton": "採購", + "stockReplenishmentEmptyBody": "採購可以幫你快速調整成分的庫存", + "stockReplenishmentTitleList": "採購列表", + "stockReplenishmentTitleCreate": "新增採購", + "stockReplenishmentTitleUpdate": "編輯採購", + "stockReplenishmentMetaAffect": "會影響 {count} 項成分", + "@stockReplenishmentMetaAffect": { + "description": "Indicates in the purchase list how many ingredients are affected", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "stockReplenishmentNever": "尚未補貨過", + "@stockReplenishmentNever": { + "description": "The stock page displays the last restock time; if never restocked, this text is set" + }, + "stockReplenishmentApplyButton": "套用採購", + "stockReplenishmentApplyConfirmButton": "套用", + "stockReplenishmentApplyConfirmTitle": "套用採購?", + "stockReplenishmentApplyConfirmColumn": "{value, select, name{名稱} amount{數量} other{UNKNOWN}}", + "@stockReplenishmentApplyConfirmColumn": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "stockReplenishmentApplyConfirmHint": "選擇套用後,將會影響以下成分的庫存", + "stockReplenishmentTutorialTitle": "成份採購", + "stockReplenishmentTutorialContent": "透過採購,你不再需要一個一個去設定成分的庫存。\n馬上設定採購,一次調整多個成份吧!", + "stockReplenishmentNameLabel": "採購名稱", + "stockReplenishmentNameHint": "例如:Costco 採購", + "stockReplenishmentNameErrorRepeat": "採購名稱重複", + "stockReplenishmentIngredientsDivider": "成分", + "stockReplenishmentIngredientsHelper": "點選以設定不同成分欲採購的量", + "stockReplenishmentIngredientAmountHint": "設定增加/減少的量", + "stockQuantityTitle": "份量", + "stockQuantityDescription": "半糖、微糖等。", + "stockQuantityTitleCreate": "新增份量", + "stockQuantityTitleUpdate": "編輯份量", + "stockQuantityEmptyBody": "份量可以快速調整成分的量,例如:\n半糖、微糖。", + "stockQuantityMetaProportion": "預設比例:{proportion}", + "@stockQuantityMetaProportion": { + "description": "Text explaining default ratios in subheadings of quantity items", + "placeholders": { + "proportion": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "stockQuantityDialogDeletionContent": "{count, plural, =0{目前無任何產品成分有本份量} other{將會一同刪除掉 {count} 個產品成分的份量'}}", + "@stockQuantityDialogDeletionContent": { + "description": "Indicates how many product ingredients will be affected when deleting the quantity", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "stockQuantityNameLabel": "份量名稱", + "stockQuantityNameHint": "例如:少量或多量", + "stockQuantityNameErrorRepeat": "份量名稱重複", + "stockQuantityProportionLabel": "預設比例", + "stockQuantityProportionHelper": "當產品成分使用此份量時,預設替該成分增加的比例。\n\n例如:此份量為「多量」預設份量為「1.5」,\n今有一產品「起司漢堡」的成分「起司」,每份漢堡會使用「2」單位的起司,\n當增加此份量時,則會自動替「起司」設定為「3」(2 * 1.5)的份量。\n\n若設為「1」則無任何影響。\n\n若設為「0」則代表將不會使用此成分", + "transitTitle": "資料轉移", + "transitDescription": "匯入、匯出店家資訊和訂單", + "transitTutorialTitle": "同步多台裝置", + "transitTutorialContent": "這裡是用來匯入匯出菜單、庫存、訂單記錄等資訊的地方。\n\n我們提供了 Google 試算表和純文字兩種方式,讓您可以方便地在不同裝置間同步資料。", + "transitMethodTitle": "請選擇欲轉移的方式", + "transitMethodName": "{name, select, googleSheet{Google 試算表} plainText{純文字} other{UNKNOWN}}", + "@transitMethodName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitCatalogName": "{name, select, order{訂單記錄} model{店家資訊} other{UNKNOWN}}", + "@transitCatalogName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitCatalogHelper": "{name, select, order{訂單資訊可以讓你匯出到第三方位置後做更細緻的統計分析。} model{商家資訊通常是用來把菜單、庫存等資訊同步到第三方位置或用來匯入到另一台手機。} other{UNKNOWN}}", + "@transitCatalogHelper": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitModelName": "{name, select, menu{菜單} stock{庫存} quantities{份量} replenisher{補貨} orderAttr{顧客設定} order{訂單} orderDetailsAttr{訂單顧客設定} orderDetailsProduct{訂單產品細項} orderDetailsIngredient{訂單成分細項} other{UNKNOWN}}", + "@transitModelName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitOrderMetaRange": "{range}的訂單", + "@transitOrderMetaRange": { + "placeholders": { + "range": { + "type": "String", + "example": "01/01 - 01/31" + } + } + }, + "transitOrderMetaRangeDays": "{days} 天的資料", + "@transitOrderMetaRangeDays": { + "placeholders": { + "days": { + "type": "int" + } + } + }, + "transitOrderCapacityTitle": "預估容量為:{size}", + "@transitOrderCapacityTitle": { + "placeholders": { + "size": { + "type": "String" + } + } + }, + "transitOrderCapacityContent": "過高的容量可能會讓執行錯誤,建議分次執行,不要一次匯出太多筆。", + "transitOrderCapacityOk": "容量剛好", + "transitOrderCapacityWarn": "容量警告", + "transitOrderCapacityDanger": "容量危險", + "transitOrderItemTitle": "{date}", + "@transitOrderItemTitle": { + "placeholders": { + "date": { + "type": "DateTime", + "format": "MMM d HH:mm:ss", + "isCustomDateFormat": "true" + } + } + }, + "transitOrderItemMetaProductCount": "餐點數:{count}", + "@transitOrderItemMetaProductCount": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitOrderItemMetaPrice": "總價:{price}", + "@transitOrderItemMetaPrice": { + "placeholders": { + "price": { + "type": "String" + } + } + }, + "transitOrderItemDialogTitle": "訂單細節", + "transitExportPreviewBtn": "預覽", + "transitExportPreviewTitle": "預覽輸出結果", + "transitExportBtn": "匯入", + "transitImportPreviewBtn": "預覽", + "transitImportPreviewTitle": "預覽匯入結果", + "transitImportPreviewHeader": "注意:匯入後將會把下面沒列到的資料移除,請確認是否執行!", + "transitImportPreviewIngredientMetaAmount": "庫存:{amount}", + "@transitImportPreviewIngredientMetaAmount": { + "placeholders": { + "amount": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "transitImportPreviewIngredientMetaMaxAmount": "{exist, plural, =0{未設定} other{最大值:{value}}}", + "@transitImportPreviewIngredientMetaMaxAmount": { + "placeholders": { + "exist": { + "type": "int" + }, + "value": { + "type": "num", + "format": "decimalPattern" + } + } + }, + "transitImportPreviewIngredientHeader": "匯入後,為了避免影響「菜單」的狀況,並不會把舊的成分移除。", + "transitImportPreviewQuantityHeader": "匯入後,為了避免影響「菜單」的狀況,並不會把舊的份量移除。", + "transitImportBtn": "匯出", + "transitImportErrorColumnCount": "資料量不足,需要 {columns} 個欄位", + "@transitImportErrorColumnCount": { + "placeholders": { + "columns": { + "type": "int" + } + } + }, + "transitImportErrorDuplicate": "將忽略本行,相同的項目已於前面出現", + "transitImportColumnStatus": "{name, select, normal{(一般)} staged{(新增)} stagedIng{(新的成分)} stagedQua{(新的份量)} updated{(異動)} other{UNKNOWN}}", + "@transitImportColumnStatus": { + "description": "Additional status of the data displayed", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSDescription": "Google 試算表是一個強大的小型資料庫,匯出之後可以做很多客制化的分析!", + "transitGSSheetNameLabel": "{name}的表單標題", + "@transitGSSheetNameLabel": { + "description": "Label of title", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSSheetNameUpdate": "修改標題", + "transitGSSpreadsheetLabel": "試算表", + "transitGSSpreadsheetActionSelect": "選擇試算表", + "transitGSSpreadsheetActionClear": "清除所選", + "transitGSSpreadsheetExportEmptyLabel": "建立匯出", + "transitGSSpreadsheetExportEmptyHint": "建立新的試算表「{name}」,並把資料匯出至此", + "@transitGSSpreadsheetExportEmptyHint": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSSpreadsheetExportExistLabel": "指定匯出", + "@transitGSSpreadsheetExportExistLabel": { + "description": "Inform the user that data will be exported to the specified spreadsheet." + }, + "transitGSSpreadsheetExportExistHint": "匯出至試算表「{name}」", + "@transitGSSpreadsheetExportExistHint": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSSpreadsheetImportAllBtn": "匯入全部", + "transitGSSpreadsheetImportAllHint": "不會有任何預覽畫面,直接覆寫全部的資料。", + "transitGSSpreadsheetImportAllConfirmTitle": "匯入全部資料?", + "transitGSSpreadsheetImportAllConfirmContent": "將會把所選表單的資料都下載,並完全覆蓋本地資料。\n此動作無法復原。", + "transitGSSpreadsheetImportExistLabel": "確認表單名稱", + "transitGSSpreadsheetImportExistHint": "從試算表中取得所有表單的名稱,並進行匯入", + "transitGSSpreadsheetImportEmptyLabel": "選擇試算表", + "transitGSSpreadsheetImportEmptyHint": "選擇要匯入的試算表後,就能開始匯入資料", + "transitGSSpreadsheetConfirm": "此動作將會{hint}", + "@transitGSSpreadsheetConfirm": { + "placeholders": { + "hint": { + "type": "String" + } + } + }, + "transitGSSpreadsheetSelectionHint": "{name, select, _{輸入試算表網址或試算表 ID} other{原試算表為「{name}」}}", + "@transitGSSpreadsheetSelectionHint": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSSpreadsheetModelDefaultName": "POS System 資料", + "transitGSSpreadsheetModelExportDivider": "選擇欲匯出的種類", + "transitGSSpreadsheetModelImportDivider": "選擇欲匯入表單", + "transitGSSpreadsheetOrderDefaultName": "訂單資料", + "transitGSSpreadsheetSnackbarAction": "開啟表單", + "transitGSProgressStatusAddSpreadsheet": "新增試算表中..", + "transitGSProgressStatusAddSheets": "新增表單中..", + "transitGSProgressStatusVerifyUser": "驗證身份中", + "transitGSProgressStatusFetchLocalOrders": "取得本地資料..", + "transitGSProgressStatusOverwriteOrders": "覆寫訂單資料..", + "transitGSProgressStatusAppendOrders": "附加進 {name}", + "@transitGSProgressStatusAppendOrders": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSModelStatus": "{model, select, menu{更新菜單中..} stock{更新庫存中..} quantities{更新份量中..} replenisher{更新補貨中..} orderAttr{更新顧客設定中..} order{匯出訂單中..} orderDetailsAttr{匯出顧客設定中..} orderDetailsProduct{匯出產品細項中..} orderDetailsIngredient{匯出成分細項中..} other{UNKNOWN}}", + "@transitGSModelStatus": { + "placeholders": { + "model": { + "type": "String" + } + } + }, + "transitGSModelProductIngredientTitle": "成分資訊", + "transitGSModelProductIngredientNote": "產品全部成分的資訊,格式如下:\n- 成分1,預設使用量\n + 份量a,額外使用量,額外價格,額外成本\n + 份量b,額外使用量,額外價格,額外成本\n- 成分2,預設使用量", + "transitGSModelReplenishmentTitle": "補貨量", + "transitGSModelReplenishmentNote": "每次補貨時特定成分的量,格式如下:\n- 成分1,補貨量\n- 成分2,補貨量", + "transitGSModelAttributeOptionTitle": "顧客設定選項", + "transitGSModelAttributeOptionHeaderTs": "時間戳記", + "transitGSModelAttributeOptionHeaderMode": "類型", + "transitGSModelAttributeOptionHeaderOptions": "選項", + "transitGSModelAttributeOptionNote": "「選項值」會根據顧客設定種類不同而有不同意義,格式如下:\n- 選項1,是否為預設,選項值\n- 選項2,是否為預設,選項值", + "transitGSOrderSettingTitle": "訂單匯出設定", + "transitGSOrderSettingOverwriteLabel": "是否覆寫表單", + "transitGSOrderSettingOverwriteHint": "覆寫表單之後,將會從第一行開始匯出", + "transitGSOrderSettingTitlePrefixLabel": "加上日期前綴", + "transitGSOrderSettingTitlePrefixHint": "表單名稱前面加上日期前綴,例如:「0101 - 0131 訂單資料」", + "transitGSOrderSettingRecommendCombination": "不覆寫而改用附加的時候,建議表單名稱「不要」加上日期前綴", + "transitGSOrderSettingNameLabel": "表單名稱", + "transitGSOrderSettingNameHelper": "拆分表單可以讓你更彈性的去分析資料,\n例如可以到訂單成份細項查詢:今天某個成分總共用了多少。", + "transitGSOrderMetaOverwrite": "{value, select, true{會覆寫} false{不會覆寫} other{UNKNOWN}}", + "@transitGSOrderMetaOverwrite": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "transitGSOrderMetaTitlePrefix": "{value, select, true{有日期前綴} false{沒有日期前綴} other{UNKNOWN}}", + "@transitGSOrderMetaTitlePrefix": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "transitGSOrderMetaMemoryWarning": "這裡的容量代表網路傳輸所消耗的量,實際佔用的雲端記憶體可能是此值的百分之一而已。\n詳細容量限制說明可以參考[本文件](https://developers.google.com/sheets/api/limits#quota)。", + "transitGSOrderHeaderTs": "時間戳記", + "transitGSOrderHeaderTime": "時間", + "transitGSOrderHeaderPrice": "總價", + "transitGSOrderHeaderProductPrice": "產品總價", + "transitGSOrderHeaderPaid": "付額", + "transitGSOrderHeaderCost": "成本", + "transitGSOrderHeaderProfit": "收入", + "transitGSOrderHeaderItemCount": "產品份數", + "@transitGSOrderHeaderItemCount": { + "description": "how many items in the order" + }, + "transitGSOrderHeaderTypeCount": "產品類數", + "@transitGSOrderHeaderTypeCount": { + "description": "how many types of products in the order" + }, + "transitGSOrderAttributeTitle": "訂單顧客設定", + "transitGSOrderAttributeHeaderTs": "時間戳記", + "transitGSOrderAttributeHeaderName": "設定類別", + "transitGSOrderAttributeHeaderOption": "選項", + "transitGSOrderProductTitle": "訂單產品細項", + "transitGSOrderProductHeaderTs": "時間戳記", + "transitGSOrderProductHeaderName": "產品", + "transitGSOrderProductHeaderCatalog": "種類", + "transitGSOrderProductHeaderCount": "數量", + "transitGSOrderProductHeaderPrice": "單一售價", + "transitGSOrderProductHeaderCost": "單一成本", + "transitGSOrderProductHeaderOrigin": "單一原價", + "transitGSOrderIngredientTitle": "訂單成分細項", + "transitGSOrderIngredientHeaderTs": "時間戳記", + "transitGSOrderIngredientHeaderName": "成分", + "transitGSOrderIngredientHeaderQuantity": "份量", + "transitGSOrderIngredientHeaderAmount": "數量", + "transitGSOrderExpandableHint": "詳見下欄", + "transitGSErrorCreateSpreadsheet": "無法建立試算表", + "transitGSErrorCreateSpreadsheetHelper": "別擔心,通常都可以簡單解決!\n可能的原因有:\n• 網路狀況不穩;\n• 尚未授權 POS 系統進行表單的編輯。", + "transitGSErrorSpreadsheetEmpty": "請先選擇試算表", + "transitGSErrorSpreadsheetIdEmpty": "不能為空", + "transitGSErrorSpreadsheetIdInvalid": "不合法的文字,必須包含:\n/spreadsheets/d//\n或者直接給 ID(英文+數字+底線+減號的組合)", + "transitGSErrorCreateSheet": "無法在試算表中建立表單", + "transitGSErrorCreateSheetHelper": "別擔心,通常都可以簡單解決!\n可能的原因有:\n• 網路狀況不穩;\n• 尚未授權 POS 系統進行表單的建立;\n• 試算表 ID 打錯了,請嘗試複製整個網址後貼上;\n• 該試算表被刪除了。", + "transitGSErrorSheetRepeat": "表單名稱重複", + "transitGSErrorSheetEmpty": "請選擇至少一個表單來匯出", + "transitGSErrorNonExistName": "找不到試算表,是否已被刪除?", + "transitGSErrorImportEmptySpreadsheet": "必須選擇試算表來匯入", + "transitGSErrorImportEmptySheet": "必須選擇指定的表單來匯入", + "transitGSErrorImportEmptyData": "在表單中沒找到任何值", + "transitGSErrorImportNotFoundSpreadsheet": "找不到試算表", + "transitGSErrorImportNotFoundSheets": "找不到表單「{name}」的資料", + "@transitGSErrorImportNotFoundSheets": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "transitGSErrorImportNotFoundHelper": "別擔心,通常都可以簡單解決!\n可能的原因有:\n• 網路狀況不穩;\n• 尚未授權 POS 系統進行表單的讀取;\n• 試算表 ID 打錯了,請嘗試複製整個網址後貼上;\n• 該試算表被刪除了。", + "transitPTDescription": "快速檢查、快速分享。", + "transitPTCopyBtn": "複製文字", + "transitPTCopySuccess": "複製成功", + "transitPTCopyWarning": "複製過大的文字可能會造成系統的崩潰", + "transitPTImportHint": "請貼上複製而來的文字", + "transitPTImportHelper": "貼上文字後,會分析文字並決定匯入的是什麼種類的資訊。", + "transitPTImportErrorNotFound": "這段文字無法匹配相應的服務,請參考匯出時的文字內容。", + "transitPTFormatOrderPrice": "{hasProducts, plural, =0{共 {price} 元。} other{共 {price} 元,其中的 {productsPrice} 元是產品價錢。}}", + "@transitPTFormatOrderPrice": { + "placeholders": { + "hasProducts": { + "type": "int" + }, + "price": { + "type": "String" + }, + "productsPrice": { + "type": "String" + } + } + }, + "transitPTFormatOrderMoney": "付額 {paid} 元、成分 {cost} 元。", + "@transitPTFormatOrderMoney": { + "placeholders": { + "paid": { + "type": "String" + }, + "cost": { + "type": "String" + } + } + }, + "transitPTFormatOrderProductCount": "{count, plural, =0{沒有任何餐點。} =1{餐點有 {count} 份,內容為:\n{products}。} other{餐點有 {count} 份({setCount} 種組合)包括:\n{products}。}}", + "@transitPTFormatOrderProductCount": { + "placeholders": { + "count": { + "type": "int" + }, + "setCount": { + "type": "int" + }, + "products": { + "type": "String" + } + } + }, + "transitPTFormatOrderProduct": "{hasIngredient, plural, =0{{product}({catalog}){count} 份共 {price} 元,沒有設定成分} other{{product}({catalog}){count} 份共 {price} 元,成份包括 {ingredients}}}", + "@transitPTFormatOrderProduct": { + "placeholders": { + "hasIngredient": { + "type": "int" + }, + "product": { + "type": "String" + }, + "catalog": { + "type": "String" + }, + "count": { + "type": "int" + }, + "price": { + "type": "String" + }, + "ingredients": { + "type": "String" + } + } + }, + "transitPTFormatOrderIngredient": "{amount, plural, =0{{ingredient}({quantity})} other{{ingredient}({quantity}),使用 {amount} 個}}", + "@transitPTFormatOrderIngredient": { + "description": "Details of ingredients and quantities for each product in the order list", + "placeholders": { + "amount": { + "type": "num", + "format": "decimalPattern" + }, + "ingredient": { + "type": "String" + }, + "quantity": { + "type": "String" + } + } + }, + "transitPTFormatOrderNoQuantity": "預設份量", + "transitPTFormatOrderOrderAttribute": "顧客的 {options}", + "@transitPTFormatOrderOrderAttribute": { + "placeholders": { + "options": { + "type": "String" + } + } + }, + "transitPTFormatOrderOrderAttributeItem": "{name} 為 {option}", + "@transitPTFormatOrderOrderAttributeItem": { + "placeholders": { + "name": { + "type": "String" + }, + "option": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuMetaCatalog": "{count} 個產品種類", + "@transitPTFormatModelMenuMetaCatalog": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelMenuMetaProduct": "{count} 個產品", + "@transitPTFormatModelMenuMetaProduct": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelMenuHeader": "本菜單共有 {catalogs} 個產品種類、{products} 個產品。", + "@transitPTFormatModelMenuHeader": { + "placeholders": { + "catalogs": { + "type": "int" + }, + "products": { + "type": "int" + } + } + }, + "transitPTFormatModelMenuHeaderPrefix": "本菜單", + "@transitPTFormatModelMenuHeaderPrefix": { + "description": "This is used to check if this text is a menu" + }, + "transitPTFormatModelMenuCatalog": "第{index}個種類叫做 {catalog},{details}。", + "@transitPTFormatModelMenuCatalog": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "catalog": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuCatalogDetails": "{count, plural, =0{沒有設定產品} other{共有 {count} 個產品}}", + "@transitPTFormatModelMenuCatalogDetails": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelMenuProduct": "第{index}個產品叫做 {name},其售價為 {price} 元,成本為 {cost} 元,{details}", + "@transitPTFormatModelMenuProduct": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "price": { + "type": "String" + }, + "cost": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuProductDetails": "{count, plural, =0{它沒有設定任何成份。} other{它的成份有 {count} 種:{names}。\n每份產品預設需要使用 {details}。}}", + "@transitPTFormatModelMenuProductDetails": { + "placeholders": { + "count": { + "type": "int" + }, + "names": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuIngredient": "{amount} 個 {name},{details}", + "@transitPTFormatModelMenuIngredient": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "amount": { + "type": "String" + }, + "name": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuIngredientDetails": "{count, plural, =0{無法做份量調整} other{它還有 {count} 個不同份量 {quantities}}}", + "@transitPTFormatModelMenuIngredientDetails": { + "placeholders": { + "count": { + "type": "int" + }, + "quantities": { + "type": "String" + } + } + }, + "transitPTFormatModelMenuQuantity": "每份產品改成使用 {amount} 個並調整產品售價 {price} 元和成本 {cost} 元", + "@transitPTFormatModelMenuQuantity": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "amount": { + "type": "String" + }, + "price": { + "type": "String" + }, + "cost": { + "type": "String" + } + } + }, + "transitPTFormatModelStockMetaIngredient": "{count} 種成分", + "@transitPTFormatModelStockMetaIngredient": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelStockHeader": "本庫存共有 {count} 種成分。", + "@transitPTFormatModelStockHeader": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelStockHeaderPrefix": "本庫存", + "@transitPTFormatModelStockHeaderPrefix": { + "description": "This is used to check if this text is stock" + }, + "transitPTFormatModelStockIngredient": "第{index}個成分叫做 {name},庫存現有 {amount} 個{details}。", + "@transitPTFormatModelStockIngredient": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "amount": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelStockIngredientDetails": "{exist, plural, =0{} other{,最大量有 {max} 個}}", + "@transitPTFormatModelStockIngredientDetails": { + "description": "String(max) are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "exist": { + "type": "int" + }, + "max": { + "type": "String" + } + } + }, + "transitPTFormatModelQuantitiesMetaQuantity": "{count} 種份量", + "@transitPTFormatModelQuantitiesMetaQuantity": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelQuantitiesHeader": "共設定 {count} 種份量。", + "@transitPTFormatModelQuantitiesHeader": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelQuantitiesHeaderSuffix": "種份量。", + "@transitPTFormatModelQuantitiesHeaderSuffix": { + "description": "This is used to check if this text is quantities" + }, + "transitPTFormatModelQuantitiesQuantity": "第{index}種份量叫做 {name},預設會讓成分的份量乘以 {prop} 倍。", + "@transitPTFormatModelQuantitiesQuantity": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "prop": { + "type": "String" + } + } + }, + "transitPTFormatModelReplenisherMetaReplenishment": "{count} 種補貨方式", + "@transitPTFormatModelReplenisherMetaReplenishment": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelReplenisherHeader": "共設定 {count} 種補貨方式。", + "@transitPTFormatModelReplenisherHeader": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelReplenisherHeaderSuffix": "種補貨方式。", + "@transitPTFormatModelReplenisherHeaderSuffix": { + "description": "This is used to check if this text is replenishment quantity" + }, + "transitPTFormatModelReplenisherReplenishment": "第{index}個成分叫做 {name},{details}。", + "@transitPTFormatModelReplenisherReplenishment": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelReplenisherReplenishmentDetails": "{count, plural, =0{它並不會調整庫存} other{它會調整{count}種成份的庫存}}", + "@transitPTFormatModelReplenisherReplenishmentDetails": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelOaMetaOa": "{count} 種顧客屬性", + "@transitPTFormatModelOaMetaOa": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelOaHeader": "共設定 {count} 種顧客屬性。", + "@transitPTFormatModelOaHeader": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelOaHeaderSuffix": "種顧客屬性。", + "@transitPTFormatModelOaHeaderSuffix": { + "description": "This is used to check if this text is customer settings" + }, + "transitPTFormatModelOaOa": "第{index}種屬性叫做 {name},屬於 {mode} 類型,{details}。", + "@transitPTFormatModelOaOa": { + "description": "Strings are used so that regex can be inserted here during import to obtain information", + "placeholders": { + "index": { + "type": "String" + }, + "name": { + "type": "String" + }, + "mode": { + "type": "String" + }, + "details": { + "type": "String" + } + } + }, + "transitPTFormatModelOaOaDetails": "{count, plural, =0{它並沒有設定選項} other{它有 {count} 個選項}}", + "@transitPTFormatModelOaOaDetails": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "transitPTFormatModelOaDefaultOption": "預設", + "transitPTFormatModelOaModeValue": "選項的值為 {value}", + "@transitPTFormatModelOaModeValue": { + "placeholders": { + "value": { + "type": "num", + "format": "decimalPattern" + } + } + }, "appTitle": "POS 系統", "actSuccess": "執行成功", "@actSuccess": { - "description": "執行行為成功,並顯示於 Snackbar 中" + "description": "Action executed successfully and displayed on the Snackbar." }, "actError": "發生未知錯誤", "@actError": { - "description": "當發生錯誤時,會顯示在 SnackBar 上的文字" + "description": "Error message displayed on the Snackbar when an error occurs." + }, + "actMoreInfo": "說明", + "@actMoreInfo": { + "description": "Button on the Snackbar to show more details." }, - "btnImport": "匯入", - "btnExport": "匯出", "singleChoice": "一次只能選擇一種", + "@singleChoice": { + "description": "Reminder to the user that only one option can be selected at a time." + }, "multiChoices": "可以選擇多種", - "totalCount": "總共 {count} 項", + "@multiChoices": { + "description": "Reminder to the user that multiple options can be selected." + }, + "totalCount": "{count, plural, other{總共 {count} 項}}", "@totalCount": { - "description": "顯示於 ListView 上的數量總數", + "description": "Total count displayed on the ListView.", "placeholders": { "count": { "type": "int", - "example": "1" + "format": "compactLong" } } }, "searchCount": "搜尋到 {count} 個結果", "@searchCount": { - "description": "顯示於 SearchScaffold 上的數量總數", + "description": "Total count displayed on the SearchScaffold.", "placeholders": { "count": { "type": "int", - "example": "1" + "format": "compact" } } }, "dialogDeletionTitle": "刪除確認通知", "@dialogDeletionTitle": { - "description": "顯示於 DeleteDialog 上的標題" + "description": "Title displayed on the DeleteDialog." }, - "dialogDeletionContent": "確定要刪除「{name}」嗎?\n\n{more}此動作將無法復原!", + "dialogDeletionContent": "確定要刪除「{name}」嗎?\n\n{more}此動作將無法復原!\n", "@dialogDeletionContent": { - "description": "顯示於 DeleteDialog 上的內文", + "description": "Content displayed on the DeleteDialog.", "placeholders": { "name": { "type": "String", - "example": "蔬菜三明治" + "example": "Veggie Sandwich" }, "more": { - "description": "更多訊息,例如刪除此項目會有什麼其他影響", - "type": "String" + "type": "String", + "description": "More details about the side effects of deletion", + "example": "What other impacts deleting this item will have" + } + } + }, + "imageHolderCreate": "點選以新增圖片", + "imageHolderUpdate": "點擊以更新圖片", + "imageBtnCrop": "裁切", + "imageGalleryTitle": "圖片管理", + "imageGalleryEmpty": "點擊開始匯入你的第一張照片!", + "imageGalleryActionCreate": "新增圖片", + "imageGalleryActionDelete": "刪除", + "imageGallerySnackbarDeleteFailed": "有一個或多個圖片沒有刪成功。", + "imageGallerySelectionTitle": "選擇相片", + "imageGallerySelectionDeleteConfirm": "將會刪除 {count} 個圖片\n刪除之後會讓相關產品顯示不到圖片", + "@imageGallerySelectionDeleteConfirm": { + "placeholders": { + "count": { + "type": "int", + "format": "compact" } } }, - "emptyBodyContent": "哎呀!這裡還是空的", - "@emptyBodyContent": { - "description": "顯示於 EmptyBody 的文字,告訴使用者現在並未有任何項目" + "emptyBodyTitle": "哎呀!這裡還是空的", + "@emptyBodyTitle": { + "description": "Text displayed on EmptyBody, informing the user that there are no items yet. This is the default text." }, - "invalidNumberType": "{field}必須是數字", - "@invalidNumberType": { - "description": "當發現輸入不是數字時的警語", + "emptyBodyAction": "立即設定", + "btnNavTo": "查看", + "@btnNavTo": { + "description": "Button text to navigate to another screen in trailing." + }, + "btnSignInWithGoogle": "使用 Google 登入", + "semanticsPercentileBar": "目前佔總數的 {percent}", + "@semanticsPercentileBar": { "placeholders": { - "field": { - "description": "欄位名稱", - "type": "String" + "percent": { + "type": "num", + "format": "percentPattern" } } }, "invalidIntegerType": "{field}必須是整數", "@invalidIntegerType": { - "description": "當發現輸入不是整數時的警語", + "description": "Warning message when the input is not an integer.", + "placeholders": { + "field": { + "type": "String" + } + } + }, + "invalidNumberType": "{field}必須是數字", + "@invalidNumberType": { + "description": "Warning message when the input is not a number.", "placeholders": { "field": { - "description": "欄位名稱", "type": "String" } } }, - "invalidPositiveNumber": "{field}不能為負數", - "@invalidPositiveNumber": { - "description": "當發現輸入不是正數時的警語", + "invalidNumberPositive": "{field}不能為負數", + "@invalidNumberPositive": { + "description": "Warning message when the input is not positive.", "placeholders": { "field": { - "description": "欄位名稱", "type": "String" } } }, - "invalidNumberMaximum": "{field}不能大於 {maximum}", + "invalidNumberMaximum": "{field}不能超過 {maximum}", "@invalidNumberMaximum": { - "description": "當發現輸入過大時的警語", + "description": "Warning message when the input exceeds the maximum value.", "placeholders": { "field": { - "description": "欄位名稱", "type": "String" }, "maximum": { - "description": "最大值,必須小於(且不等於)該值", "type": "num", + "description": "Maximum value", "format": "decimalPattern" } } }, - "invalidNumberMinimum": "{field}不能大於 {minimum}", + "invalidNumberMinimum": "{field}不能低於 {minimum}", "@invalidNumberMinimum": { - "description": "當發現輸入過小時的警語", + "description": "Warning message when the input is less than the minimum value.", "placeholders": { "field": { - "description": "欄位名稱", "type": "String" }, "minimum": { - "description": "最小值,必須大於(且不等於)該值", "type": "num", + "description": "Minimum value", "format": "decimalPattern" } } }, - "invalidEmptyString": "{field}不能為空", - "@invalidEmptyString": { - "description": "當發現尚未輸入文字的警語", + "invalidStringEmpty": "{field}不能為空", + "@invalidStringEmpty": { + "description": "Warning message when no text is entered.", "placeholders": { "field": { - "description": "欄位名稱", "type": "String" } } }, - "invalidStringMaximum": "{field}的長度不能超過 {maximum}", + "invalidStringMaximum": "{field}不能超過 {maximum} 個字", "@invalidStringMaximum": { - "description": "當發現尚未輸入文字的警語", + "description": "Warning message when the input exceeds the maximum character limit.", "placeholders": { "field": { - "description": "欄位名稱", "type": "String" }, "maximum": { - "description": "最大值,文字長度必須小於(且不等於)該值", - "type": "int" + "type": "int", + "description": "Maximum number of characters" + } + } + }, + "singleMonth": "單月", + "@singleMonth": { + "description": "One of the units for calendar period conversion." + }, + "singleWeek": "單週", + "@singleWeek": { + "description": "One of the units for calendar period conversion." + }, + "twoWeeks": "雙週", + "@twoWeeks": { + "description": "One of the units for calendar period conversion." + }, + "orderAttributeTitle": "顧客設定", + "orderAttributeDescription": "內用、外帶等幫助分析的資訊", + "orderAttributeTitleCreate": "新增顧客設定", + "orderAttributeTitleUpdate": "編輯顧客設定", + "orderAttributeTitleReorder": "排序顧客設定", + "orderAttributeEmptyBody": "顧客設定可以幫助我們統計哪些人來消費,例如:\n20-30歲、外帶、上班族。", + "orderAttributeHeaderInfo": "顧客設定", + "@orderAttributeHeaderInfo": { + "description": "Displayed on the upper rectangle in homepage" + }, + "orderAttributeTutorialTitle": "顧客設定", + "orderAttributeTutorialContent": "這裡是用來設定顧客的資訊,例如:內用、外帶、上班族等。\n這些資訊可以幫助我們統計哪些人來消費,進而做出更好的經營策略。", + "orderAttributeMetaMode": "種類:{name}", + "@orderAttributeMetaMode": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "orderAttributeMetaDefault": "預設:{name}", + "@orderAttributeMetaDefault": { + "placeholders": { + "name": { + "type": "String" } } }, - "homeTabAnalysis": "統計", - "homeTabStock": "庫存", - "homeTabCashier": "收銀", - "homeTabSetting": "設定", - "analysisCalendarMonth": "單月", - "@analysisCalendarMonth": { - "description": "分析頁的日曆週期轉換單位之一" + "orderAttributeMetaNoDefault": "未設定預設", + "orderAttributeModeDivider": "顧客設定種類", + "orderAttributeModeName": "{name, select, statOnly{一般} changePrice{變價} changeDiscount{折扣} other{UNKNOWN}}", + "@orderAttributeModeName": { + "description": "name" + }, + "orderAttributeModeHelper": "{name, select, statOnly{一般的設定,選取時並不會影響點單價格。} changePrice{選取設定時,可能會影響價格。\n例如:外送 + 30塊錢、環保杯 - 5塊錢。\n} changeDiscount{選取設定時,會根據折扣影響總價。\n例如:內用 + 10% 服務費、親友價 - 10%。\n} other{UNKNOWN}}", + "@orderAttributeModeHelper": { + "description": "Explanation of customer setting categories", + "placeholders": { + "name": { + "type": "String" + } + } }, - "analysisCalendarWeek": "單週", - "@analysisCalendarWeek": { - "description": "分析頁的日曆週期轉換單位之一" + "orderAttributeNameLabel": "顧客設定名稱", + "orderAttributeNameHint": "例如:顧客年齡", + "orderAttributeNameErrorRepeat": "名稱不能重複", + "orderAttributeOptionTitleCreate": "新增選項", + "orderAttributeOptionTitleCreateWith": "新增{name}的選項", + "@orderAttributeOptionTitleCreateWith": { + "placeholders": { + "name": { + "type": "String" + } + } }, - "analysisCalendarTwoWeek": "雙週", - "@analysisCalendarTwoWeek": { - "description": "分析頁的日曆週期轉換單位之一" + "orderAttributeOptionTitleUpdate": "編輯選項", + "orderAttributeOptionTitleReorder": "排序選項", + "orderAttributeOptionMetaDefault": "預設", + "orderAttributeOptionNameLabel": "選項名稱", + "orderAttributeOptionNameHelper": "以年齡為例,可能的選項有:\n- 20 歲以下\n- 20 到 30 歲", + "orderAttributeOptionNameErrorRepeat": "名稱不能重複", + "orderAttributeOptionModeTitle": "選項模式", + "orderAttributeOptionModeHelper": "{name, select, statOnly{因為本設定為「一般」故無須設定「折價」或「變價」} changePrice{訂單時選擇此項會套用此變價} changeDiscount{訂單時選擇此項會套用此折價} other{UNKNOWN}}", + "@orderAttributeOptionModeHelper": { + "description": "Explanation of mode", + "placeholders": { + "name": { + "type": "String" + } + } }, - "analysisOrderListItemMetaPrice": "售價:{price}", - "@analysisOrderListItemMetaPrice": { - "description": "分析頁的點餐列表中,每次點餐的售價值", + "orderAttributeOptionModeHint": "{name, select, statOnly{} changePrice{例如:-30 代表減少三十塊} changeDiscount{例如:80 代表「八折」} other{UNKNOWN}}", + "@orderAttributeOptionModeHint": { "placeholders": { - "price": { - "type": "num", - "format": "currency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "name": { + "type": "String" } } }, - "analysisOrderListItemMetaPaid": "付額:{paid}", - "@analysisOrderListItemMetaPaid": { - "description": "分析頁的點餐列表中,每次點餐的付額", + "orderAttributeOptionToDefaultLabel": "設為預設", + "orderAttributeOptionToDefaultHelper": "設定此選項為預設值,每個訂單預設都會是使用這個選項。", + "orderAttributeOptionToDefaultConfirmChangeTitle": "覆蓋選項預設?", + "orderAttributeOptionToDefaultConfirmChangeContent": "這麼做會讓「{name}」變成非預設值", + "@orderAttributeOptionToDefaultConfirmChangeContent": { + "description": "Prompt to ensure the user knows what the original default value was", "placeholders": { - "paid": { - "type": "num", - "format": "currency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "name": { + "type": "String" } } }, - "analysisOrderListItemMetaIncome": "淨利:{income}", - "@analysisOrderListItemMetaIncome": { - "description": "分析頁的點餐列表中,每次點餐的淨利", + "orderAttributeValueEmpty": "不影響價錢", + "orderAttributeValueFree": "免費", + "orderAttributeValueDiscountIncrease": "增加至 {value} 倍", + "@orderAttributeValueDiscountIncrease": { "placeholders": { - "income": { + "value": { "type": "num", - "format": "currency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "format": "decimalPattern" } } }, - "orderListEmpty": "查無點餐紀錄", - "orderListMetaPrice": "總營收:{price}", - "@orderListMetaPrice": { - "description": "分析頁的點餐列表中,單日點餐的營收", + "orderAttributeValueDiscountDecrease": "減少至 {value} 倍", + "@orderAttributeValueDiscountDecrease": { "placeholders": { - "price": { + "value": { "type": "num", - "format": "compactCurrency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "format": "decimalPattern" } } }, - "orderListMetaCount": "總單數:{count}", - "@orderListMetaCount": { - "description": "分析頁的點餐列表中,單日點餐的數量", + "orderAttributeValuePriceIncrease": "增加 {value} 元", + "@orderAttributeValuePriceIncrease": { "placeholders": { - "count": { - "type": "int" + "value": { + "type": "String" } } }, - "menuTitle": "菜單", - "menuSearchProductHint": "搜尋產品、成分、份量", - "@menuSearchProductHint": { - "description": "搜尋產品時的文字匡 placeholder" - }, - "menuSearchProductNotFound": "搜尋不到相關資訊,打錯字了嗎?", - "@menuSearchProductNotFound": { - "description": "搜尋產品時找不到該文字的搜尋結果" - }, - "menuCatalogListEmptyProduct": "尚未設定產品", - "@menuCatalogListEmptyProduct": { - "description": "當產品種類並未有產品時在列表中的子標題文字" - }, - "menuCatalogCreate": "新增產品種類", - "@menuCatalogCreate": { - "description": "菜單頁的 floatingActionButton 說明" - }, - "menuCatalogUpdate": "編輯產品種類", - "@menuCatalogUpdate": { - "description": "菜單頁編輯產品種類" - }, - "menuCatalogReorder": "排序產品種類", - "@menuCatalogReorder": { - "description": "菜單頁排序產品種類" - }, - "menuCatalogMetaTitle": "產品種類", - "menuCatalogMetaCreatedAt": "建立時間:{createdAt}", - "@menuCatalogMetaCreatedAt": { - "description": "產品種類建立時間", + "orderAttributeValuePriceDecrease": "減少 {value} 元", + "@orderAttributeValuePriceDecrease": { "placeholders": { - "createdAt": { - "type": "DateTime", - "format": "MMMd" + "value": { + "type": "String" } } }, - "menuCatalogEmptyBody": "可以新增產品囉!", - "@menuCatalogEmptyBody": { - "description": "若產品種類沒有任何產品,在其產品種類頁會顯示的文字" - }, - "menuCatalogDialogDeletionContent": "{count, plural, =0{其內無任何產品\n\n} other{將會一同刪除掉 {count} 個產品\n\n}}", + "menuTitle": "菜單", + "menuSubtitle": "產品種類、產品", + "menuTutorialTitle": "建立屬於你的菜單", + "menuTutorialContent": "首先我們來開始建立一份菜單吧!", + "menuSearchHint": "搜尋產品、成分、份量", + "menuSearchNotFound": "搜尋不到相關資訊,打錯字了嗎?", + "menuCatalogHeaderInfo": "種類", + "@menuCatalogHeaderInfo": { + "description": "Displayed on the upper rectangle in homepage" + }, + "menuCatalogTutorialTitle": "Create First Catalog", + "menuCatalogEmptyBody": "我們會把相似「產品」放在「產品種類」中,\n到時候點餐會比較方便,例如:\n• 「起司漢堡」、「蔬菜漢堡」整合進「漢堡」\n• 「塑膠袋」、「環保杯」整合進「其他」", + "menuCatalogTitleCreate": "新增產品種類", + "@menuCatalogTitleCreate": { + "description": "FloatingActionButton description on the menu page" + }, + "menuCatalogTitleUpdate": "編輯產品種類", + "menuCatalogTitleReorder": "排序產品種類", + "menuCatalogDialogDeletionContent": "{count, plural, =0{其內無任何產品} other{將會一同刪除掉 {count} 個產品}}", "@menuCatalogDialogDeletionContent": { - "description": "菜單頁刪除產品種類時的警語", + "description": "Warning message when deleting product categories on the menu page", "placeholders": { "count": { - "description": "產品數量", "type": "int" } } }, - "menuCatalogNameLabel": "種類名稱", - "@menuCatalogNameLabel": { - "description": "產品種類名稱的標題" - }, + "menuCatalogNameLabel": "產品種類名稱", "menuCatalogNameHint": "例如:漢堡", - "@menuCatalogNameHint": { - "description": "產品種類名稱的範例" - }, - "menuCatalogNameRepeatError": "種類名稱重複", - "@menuCatalogNameRepeatError": { - "description": "產品種類名稱發現重複的說明" - }, + "menuCatalogNameErrorRepeat": "名稱重複了,請改個名字吧!", + "menuCatalogEmptyProducts": "尚未設定產品", + "menuProductHeaderInfo": "產品", + "@menuProductHeaderInfo": { + "description": "Displayed on the upper rectangle in homepage" + }, + "menuProductEmptyBody": "「產品」是菜單裡的基本單位,例如:\n「起司漢堡」、「可樂」", + "menuProductTitleCreate": "新增產品", + "menuProductTitleUpdate": "編輯產品", + "menuProductTitleReorder": "排序產品", + "menuProductTitleUpdateImage": "更新照片", "menuProductMetaTitle": "產品", + "@menuProductMetaTitle": { + "description": "Prefix for meta, so users know this is product meta info, not category" + }, "menuProductMetaPrice": "價格:{price}", "@menuProductMetaPrice": { - "description": "產品價格", + "description": "Price of the product", "placeholders": { "price": { "type": "num", - "format": "compactCurrency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "format": "compact" } } }, "menuProductMetaCost": "成本:{cost}", "@menuProductMetaCost": { - "description": "產品成本", + "description": "Cost of the product", "placeholders": { "cost": { "type": "num", - "format": "compactCurrency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "format": "compact" } } }, - "menuProductListEmptyIngredient": "尚未設定成分", - "menuProductCreate": "新增產品", - "menuProductUpdate": "編輯產品", - "menuProductReorder": "排序產品", - "menuProductEmptyBody": "可以設定產品的成分囉!", + "menuProductMetaEmpty": "尚未設定成分", + "@menuProductMetaEmpty": { + "description": "Text displayed in the subtitle in the product list" + }, "menuProductNameLabel": "產品名稱", "menuProductNameHint": "例如:起司漢堡", - "menuProductNameRepeatError": "產品名稱重複", + "menuProductNameErrorRepeat": "產品名稱重複", "menuProductPriceLabel": "產品價格", - "menuProductPriceHint": "給客人看的價錢", + "menuProductPriceHelper": "訂單頁面會呈現的價錢", "menuProductCostLabel": "產品成本", - "menuProductCostHint": "用來算出利潤,理應小於價錢", + "menuProductCostHelper": "用來算出利潤,理應小於價錢", + "menuProductEmptyIngredients": "尚未設定成分", + "menuIngredientEmptyBody": "你可以在產品中設定成分等資訊,例如:\n「起司漢堡」有「起司」、「麵包」等成分", + "menuIngredientTitleCreate": "新增成分", + "menuIngredientTitleUpdate": "編輯成分", + "menuIngredientTitleReorder": "排序成分", "menuIngredientMetaAmount": "使用量:{amount}", "@menuIngredientMetaAmount": { "placeholders": { "amount": { "type": "num", - "format": "compact" + "format": "decimalPattern" } } }, - "menuIngredientCreate": "新增成分", - "menuIngredientUpdate": "編輯成分", - "menuIngredientSearchLabel": "成分", + "menuIngredientSearchLabel": "搜尋成分", + "menuIngredientSearchHelper": "新增成分後,可至「庫存」設定相關資訊。", + "menuIngredientSearchHint": "例如:起司", "menuIngredientSearchAdd": "新增成分「{name}」", "@menuIngredientSearchAdd": { - "description": "當搜尋不到成分時,需要新增成分", + "description": "Button to add ingredient if search result not found", "placeholders": { "name": { - "description": "成分名稱", "type": "String" } } }, - "menuIngredientSearchHint": "搜尋成分,例如:起司", - "menuIngredientSearchEmptyError": "必須設定成分,請點選以設定。", - "menuIngredientSearchHelper": "新增成分後,可至「庫存」設定相關資訊。", - "menuIngredientRepeatError": "成分不能重複選取。", + "menuIngredientSearchErrorEmpty": "必須設定成分,請點選以設定。", + "menuIngredientSearchErrorRepeat": "產品已經有相同的成分了,不能重複選取。", "menuIngredientAmountLabel": "使用量", "menuIngredientAmountHelper": "預設的使用量,若餐點可以調整該成分的使用量,請於成分的「份量」中設定。", + "menuQuantityTitleCreate": "新增份量", + "menuQuantityTitleUpdate": "編輯份量", "menuQuantityMetaAmount": "使用量:{amount}", "@menuQuantityMetaAmount": { "placeholders": { "amount": { "type": "num", - "format": "compact" + "format": "decimalPattern" } } }, - "menuQuantityMetaPrice": "額外售價:{price}", - "@menuQuantityMetaPrice": { + "menuQuantityMetaAdditionalPrice": "額外售價:{price}", + "@menuQuantityMetaAdditionalPrice": { "placeholders": { "price": { - "type": "num", - "format": "compactCurrency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "type": "String" } } }, - "menuQuantityMetaCost": "額外成本:{cost}", - "@menuQuantityMetaCost": { + "menuQuantityMetaAdditionalCost": "額外成本:{cost}", + "@menuQuantityMetaAdditionalCost": { "placeholders": { "cost": { - "type": "num", - "format": "compactCurrency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "type": "String" } } }, - "menuQuantityCreate": "新增份量", - "menuQuantitySearchLabel": "份量", + "menuQuantitySearchLabel": "搜尋份量", + "menuQuantitySearchHelper": "新增成分份量後,可至「份量」設定相關資訊。", + "menuQuantitySearchHint": "例如:多量、少量", "menuQuantitySearchAdd": "新增份量「{name}」", "@menuQuantitySearchAdd": { - "description": "當搜尋不到份量時,需要新增份量", + "description": "Button to add quantity if search result not found", "placeholders": { "name": { - "description": "份量名稱", "type": "String" } } }, - "menuQuantitySearchHint": "搜尋份量", - "menuQuantitySearchEmptyError": "必須設定份量,請點選以設定。", - "menuQuantitySearchHelper": "新增成分份量後,可至「份量」設定相關資訊。", - "menuQuantityRepeatError": "份量不能重複選取。", + "menuQuantitySearchErrorEmpty": "必須設定份量,請點選以設定。", + "menuQuantitySearchErrorRepeat": "產品已經有相同的份量了,不能重複選取。", "menuQuantityAmountLabel": "使用量", "menuQuantityAdditionalPriceLabel": "額外售價", "menuQuantityAdditionalPriceHelper": "設為 0 則代表加量(減量)不加價。", "menuQuantityAdditionalCostLabel": "額外成本", "menuQuantityAdditionalCostHelper": "預額外成本可以為負數,如「少量」會減少成分的使用,相對成本降低。", - "stockHasNotReplenishEver": "尚未補貨過", - "@stockHasNotReplenishEver": { - "description": "庫存頁面會顯示上次補貨時間,若未補貨過則設此文字" - }, - "stockUpdatedAt": "上次補貨時間:{updatedAt}", - "@stockUpdatedAt": { - "description": "庫存上次補貨的時間", + "cashierTab": "收銀", + "cashierUnitLabel": "幣值:{unit}", + "@cashierUnitLabel": { "placeholders": { - "updatedAt": { - "type": "DateTime", - "format": "MMMEd" + "unit": { + "type": "String" } } }, - "stockIngredientCreate": "新增成分", - "stockIngredientDialogDeletionContent": "{count, plural, =0{目前無任何產品有本成分\n\n} other{將會一同刪除掉 {count} 個產品的成分\n\n}}", - "@stockIngredientDialogDeletionContent": { - "description": "刪除成分時會提示多少個產品受影響", - "placeholders": { - "count": { - "description": "產品數量", - "type": "int" + "cashierCounterLabel": "數量", + "@cashierCounterLabel": { + "description": "設定幣值數量時的標籤" + }, + "cashierToDefaultTitle": "設為預設", + "cashierToDefaultTutorialTitle": "收銀機預設狀態", + "cashierToDefaultTutorialContent": "在下面設定完收銀機各幣值的數量後,\n按這裡設定預設狀態!\n設定好的數量就會是各個幣值狀態條的「最大值」。", + "cashierToDefaultDialogTitle": "調整收銀臺預設?", + "cashierToDefaultDialogContent": "這將會把目前的收銀機狀態設定為預設狀態。\n此動作將會覆蓋掉先前的設定。", + "cashierChangerTitle": "換錢", + "cashierChangerButton": "套用", + "cashierChangerTutorialTitle": "收銀機換錢", + "cashierChangerTutorialContent": "一百塊換成 10 個十塊之類。\n幫助快速調整收銀機狀態。", + "cashierChangerErrorNoSelection": "請選擇要套用的組合", + "cashierChangerErrorNotEnough": "{unit} 元不夠換", + "@cashierChangerErrorNotEnough": { + "placeholders": { + "unit": { + "type": "String" } } }, - "stockIngredientConnectedProductsCount": "共有 {count} 個產品使用 {name}", - "@stockIngredientConnectedProductsCount": { - "description": "在編輯成分時,會提示有幾個產品正在使用,並允許點擊導進產品頁", + "cashierChangerErrorInvalidHead": "{count} 個 {unit} 元沒辦法換", + "@cashierChangerErrorInvalidHead": { "placeholders": { "count": { - "description": "產品數量", "type": "int" }, - "name": { - "description": "成分名稱", + "unit": { "type": "String" } } }, - "stockIngredientAddTutorialTitle": "新增成份", - "stockIngredientAddTutorialMessage": "成份可以幫助我們確認相關產品的庫存。\n你可以在「產品」中設定成分,並在這裡設定庫存。", - "stockIngredientNameLabel": "成分名稱", - "stockIngredientNameHint": "例如:起司", - "stockIngredientNameRepeatError": "成分名稱重複", - "stockIngredientAmountLabel": "現有庫存", - "stockIngredientTotalAmountLabel": "庫存最大值", - "stockIngredientTotalAmountHelper": "填空則重設。\n設定這個值可以幫助你一眼看出用了多少成分。\n不填寫則每次「增加庫存」會自動設定這值", - "stockReplenishmentTitle": "採購列表", - "stockReplenishmentSubtitle": "會影響 {count} 項成分", - "stockReplenishmentButton": "採購", - "stockReplenishmentTutorialTitle": "成份採購", - "stockReplenishmentTutorialMessage": "透過採購,你不再需要一個一個去設定庫存。' '馬上設定採購,一次調整多個成份吧!", - "@stockReplenishmentSubtitle": { - "description": "採購列表中各個項目會顯示的子標題", + "cashierChangerErrorInvalidBody": "{count} 個 {unit} 元", + "@cashierChangerErrorInvalidBody": { + "description": "Concatenated multiple lines after `invalidHead` to form a complete sentence.", "placeholders": { "count": { - "description": "會影響的成分數量", "type": "int" + }, + "unit": { + "type": "String" } } }, - "stockReplenishmentApplyConfirmTitle": "套用採購?", - "@stockReplenishmentApplyConfirmTitle": { - "description": "套用採購時的確認標題" - }, - "stockReplenishmentApplyConfirmContent": "將會影響以下的成分:", - "@stockReplenishmentApplyConfirmContent": { - "description": "套用採購時的確認內文第一行" - }, - "stockReplenishmentIngredientListTitle": "點選以設定不同成分欲採購的量", - "@stockReplenishmentIngredientListTitle": { - "description": "設定採購時成分列表的標題" - }, - "stockReplenishmentCreate": "新增採購種類", - "stockReplenishmentNameLabel": "採購名稱", - "stockReplenishmentNameHint": "例如:Costco 採購", - "stockReplenishmentNameRepeatError": "採購名稱重複", - "stockReplenishmentIngredientAmountHint": "設定增加/減少的量", - "quantityTitle": "份量", - "quantityMetaProportion": "預設比例:{proportion}", - "@quantityMetaProportion": { - "description": "份量列表中各個項目的子標題中,說明預設比例的文字", - "placeholders": { - "proportion": { - "description": "預設會套用的比例", - "type": "num", - "format": "decimalPattern" - } - } - }, - "quantityDialogDeletionContent": "{count, plural, =0{目前無任何產品成分有本份量\n\n} other{將會一同刪除掉 {count} 個產品成分的份量\n\n}}", - "@quantityDialogDeletionContent": { - "description": "刪除份量時會提示多少個產品成分受影響", + "cashierChangerFavoriteTab": "常用", + "cashierChangerFavoriteHint": "選完後請點選「套用」來使用該組合", + "cashierChangerFavoriteEmptyBody": "這裡可以幫助你快速轉換不同幣值", + "cashierChangerFavoriteItemFrom": "用 {count} 個 {unit} 元換", + "@cashierChangerFavoriteItemFrom": { "placeholders": { "count": { - "description": "產品成分數量", "type": "int" + }, + "unit": { + "type": "String" } } }, - "quantityCreate": "新增份量", - "quantityNameLabel": "份量名稱", - "quantityNameHint": "例如:少量或多量", - "quantityNameRepeatError": "份量名稱重複", - "quantityProportionLabel": "預設比例", - "quantityProportionHelper": "當產品成分使用此份量時,預設替該成分增加的比例。\n例如:此份量為「多量」預設份量為「1.5」,\n今有一產品「起司漢堡」的成分「起司」,每份漢堡會使用「2」單位的起司,\n當增加此份量時,則會自動替「起司」設定為「3」(1.5 * 2)的份量。\n若設為「1」則無任何影響。\n若設為「0」則代表將不會使用此成分", - "featureRequestTitle": "建議", - "settingTitle": "其他設定", - "settingThemeTitle": "調色盤", - "@settingThemeTitle": { - "description": "設定頁面中主題的標題" - }, - "settingThemeTypes": "{type, select, dark{暗色模式} light{日光模式} system{跟隨系統} other{UNKNOWN}}", - "@settingThemeTypes": { - "description": "應用程式 theme 的類型", + "cashierChangerFavoriteItemTo": "{count} 個 {unit} 元", + "@cashierChangerFavoriteItemTo": { "placeholders": { - "type": { - "type": "String", - "description": "dark, light, system", - "example": "system" + "count": { + "type": "int" + }, + "unit": { + "type": "String" } } }, - "settingLanguageTitle": "語言", - "@settingLanguageTitle": { - "description": "設定頁面中語言的標題" - }, - "settingCheckoutWarningTitle": "收銀機提示", - "@settingCheckoutWarningTitle": { - "description": "設定頁面中是否顯示收銀機的提示的標題" - }, - "settingCheckoutWarningTypes": "{type, select, showAll{全部顯示} onlyNotEnough{僅不夠時顯示} hideAll{全部隱藏} other{UNKNOWN}}", - "@settingCheckoutWarningTypes": { - "description": "是否顯示收銀機的提示", - "placeholders": { - "type": { - "type": "String", - "description": "showAll, onlyNotEnough, hideAll", - "example": "showAll" + "cashierChangerCustomTab": "自訂", + "cashierChangerCustomAddBtn": "新增常用", + "cashierChangerCustomCountLabel": "數量", + "cashierChangerCustomUnitLabel": "幣值", + "cashierChangerCustomUnitAddBtn": "新增幣種", + "cashierChangerCustomDividerFrom": "從收銀機中拿出", + "cashierChangerCustomDividerTo": "換", + "cashierSurplusTitle": "結餘", + "cashierSurplusButton": "結餘", + "cashierSurplusTutorialTitle": "每日結餘", + "cashierSurplusTutorialContent": "結餘可以幫助我們在每天打烊時,\n計算現有金額和預設金額的差異。", + "cashierSurplusErrorEmptyDefault": "尚未設定預設狀態", + "cashierSurplusTableHint": "若你確認收銀機的金錢都沒問題之後就可以完成結餘囉!", + "cashierSurplusColumnName": "{name, select, unit{單位} currentCount{現有} diffCount{差異} defaultCount{預設} other{UNKNOWN}}", + "cashierSurplusCounterLabel": "幣值{unit}的數量", + "@cashierSurplusCounterLabel": { + "description": "Allow users to customize currency when surplus.", + "placeholders": { + "unit": { + "type": "String" } } }, - "settingOrderOutlookTitle": "點餐的外觀", - "@settingOrderOutlookTitle": { - "description": "設定頁面中點餐外觀的標題" + "cashierSurplusCounterShortLabel": "數量", + "@cashierSurplusCounterShortLabel": { + "description": "This is for display in error messages, e.g., \"Quantity cannot be 0\"." }, - "settingOrderOutlookTypes": "{type, select, slidingPanel{酷炫面板} singleView{經典模式} other{UNKNOWN}}", - "@settingOrderOutlookTypes": { - "description": "點餐時的外觀類型", + "cashierSurplusCurrentTotalLabel": "現有總額", + "cashierSurplusCurrentTotalHelper": "現在收銀機應該要有的總額。\n若你發現現金和這值對不上,想一想今天有沒有用收銀機的錢買東西?", + "cashierSurplusDiffTotalLabel": "差額", + "cashierSurplusDiffTotalHelper": "和收銀機最一開始的總額的差額。\n這可以快速幫你了解今天收銀機多了多少錢唷。", + "orderTitle": "點餐", + "orderBtn": "點餐", + "orderSnackbarCashierNotEnough": "收銀機錢不夠找囉!", + "orderSnackbarCashierUsingSmallMoney": "收銀機使用小錢去找零", + "orderSnackbarCashierUsingSmallMoneyHelper": "找錢給顧客時,收銀機無法使用最適合的錢,就會顯示這個訊息。\n\n例如,售價「65」,消費者支付「100」,此時應找「35」\n如果收銀機只有兩個十元,且有三個以上的五元,就會顯示本訊息。\n\n怎麼避免本提示:\n• 到換錢頁面把各幣值補足。\n• 到[設定頁]({link})關閉收銀機的相關提示。", + "@orderSnackbarCashierUsingSmallMoneyHelper": { "placeholders": { - "type": { - "type": "String", - "description": "slidingPanel, singleView", - "example": "slidingPanel" + "link": { + "type": "String" } } }, - "settingOrderAwakeningTitle": "點餐時不關閉螢幕", - "@settingOrderAwakeningTitle": { - "description": "設定頁面中是否啟動在點餐過程中,即使閒置仍不關閉螢幕這項功能" + "orderActionCheckout": "結帳", + "@orderActionCheckout": { + "description": "Proceed to the next step after confirming the items in your cart" }, - "orderAttributeTitle": "顧客設定", - "orderAttributeHint": "顧客設定可以幫助我們統計哪些人來消費,例如:\n20-30歲、外帶、上班族。", - "orderAttributeCreate": "新增顧客設定", - "orderAttributeUpdate": "編輯顧客設定", - "orderAttributeReorder": "排序設定順序", - "orderAttributeMetaMode": "種類:{mode}", - "@orderAttributeMetaMode": { - "description": "顧客設定列表的設定資訊之一", + "orderActionExchange": "換錢", + "orderActionStash": "暫存本次點餐", + "orderActionReview": "訂單記錄", + "orderLoaderMetaTotalRevenue": "總營收:{revenue}", + "@orderLoaderMetaTotalRevenue": { + "description": "Total revenue from orders in the order list", "placeholders": { - "mode": { + "revenue": { "type": "String" } } }, - "orderAttributeMetaNoDefault": "無", - "orderAttributeMetaDefault": "預設:{name}", - "@orderAttributeMetaDefault": { - "description": "顧客設定列表的設定資訊之一", + "orderLoaderMetaTotalCost": "總成本:{cost}", + "@orderLoaderMetaTotalCost": { + "description": "Total cost from orders in the order list", "placeholders": { - "name": { + "cost": { "type": "String" } } }, - "orderAttributeModeNames": "{mode, select, statOnly{一般} changePrice{變價} changeDiscount{折扣} other{UNKNOWN}}", - "@orderAttributeModeNames": { - "description": "顧客設定種類名稱", - "placeholders": { - "mode": {} - } - }, - "orderAttributeModeDescriptions": "{mode, select, statOnly{一般的設定,選取時並不會影響點單價格。} changePrice{選取設定時,可能會影響價格。例如:外送 + 30塊錢、環保杯 - 5塊錢。} changeDiscount{選取設定時,會根據折扣影響總價。例如:內用 + 10% 服務費、親友價 - 10%。} other{UNKNOWN}}", - "@orderAttributeModeDescriptions": { - "description": "顧客設定種類名稱", + "orderLoaderMetaTotalCount": "總數:{count}", + "@orderLoaderMetaTotalCount": { + "description": "Total number of orders in the order list", "placeholders": { - "mode": {} + "count": { + "type": "int", + "format": "compact" + } } }, - "orderAttributeNameLabel": "顧客設定名稱", - "orderAttributeNameHint": "例如:顧客年齡", - "orderAttributeNameRepeatError": "名稱不能重複", - "orderAttributeModeTitle": "顧客設定種類", - "orderAttributeOptionCreate": "新增選項", - "orderAttributeOptionIsDefault": "預設", - "orderAttributeOptionReorder": "排序選項順序", - "orderAttributeOptionCreateTitle": "新增{name}的選項", - "@orderAttributeOptionCreateTitle": { - "description": "讓標題更清楚", + "orderLoaderEmpty": "查無點餐紀錄", + "orderCatalogListEmpty": "尚未設定產品種類", + "orderProductListTutorialTitle": "開始點餐!", + "orderProductListTutorialContent": "透過圖片點餐更方便!\n可以至「設定」>「[每行顯示幾個產品]({link})」調整\n讓這裡僅使用文字點餐。", + "@orderProductListTutorialContent": { "placeholders": { - "name": { + "link": { "type": "String" } } }, - "orderAttributeOptionNameLabel": "選項名稱", - "orderAttributeOptionNameHelper": "以年齡為例,可能的選項有:\n- 20 歲以下\n- 20 到 30 歲", - "orderAttributeOptionNameRepeatError": "名稱不能重複", - "orderAttributeOptionsModeHelper": "{mode, select, statOnly{因為本設定為「一般」故無須設定「折價」或「變價」} changePrice{訂單時選擇此項會套用此變價} changeDiscount{訂單時選擇此項會套用此折價} other{UNKNOWN}}", - "@orderAttributeOptionsModeHelper": { - "description": "模式的說明", - "placeholders": { - "mode": {} - } - }, - "orderAttributeOptionsModeHint": "{mode, select, statOnly{} changePrice{例如:-30 代表減少三十塊} changeDiscount{例如:80 代表「八折」} other{UNKNOWN}}", - "@orderAttributeOptionsModeHint": { - "description": "模式的提示", - "placeholders": { - "mode": {} - } - }, - "orderAttributeOptionSetToDefault": "設為預設", - "orderAttributeOptionConfirmChangeDefaultTitle": "覆蓋選項預設?", - "orderAttributeOptionConfirmChangeDefaultContent": "這麼做會讓「{name}」變成非預設值", - "@orderAttributeOptionConfirmChangeDefaultContent": { - "description": "為了確保使用者知道原先的預設值是什麼而做的提示", - "placeholders": { - "name": { + "orderCartActionBulkify": "批量操作", + "orderCartActionToggle": "反選", + "orderCartActionSelectAll": "全選", + "orderCartActionDiscount": "打折", + "orderCartActionDiscountLabel": "折扣", + "orderCartActionDiscountHint": "例如:50,代表打五折(半價)", + "orderCartActionDiscountHelper": "這裡的數字代表「折」,即,85 代表 85 折,總價乘 0.85。若需要準確的價錢請用「變價」。", + "orderCartActionDiscountSuffix": "折", + "orderCartActionChangePrice": "變價", + "orderCartActionChangePriceLabel": "價錢", + "orderCartActionChangePriceHint": "每項產品的價錢", + "orderCartActionChangePricePrefix": "", + "orderCartActionChangePriceSuffix": "元", + "orderCartActionChangeCount": "變更數量", + "orderCartActionChangeCountLabel": "數量", + "orderCartActionChangeCountHint": "產品數量", + "orderCartActionChangeCountSuffix": "個", + "orderCartActionFree": "招待", + "orderCartActionDelete": "刪除", + "orderCartSnapshotTutorialTitle": "購物車", + "orderCartSnapshotTutorialContent": "為了讓點選產品可以更方便,\n我們把點餐後的產品設定至於此面板。\n如果需要一次顯示所有訊息的排版(適合大螢幕),\n可以至「設定」>「[點餐的外觀]({link})」調整。", + "@orderCartSnapshotTutorialContent": { + "placeholders": { + "link": { "type": "String" } } }, - "orderMetaTotalPrice": "總價:{price}", - "@orderMetaTotalPrice": { - "description": "點單的總價", + "orderCartSnapshotEmpty": "尚未點餐", + "orderCartMetaTotalPrice": "總價:{price}", + "@orderCartMetaTotalPrice": { + "description": "Total price of items in the cart", "placeholders": { "price": { - "type": "num", - "format": "compactCurrency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "type": "String" } } }, - "orderMetaTotalCount": "總數:{count}", - "@orderMetaTotalCount": { - "description": "點單的總數", + "orderCartMetaTotalCount": "總數:{count}", + "@orderCartMetaTotalCount": { + "description": "Total number of items in the cart", "placeholders": { "count": { - "type": "int" + "type": "int", + "format": "compact" } } }, - "orderActionsCheckout": "結帳", - "@orderActionsCheckout": { - "description": "確認好購物車裡的餐點後準備往下一步走" - }, - "orderActionsOpenChanger": "換錢", - "orderActionsLeaveHistoryMode": "退出改單模式", - "@orderActionsLeaveHistoryMode": { - "description": "在使用「顯示最後一次點餐」後,退出編輯該點餐的行為" - }, - "orderActionsShowLastOrder": "顯示最後一次點餐", - "@orderActionsShowLastOrder": { - "description": "顯示當日最後一次的單點" - }, - "orderActionsShowLastOrderNotFound": "找不到當日上一次的紀錄\n可以去點單紀錄查詢更久的紀錄", - "@orderActionsShowLastOrderNotFound": { - "description": "在資料庫中找不到任何一筆當日點餐紀錄" - }, - "orderActionsStash": "暫存本次點餐", - "@orderActionsStash": { - "description": "暫存購物車裡的餐點" - }, - "orderActionsLeave": "離開點餐頁面", - "@orderActionsLeave": { - "description": "退出點餐頁" - }, - "orderCartSnapshotTutorialTitle": "點餐介面", - "orderCartSnapshotTutorialMessage": "為了讓點選產品可以更方便,\n我們把點餐後的產品設定至於此面板。\n如果需要一次顯示所有訊息的排版(適合大螢幕),\n可以至「設定」>「點餐的外觀」調整。", - "orderCartSnapshotEmpty": "尚未點餐", - "orderCartToggleSelection": "反選", - "@orderCartToggleSelection": { - "description": "點選的取消點選,未點選的則點選" - }, - "orderCartActionsBtn": "使所選物", - "@orderCartActionsBtn": { - "description": "針對所選物進行操作的按鈕" - }, - "orderCartActionsDiscount": "打折", - "orderCartActionsDiscountLabel": "折扣", - "orderCartActionsDiscountHint": "例如:50,代表打五折(半價)", - "orderCartActionsDiscountHelper": "這裡的數字代表「折」,即,85 代表 85 折,總價乘 0.85。若需要準確的價錢請用「變價」。", - "orderCartActionsDiscountSuffix": "折", - "orderCartActionsChangePrice": "變價", - "orderCartActionsChangePriceLabel": "價錢", - "orderCartActionsChangePriceHint": "每項產品的價錢", - "orderCartActionsChangePriceSuffix": "元", - "orderCartActionsChangeCount": "變更數量", - "orderCartActionsChangeCountLabel": "數量", - "orderCartActionsChangeCountHint": "產品數量", - "orderCartActionsChangeCountSuffix": "個", - "orderCartActionsFree": "招待", - "orderCartActionsDelete": "刪除", - "orderCartEmptyCatalog": "尚未設定產品種類", - "orderCartItemPrice": "{price, plural, =0{免費} other{{price}元}}", - "@orderCartItemPrice": { - "description": "產品的價格", + "orderCartProductPrice": "{price, select, 0{免費} other{{price}元}}", + "@orderCartProductPrice": { + "description": "Price of the product", "placeholders": { "price": { - "type": "num", - "format": "compactCurrency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } + "type": "String" + } + } + }, + "orderCartProductIncrease": "數量加一", + "orderCartProductDefaultQuantity": "預設份量", + "orderCartProductIngredient": "{name}({quantity})", + "@orderCartProductIngredient": { + "description": "Ingredients and quantities of each item in the product list when ordering", + "placeholders": { + "name": { + "type": "String" + }, + "quantity": { + "type": "String" } } }, "orderCartIngredientStatus": "{status, select, emptyCart{請選擇產品來設定其成分} differentProducts{請選擇相同的產品來設定其成分} noNeedIngredient{這個產品沒有可以設定的成分} other{UNKNOWN}}", "@orderCartIngredientStatus": { - "description": "點餐時,若點選的產品不需要設定成分資訊,會需要提示給使用者", + "description": "Prompt to users during ordering if the selected product doesn't require ingredient settings", "placeholders": { "status": { "type": "String" @@ -751,261 +1645,215 @@ }, "orderCartQuantityNotAble": "請選擇成分來設定份量", "@orderCartQuantityNotAble": { - "description": "點餐時,須先點選要設定的成分才可以設定份量" + "description": "During ordering, select the ingredient to set the quantity" }, - "orderCartQuantityDefault": "預設值({amount})", - "@orderCartQuantityDefault": { - "description": "設定成分時,份量可以自訂或使用預設值(不使用份量)", + "orderCartQuantityLabel": "{name}({amount})", + "@orderCartQuantityLabel": { "placeholders": { + "name": { + "type": "String" + }, "amount": { - "description": "成分預設需要使用的量", "type": "num", "format": "decimalPattern" } } }, - "orderSetAttributeTitle": "顧客設定", - "@orderSetAttributeTitle": { - "description": "點完餐後的顧客設定頁面的標題" - }, - "orderCashierDefaultButton": "設為預設", - "orderCashierDefaultTutorialTitle": "收銀機預設狀態", - "orderCashierDefaultTutorialMessage": "在下面設定完收銀機各幣值的數量後,\n按這裡設定預設狀態!\n設定好的數量就會是各個幣值狀態條的「最大值」。", - "orderCashierChangeButton": "換錢", - "orderCashierChangeTutorialTitle": "收銀機換錢", - "orderCashierChangeTutorialMessage": "一百塊換成 10 個十塊之類。\n幫助快速調整收銀機狀態。", - "orderCashierSurplusButton": "結餘", - "orderCashierSurplusTutorialTitle": "每日結餘", - "orderCashierSurplusTutorialMessage": "結餘可以幫助我們在每天打烊時,\n' '計算現有金額和預設金額的差異。", - "orderCashierTitle": "收銀機", - "@orderCashierTitle": { - "description": "點完餐後的收銀機頁面的標題" - }, - "orderCashierPaidFailed": "糟糕,付額小於總價唷", - "@orderCashierPaidFailed": { - "description": "點餐後失敗的說明,目前僅有付額小於售價,未來新增需要改用 select" - }, - "orderCashierPaidNotEnough": "收銀機錢不夠找囉!", - "orderCashierPaidUsingSmallMoney": "收銀機使用小錢去找零", - "orderCashierPaidUsingSmallMoneyAction": "說明", - "orderCashierPaidUsingSmallMoneyHint": "找錢給顧客時,收銀機無法使用最適合的錢,就會顯示這個訊息。\n\n例如,售價「65」,消費者支付「100」,此時應找「35」\n如果收銀機只有兩個十元,且有三個以上的五元,就會顯示本訊息。\n\n怎麼避免本提示:\n• 到換錢頁面把各幣值補足。\n• 到設定頁關閉收銀機的相關提示。", - "orderCashierPaidConfirmLeaveHistoryMode": "確定要要變更上次的點餐紀錄嗎?", - "@orderCashierPaidConfirmLeaveHistoryMode": { - "description": "若本次點餐是更改上次的點餐紀錄,需要提示給使用者" - }, - "orderCashierPaidLabel": "付額", - "orderCashierChangeLabel": "找錢", - "orderCashierCalculatorChangeNotEnough": "必須大於訂單總價", - "orderCashierSnapshotChangeField": "找錢:{price}", - "@orderCashierSnapshotChangeField": { - "description": "快照上說明的找錢資訊", + "orderCartQuantityDefaultLabel": "預設值({amount})", + "@orderCartQuantityDefaultLabel": { + "description": "During ingredient setup, the quantity can be customized or set to default (no quantity used)", "placeholders": { - "price": { + "amount": { "type": "num", - "format": "compactCurrency", - "optionalParameters": { - "decimalDigits": 0, - "symbol": "" - } - } - } - }, - "orderProductIngredientDefaultName": "{ingredient}(預設)", - "@orderProductIngredientDefaultName": { - "description": "點單時的產品列表中各項產品的成分資訊", - "placeholders": { - "ingredient": { - "type": "String" + "format": "decimalPattern" } } }, - "orderProductIngredientName": "{ingredient}({quantity})", - "@orderProductIngredientName": { - "description": "點單時的產品列表中各項產品的成分和份量資訊", - "placeholders": { - "ingredient": { - "type": "String" - }, - "quantity": { + "orderCheckoutEmptyCart": "請先進行點單。", + "orderCheckoutActionStash": "暫存", + "orderCheckoutActionConfirm": "確認", + "orderCheckoutStashTab": "暫存", + "orderCheckoutStashEmpty": "目前無任何暫存餐點。", + "orderCheckoutStashNoProducts": "沒有任何產品", + "orderCheckoutStashActionCheckout": "結帳", + "orderCheckoutStashActionRestore": "還原", + "orderCheckoutStashDialogCalculator": "結帳計算機", + "orderCheckoutStashDialogRestoreTitle": "還原暫存訂單", + "orderCheckoutStashDialogRestoreContent": "此動作將會覆蓋掉現在購物車內的訂單。", + "orderCheckoutStashDialogDeleteName": "訂單", + "orderCheckoutAttributeTab": "顧客設定", + "orderCheckoutCashierTab": "收銀機", + "orderCheckoutCashierCalculatorLabelPaid": "付額", + "orderCheckoutCashierCalculatorLabelChange": "找錢", + "orderCheckoutCashierSnapshotLabelChange": "找錢:{change}", + "@orderCheckoutCashierSnapshotLabelChange": { + "description": "Change given by the cashier after the customer's payment", + "placeholders": { + "change": { "type": "String" } } }, - "orderObjectChange": "找錢", - "orderObjectTotalPrice": "訂單總價:{price}", - "@orderObjectTotalPrice": { - "description": "點餐後的總價資訊", + "orderCheckoutSnackbarPaidFailed": "付額小於訂單總價,無法結帳。", + "orderObjectViewEmpty": "查無點餐紀錄", + "orderObjectViewChange": "找錢", + "orderObjectViewPriceTotal": "訂單總價:{price}", + "@orderObjectViewPriceTotal": { + "description": "Total price information after ordering", "placeholders": { "price": { "type": "String" } } }, - "orderObjectProductsPrice": "產品總價", - "orderObjectAttributesPrice": "顧客設定總價", - "orderObjectProductsCost": "產品成本", - "orderObjectRevenue": "淨利", - "orderObjectPaid": "付額", - "orderObjectAttributeTitle": "顧客設定", - "orderObjectAttributeCount": "設定 {count} 項", - "@orderObjectAttributeCount": { - "description": "總共設定了幾項顧客設定", - "placeholders": { - "count": { - "description": "數量", - "type": "int" - } - } - }, - "orderObjectProductsCount": "設定 {count} 項", - "@orderObjectProductsCount": { - "description": "總共點了幾個產品", - "placeholders": { - "count": { - "description": "數量", - "type": "int" - } - } - }, - "orderObjectProductTitle": "產品資訊", - "orderObjectProductPrice": "總價", - "orderObjectProductCost": "總成本", - "orderObjectProductCount": "總數", - "orderObjectProductSinglePrice": "單價", - "orderObjectProductOriginalPrice": "折扣前單價", - "orderObjectProductCatalog": "產品種類", - "orderObjectProductIngredient": "成分", - "transitTitle": "資料轉移", - "transitDescription": "請選擇欲轉移的方式", - "transitMethod": "{label, select, googleSheet{Google 試算表} plainText{純文字} other{}}", - "@transitMethod": { - "description": "會出方法", - "placeholders": { - "label": { - "type": "String" - } - } - }, - "transitPreviewExportTitle": "預覽輸出結果", - "transitBasicTitle": "POS System 資料", - "transitOrderTitle": "訂單資料", - "transitType": "{label, select, menu{菜單} stock{庫存} quantities{份量} replenisher{補貨} orderAttr{顧客設定} order{訂單} orderSetAttr{訂單顧客設定} orderProduct{訂單產品細項} orderIngredient{訂單成分細項} other{UNKNOWN}}", - "@transitType": { - "description": "各種匯出資料的名稱,這會變成預設的表單名稱", - "placeholders": { - "label": { - "type": "String" - } - } - }, - "transitGSDescription": "Google 試算表是一個強大的小型資料庫,匯出之後可以做很多客制化的分析!", - "transitGSErrors": "{error, select, spreadsheet{無法建立試算表} spreadsheetEmpty{請先選擇試算表} sheet{無法在試算表中建立表單} sheetRepeat{表單名稱重複} sheetEmpty{請選擇至少一個表單來匯出} nonExistName{找不到試算表,是否已被刪除?} other{{error}}}", - "@transitGSErrors": { - "description": "發生錯誤時的提示訊息", + "orderObjectViewPriceProducts": "產品總價", + "orderObjectViewPriceAttributes": "顧客設定總價", + "orderObjectViewCost": "成本", + "orderObjectViewProfit": "淨利", + "orderObjectViewPaid": "付額", + "orderObjectViewDividerAttribute": "顧客設定", + "orderObjectViewDividerProduct": "產品資訊", + "orderObjectViewProductPrice": "總價", + "orderObjectViewProductCost": "總成本", + "orderObjectViewProductCount": "總數", + "orderObjectViewProductSinglePrice": "單價", + "orderObjectViewProductOriginalPrice": "折扣前單價", + "orderObjectViewProductCatalog": "產品種類", + "orderObjectViewProductIngredient": "成分", + "orderObjectViewProductDefaultQuantity": "預設", + "analysisTab": "統計", + "analysisHistoryBtn": "紀錄", + "analysisHistoryTitle": "訂單記錄", + "analysisHistoryTitleEmpty": "查無點餐紀錄", + "analysisHistoryCalendarTutorialTitle": "日曆", + "analysisHistoryCalendarTutorialContent": "上下滑動可以調整週期單位,如月或週。\n左右滑動可以調整日期起訖。", + "analysisHistoryExportBtn": "匯出", + "analysisHistoryExportTutorialTitle": "訂單資料匯出", + "analysisHistoryExportTutorialContent": "把訂單匯出到外部,讓你可以做進一步分析或保存。\n你可以到「資料轉移」去匯出多日訂單。", + "analysisHistoryOrderListMetaPrice": "售價:{price}", + "@analysisHistoryOrderListMetaPrice": { + "description": "Price of specific orders in the order list.", "placeholders": { - "error": { - "type": "String" + "price": { + "type": "num", + "format": "compactCurrency", + "optionalParameters": { + "symbol": "$" + } } } }, - "transitGSSpreadsheetLabel": "試算表", - "transitGSSheetLabel": "{item}的表單標題", - "@transitGSSheetLabel": { - "description": "表單標題的說明", + "analysisHistoryOrderListMetaPaid": "付額:{paid}", + "@analysisHistoryOrderListMetaPaid": { + "description": "Payment amount for specific orders in the order list.", "placeholders": { - "item": { - "type": "String" + "paid": { + "type": "num", + "format": "compactCurrency", + "optionalParameters": { + "symbol": "$" + } } } }, - "transitGSProgressStatus": "{status, select, addSpreadsheet{新增試算表中..} addSheets{新增表單中..} other{{status}}}", - "@transitGSProgressStatus": { - "description": "執行行為時的狀態說明", + "analysisHistoryOrderListMetaProfit": "淨利:{profit}", + "@analysisHistoryOrderListMetaProfit": { + "description": "Net profit for specific orders in the order list.", "placeholders": { - "status": { - "type": "String" + "profit": { + "type": "num", + "format": "compactCurrency", + "optionalParameters": { + "symbol": "$" + } } } }, - "transitGSUpdateModelStatus": "{model, select, menu{更新菜單中..} stock{更新庫存中..} quantities{更新份量中..} replenisher{更新補貨中..} orderAttr{更新顧客設定中..} order{匯出訂單中..} orderSetAttr{匯出顧客設定中..} orderProduct{匯出產品細項中..} orderIngredient{匯出成分細項中..} other{{model}}}", - "@transitGSUpdateModelStatus": { - "description": "更新 Model 時的狀態說明", + "analysisHistoryOrderTitle": "訂單詳情", + "analysisHistoryOrderNotFound": "找不到相關訂單", + "analysisHistoryOrderDeleteDialog": "確定要刪除 {name} 的訂單嗎?\n將不會復原收銀機和庫存資料。\n此動作無法復原。", + "@analysisHistoryOrderDeleteDialog": { "placeholders": { - "model": { + "name": { "type": "String" } } }, - "transitProductIngredientInfoTitle": "成分資訊", - "transitReplenishmentTitle": "補貨量", - "transitOrderAttributeOptionTitle": "顧客設定選項", - "transitProductIngredientInfoGSNote": "產品全部成分的資訊,格式如下:\n- 成分1,預設使用量\n + 份量a,額外使用量,額外價格,額外成本\n + 份量b,額外使用量,額外價格,額外成本\n- 成分2,預設使用量", - "transitReplenishmentGSNote": "每次補貨時特定成分的量,格式如下:\n- 成分1,補貨量\n- 成分2,補貨量", - "transitOrderAttributeOptionGSNote": "「選項值」會根據顧客設定種類不同而有不同意義,格式如下:\n- 選項1,是否為預設,選項值\n- 選項2,是否為預設,選項值", - "transitGSImportError": "{error, select, emptySpreadsheet{必須選擇試算表來匯入} emptySheet{必須選擇指定的表單來匯入} emptyData{在表單中沒找到任何值} other{{error}}}", - "@transitGSImportError": { - "description": "匯入時會有的錯誤", + "analysisGoalsTitle": "本日總結", + "analysisGoalsCountTitle": "訂單數", + "analysisGoalsCountDescription": "訂單數反映了產品對顧客的吸引力。\n它代表了市場對你產品的需求程度,能幫助你了解何種產品或時段最受歡迎。\n高訂單數可能意味著你的定價策略或行銷活動取得成功,是商業模型有效性的指標之一。\n但要注意,單純追求高訂單數可能會忽略盈利能力。", + "analysisGoalsRevenueTitle": "營收", + "analysisGoalsRevenueDescription": "營收代表總銷售額,是業務規模的指標。\n高營收可能顯示了你的產品受歡迎且銷售良好,但營收無法反映出業務的可持續性和盈利能力。\n有時候,為了提高營收,公司可能會採取降價等策略,這可能會對公司的盈利能力造成影響。", + "analysisGoalsProfitTitle": "淨利", + "analysisGoalsProfitDescription": "淨利是營業收入減去營業成本後的餘額,是公司能否持續經營的關鍵。\n盈利直接反映了營運效率和成本管理能力。\n不同於營收,盈利考慮了生意的開支,包括原料成本、人力、租金等,\n這是一個更實際的指標,能幫助你評估經營是否有效且可持續。", + "analysisGoalsCostTitle": "成本", + "analysisGoalsAchievedRate": "利潤達成\n{rate}", + "@analysisGoalsAchievedRate": { "placeholders": { - "error": { + "rate": { "type": "String" } } }, - "transitPreviewImportTitle": "預覽結果", - "transitImportColumnsCountError": "資料量不足,需要 {columns} 個欄位", - "@transitImportColumnsCountError": { - "description": "匯入時需要的資料量不足", - "placeholders": { - "columns": { - "type": "int" - } - } - }, - "transitImportColumnStatus": "{status, select, normal{(一般)} staged{(新增)} stagedIng{(新的成分)} stagedQua{(新的份量)} updated{(異動)} other{UNKNOWN}}", - "@transitImportColumnStatus": { - "description": "顯示該資料的額外狀態", + "analysisChartTitle": "圖表分析", + "analysisChartTitleCreate": "新增圖表", + "analysisChartTitleReorder": "排序圖表", + "analysisChartTutorialTitle": "圖表分析", + "analysisChartTutorialContent": "透過圖表,你可以更直觀地看到數據變化。\n現在就開始設計圖表追蹤你的銷售狀況吧!。", + "analysisChartCardEmptyData": "沒有資料", + "analysisChartCardTitleUpdate": "編輯圖表", + "analysisChartMetricName": "{name, select, revenue{營收} cost{成本} profit{淨利} count{數量} other{UNKNOWN}}", + "@analysisChartMetricName": { "placeholders": { - "status": { + "name": { "type": "String" } } }, - "analysisChartCreate": "新增圖表", - "analysisChartNameLabel": "圖表標題", - "analysisChartIgnoreEmptyLabel": "是否忽略空資料", - "analysisChartIgnoreEmptyHelper": "某商品或指標在該時段沒有資料,則不顯示。", - "analysisChartTypeLabel": "圖表類型", - "analysisChartType": "{val, select, cartesian{時序圖} circular{圓餅圖} other{UNKNOWN}}", - "@analysisChartType": { + "analysisChartTargetName": "{name, select, order{訂單} catalog{產品種類} product{產品} ingredient{成分} attribute{顧客屬性} other{UNKNOWN}}", + "@analysisChartTargetName": { "placeholders": { - "val": { + "name": { "type": "String" } } }, - "analysisChartDataPropertiesDivider": "圖表資料", - "analysisChartMetricLabel": "觀看指標", - "analysisChartMetricHelper": "根據不同目的,選擇不同指標類型。", - "analysisChartMetric": "{val, select, price{營收} cost{成本} revenue{淨利} count{數量} other{UNKNOWN}}", - "@analysisChartMetric": { + "analysisChartRangeYesterday": "昨天", + "analysisChartRangeToday": "今天", + "analysisChartRangeLastWeek": "上週", + "analysisChartRangeThisWeek": "本週", + "analysisChartRangeLast7Days": "最近7日", + "analysisChartRangeLastMonth": "上月", + "analysisChartRangeThisMonth": "本月", + "analysisChartRangeLast30Days": "最近30日", + "analysisChartRangeTabName": "{name, select, day{日期} week{週} month{月} custom{自訂} other{UNKNOWN}}", + "@analysisChartRangeTabName": { "placeholders": { - "val": { + "name": { "type": "String" } } }, - "analysisChartTargetLabel": "項目種類", - "analysisChartTargetHelper": "選擇圖表中要針對哪些資訊做分析。", - "analysisChartTargetError": "請選擇一個項目種類", - "analysisChartTarget": "{val, select, order{訂單} catalog{產品種類} product{產品} ingredient{成分} attribute{顧客屬性} other{UNKNOWN}}", - "@analysisChartTarget": { + "analysisChartModalNameLabel": "圖表名稱", + "analysisChartModalNameHint": "例如:每日營收", + "analysisChartModalIgnoreEmptyLabel": "忽略空資料", + "analysisChartModalIgnoreEmptyHelper": "某商品或指標在該時段沒有資料,則不顯示。", + "analysisChartModalDivider": "資料設定", + "analysisChartModalTypeLabel": "圖表類型", + "analysisChartModalTypeName": "{name, select, cartesian{時序圖} circular{圓餅圖} other{UNKNOWN}}", + "@analysisChartModalTypeName": { "placeholders": { - "val": { + "name": { "type": "String" } } }, - "analysisChartTargetItemLabel": "項目選擇", - "analysisChartTargetItemHelper": "你想要觀察哪些項目的變化,例如區間內某商品的數量。", - "analysisChartTargetItemSelectAll": "全選" -} + "analysisChartModalMetricLabel": "觀看指標", + "analysisChartModalMetricHelper": "根據不同目的,選擇不同指標類型。", + "analysisChartModalTargetLabel": "項目種類", + "analysisChartModalTargetHelper": "選擇圖表中要針對哪些資訊做分析。", + "analysisChartModalTargetErrorEmpty": "請選擇一個項目種類", + "analysisChartModalTargetItemLabel": "項目選擇", + "analysisChartModalTargetItemHelper": "你想要觀察哪些項目的變化,例如區間內某商品的數量。", + "analysisChartModalTargetItemSelectAll": "全選" +} \ No newline at end of file diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb deleted file mode 100644 index 0967ef42..00000000 --- a/lib/l10n/app_zh_Hant.arb +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/lib/l10n/app_zh_Hant_TW.arb b/lib/l10n/app_zh_Hant_TW.arb deleted file mode 100644 index 0967ef42..00000000 --- a/lib/l10n/app_zh_Hant_TW.arb +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/lib/l10n/app_zh_TW.arb b/lib/l10n/app_zh_TW.arb deleted file mode 100644 index 0967ef42..00000000 --- a/lib/l10n/app_zh_TW.arb +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/lib/main.dart b/lib/main.dart index 360c0a7d..0699f4a5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -54,8 +54,8 @@ void main() async { await Storage.instance.initialize(); await Cache.instance.initialize(); - final settings = SettingsProvider(SettingsProvider.allSettings); - Log.allowSendEvents = SettingsProvider.of().value; + SettingsProvider.instance.initialize(); + Log.allowSendEvents = CollectEventsSetting.instance.value; await Stock().initialize(); await Quantities().initialize(); @@ -66,7 +66,7 @@ void main() async { // Last for setup ingredient and quantity await Menu().initialize(); - if (kDebugMode) { + if (isLocalTest) { await debugSetupMenu(); } @@ -74,35 +74,17 @@ void main() async { /// https://stackoverflow.com/questions/57157823/provider-vs-inheritedwidget runApp(MultiProvider( providers: [ - ChangeNotifierProvider.value( - value: settings, - ), - ChangeNotifierProvider( - create: (_) => Menu.instance, - ), - ChangeNotifierProvider( - create: (_) => Stock.instance, - ), - ChangeNotifierProvider( - create: (_) => Quantities.instance, - ), - ChangeNotifierProvider( - create: (_) => Replenisher.instance, - ), - ChangeNotifierProvider( - create: (_) => OrderAttributes.instance, - ), - ChangeNotifierProvider( - create: (_) => Seller.instance, - ), - ChangeNotifierProvider( - create: (_) => Cashier.instance, - ), - ChangeNotifierProvider( - create: (_) => Cart.instance, - ), + ChangeNotifierProvider.value(value: SettingsProvider.instance), + ChangeNotifierProvider.value(value: Menu.instance), + ChangeNotifierProvider.value(value: Stock.instance), + ChangeNotifierProvider.value(value: Quantities.instance), + ChangeNotifierProvider.value(value: Replenisher.instance), + ChangeNotifierProvider.value(value: OrderAttributes.instance), + ChangeNotifierProvider.value(value: Seller.instance), + ChangeNotifierProvider.value(value: Cashier.instance), + ChangeNotifierProvider.value(value: Cart.instance), ], - child: MyApp(settings: settings), + child: const MyApp(), )); }, (error, stack) => FirebaseCrashlytics.instance.recordError(error, stack, fatal: true), diff --git a/lib/models/analysis/chart.dart b/lib/models/analysis/chart.dart index 78a36ba1..70a4d07d 100644 --- a/lib/models/analysis/chart.dart +++ b/lib/models/analysis/chart.dart @@ -19,7 +19,7 @@ class Chart extends Model with ModelStorage, ModelOrde /// Which target to show, product, category, or ingredients OrderMetricTarget target; - /// Which metrics to show, price, cost, or revenue + /// Which metrics to show, revenue, cost, or profit List metrics; /// Target's specified items IDs. @@ -33,7 +33,7 @@ class Chart extends Model with ModelStorage, ModelOrde this.type = AnalysisChartType.cartesian, this.ignoreEmpty = false, this.target = OrderMetricTarget.order, - this.metrics = const [OrderMetricType.price], + this.metrics = const [OrderMetricType.revenue], this.targetItems = const [], }) { this.index = index; @@ -47,7 +47,7 @@ class Chart extends Model with ModelStorage, ModelOrde type: object.type ?? AnalysisChartType.cartesian, ignoreEmpty: object.ignoreEmpty ?? false, target: object.target ?? OrderMetricTarget.order, - metrics: object.metrics ?? const [OrderMetricType.price], + metrics: object.metrics ?? const [OrderMetricType.revenue], targetItems: object.targetItems ?? const [], ); } @@ -101,7 +101,7 @@ class Chart extends Model with ModelStorage, ModelOrde } } - Future> _loadCartesian(DateTimeRange range) { + Future> _loadCartesian(DateTimeRange range) { return target == OrderMetricTarget.order ? Seller.instance.getMetricsInPeriod( range.start, diff --git a/lib/models/menu/product_ingredient.dart b/lib/models/menu/product_ingredient.dart index b0e4181b..c01b4878 100644 --- a/lib/models/menu/product_ingredient.dart +++ b/lib/models/menu/product_ingredient.dart @@ -106,7 +106,9 @@ class ProductIngredient extends Model @override String get statusName { - // 當產品是新的,代表產品成份一定也是新的,這樣就只需要輸出是否為「新的庫存成分」 + // When the product is new, it means that the product + // ingredient must also be new, so only need to output + // whether it is a "new inventory ingredient" if (product.status == ModelStatus.staged) { return ingredient.status == ModelStatus.staged ? 'stagedIng' : 'normal'; } diff --git a/lib/models/menu/product_quantity.dart b/lib/models/menu/product_quantity.dart index ce3ce48f..8662669c 100644 --- a/lib/models/menu/product_quantity.dart +++ b/lib/models/menu/product_quantity.dart @@ -95,7 +95,9 @@ class ProductQuantity extends Model @override String get statusName { - // 當成分是新的,代表產品份量一定也是新的,這樣就只需要輸出是否為「新的份量種類」 + // When the ingredient is new, the product quantity + // must also be new, so only need to output whether + // it is a "new quantity type" if (ingredient.status == ModelStatus.staged) { return quantity.status == ModelStatus.staged ? 'stagedQua' : 'normal'; } diff --git a/lib/models/model.dart b/lib/models/model.dart index 220db2db..a123d21e 100644 --- a/lib/models/model.dart +++ b/lib/models/model.dart @@ -19,7 +19,9 @@ abstract class Model extends ChangeNotifier { String name; - /// 是否是暫存的資料,並未存進檔案系統中,僅存在於記憶體中。 + /// Whether the data is saved in the file system, only exists in memory. + /// + /// This is used to import/export data. ModelStatus status; Model({ diff --git a/lib/models/objects/order_object.dart b/lib/models/objects/order_object.dart index ed2c2528..32ed9bb3 100644 --- a/lib/models/objects/order_object.dart +++ b/lib/models/objects/order_object.dart @@ -4,8 +4,8 @@ import 'dart:math'; import 'package:possystem/helpers/util.dart'; import 'package:possystem/models/menu/product_ingredient.dart'; import 'package:possystem/models/objects/order_attribute_object.dart'; -import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/order/cart_product.dart'; +import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/seller.dart'; @@ -51,8 +51,8 @@ class OrderObject extends _Object { required this.createdAt, }); - /// Revenue, [price] minus [cost]. - num get revenue => price - cost; + /// Profit, [price] minus [cost]. + num get profit => price - cost; /// Price that cause by order attributes, [price] minus [productsPrice]. num get attributesPrice => price - productsPrice; @@ -101,7 +101,7 @@ class OrderObject extends _Object { 'paid': paid, 'price': price, 'cost': cost, - 'revenue': revenue, + 'revenue': profit, 'productsPrice': productsPrice, 'productsCount': productsCount, 'attributesPrice': attributesPrice, diff --git a/lib/models/repository.dart b/lib/models/repository.dart index fa726d9c..83346f28 100644 --- a/lib/models/repository.dart +++ b/lib/models/repository.dart @@ -21,7 +21,9 @@ mixin Repository on ChangeNotifier { Iterable get stagedItems => _stagedItems.values; - /// 捨棄那些暫存的資料 + /// Abort the data not saved in file system + /// + /// This is used to import/export data. void abortStaged() { _stagedItems.clear(); } @@ -48,7 +50,12 @@ mixin Repository on ChangeNotifier { _stagedItems[item.id] = item; } - /// 提交那些暫存的資料 + /// Commit the staged data to the file system + /// + /// [save] whether to save the data to the file system + /// [reset] whether to clear the original data and use staged data to replace it + /// + /// It is use for import/export data Future commitStaged({bool save = true, bool reset = true}) async { if (reset) { _items.clear(); diff --git a/lib/models/repository/cashier.dart b/lib/models/repository/cashier.dart index 1e0a14d1..42fe8135 100644 --- a/lib/models/repository/cashier.dart +++ b/lib/models/repository/cashier.dart @@ -178,9 +178,9 @@ class Cashier extends ChangeNotifier { /// Customer [given] money for the [price] and update the cashier /// - /// Example: - /// 給一百元來支付六十五元的商品,並更新收銀機的錢 - /// 以此為例則是增加一張百元鈔,減少三十五塊的現金來找錢 + /// For example: + /// given 100 for 65, then the cashier will + /// have add 1 100-dollar bill but minus 3 10-dollar and 1 5-dollar bill Future paid(num given, num price) async { final amounts = {}; @@ -224,7 +224,7 @@ class Cashier extends ChangeNotifier { return CashierUpdateStatus.notEnough; } - /// When [Currency] changed, it must be fired + /// When [CurrencySetting] changed, it must be fired Future reset() async { _recordName = CurrencySetting.instance.recordName; final record = await Storage.instance.get(Stores.cashier, _recordName); @@ -375,15 +375,19 @@ class CashierDiffItem { num get unit => currentData.unit; } -/// 當收銀機在更新錢的時,有任何狀況會回這個 +/// When the cashier is updating the money will return this enum CashierUpdateStatus { - /// 當收銀機沒有足夠的錢去找錢,會回應這個 + /// When the cashier does not have enough money to change notEnough, - /// 當收銀機嘗試用較小的額度去換錢時,會回應這個 + /// When the cashier is using smaller units to change /// - /// 例如,找錢 35 時,只有兩個 10 元,於是就使用 3 個 5元。 - /// 若完全不夠換會使用 [CashierUpdateStatus.notEnough] + /// For example, change 35 with 2 10-dollar bills and 3 5-dollar bills + /// + /// If the cashier does not have enough bills to change, + /// it will return [CashierUpdateStatus.usingSmall] usingSmall, + + /// When the cashier has enough money to change ok } diff --git a/lib/models/repository/seller.dart b/lib/models/repository/seller.dart index 78a1596d..39ae2c2a 100644 --- a/lib/models/repository/seller.dart +++ b/lib/models/repository/seller.dart @@ -37,9 +37,9 @@ class Seller extends ChangeNotifier { orderTable, columns: [ 'COUNT(*) count', - 'SUM(price) price', + 'SUM(price) revenue', 'SUM(cost) cost', - 'SUM(revenue) revenue', + 'SUM(revenue) profit', ], where: 'createdAt BETWEEN ? AND ?', whereArgs: [begin, finish], @@ -50,21 +50,18 @@ class Seller extends ChangeNotifier { queries.addAll([ Database.instance.query( productTable, - // so far so good, add new if we need it columns: ['COUNT(*) count'], where: 'createdAt BETWEEN ? AND ?', whereArgs: [begin, finish], ), Database.instance.query( ingredientTable, - // so far so good, add new if we need it columns: ['COUNT(*) count'], where: 'createdAt BETWEEN ? AND ?', whereArgs: [begin, finish], ), Database.instance.query( attributeTable, - // so far so good, add new if we need it columns: ['COUNT(*) count'], where: 'createdAt BETWEEN ? AND ?', whereArgs: [begin, finish], @@ -99,9 +96,9 @@ class Seller extends ChangeNotifier { /// Get the metric of orders grouped by the day. /// /// - [types] is the metrics type to calculate. - /// - [period] is the time interval to group by. + /// - [interval] is the time interval to group by. /// - [ignoreEmpty] whether to ignore the empty day. - Future> getMetricsInPeriod( + Future> getMetricsInPeriod( DateTime start, DateTime end, { List types = const [OrderMetricType.count], @@ -130,10 +127,10 @@ class Seller extends ChangeNotifier { escapeTable: false, ); - final result = [ + final result = [ for (final row in rows) if (row['day'] != null) - OrderDataPerDay( + OrderSummary( at: Util.fromUTC(begin + (row['day'] as int) * interval.seconds), values: row.cast(), ), @@ -148,7 +145,7 @@ class Seller extends ChangeNotifier { /// - [interval] is the time interval to group by. /// - [selection] is the specific items to group by. /// - [ignoreEmpty] whether to ignore the empty day. - Future> getItemMetricsInPeriod( + Future> getItemMetricsInPeriod( DateTime start, DateTime end, { required OrderMetricType type, @@ -191,7 +188,7 @@ class Seller extends ChangeNotifier { .where((e) => e['day'] != null) .groupListsBy((row) => row['day']) .values - .map((e) => OrderDataPerDay( + .map((e) => OrderSummary( at: Util.fromUTC(begin + (e.first['day'] as int) * interval.seconds), values: { for (final row in e) row['name'] as String: row['value'] as num, @@ -420,17 +417,17 @@ class Seller extends ChangeNotifier { return items.length; } - List _fulfillPeriodData( + List _fulfillPeriodData( DateTime start, DateTime end, Duration interval, - List data, + List data, ) { var i = 0; - return [ + return [ for (var v = start; v.isBefore(end); v = v.add(interval)) // `result is not enough` or `result has not contains the day` - i >= data.length || data[i].at != v ? OrderDataPerDay(at: v) : data[i++], + i >= data.length || data[i].at != v ? OrderSummary(at: v) : data[i++], ]; } } @@ -440,14 +437,14 @@ class OrderMetrics { /// Total count of orders in specific day range. final int count; - /// Total price of orders in specific day range. - final num price; + /// Total revenue of orders in specific day range. + final num revenue; /// Total cost of orders in specific day range. final num cost; - /// Total revenue of orders in specific day range. - final num revenue; + /// Total (net) profit of orders in specific day range. + final num profit; /// How many rows in the table of products. final int? productCount; @@ -463,9 +460,9 @@ class OrderMetrics { /// see detailed in [Seller.getMetrics]. const OrderMetrics._({ required this.cost, - required this.price, - required this.count, required this.revenue, + required this.count, + required this.profit, this.productCount, this.ingredientCount, this.attrCount, @@ -480,9 +477,9 @@ class OrderMetrics { }) { return OrderMetrics._( count: map['count'] as int? ?? 0, - price: map['price'] as num? ?? 0, - cost: map['cost'] as num? ?? 0, revenue: map['revenue'] as num? ?? 0, + cost: map['cost'] as num? ?? 0, + profit: map['profit'] as num? ?? 0, productCount: productCount, ingredientCount: ingredientCount, attrCount: attrCount, @@ -490,12 +487,12 @@ class OrderMetrics { } } -class OrderDataPerDay { +class OrderSummary { final DateTime at; final Map values; - const OrderDataPerDay({ + const OrderSummary({ required this.at, this.values = const {}, }); @@ -506,11 +503,11 @@ class OrderDataPerDay { int get count => value('count').toInt(); - num get price => value('price'); + num get revenue => value('revenue'); num get cost => value('cost'); - num get revenue => value('revenue'); + num get profit => value('profit'); } class OrderMetricPerItem { @@ -518,7 +515,7 @@ class OrderMetricPerItem { final num value; final double percent; - OrderMetricPerItem(this.name, this.value, num total) : percent = total == 0 ? 0 : (value / total * 100).toDouble(); + OrderMetricPerItem(this.name, this.value, num total) : percent = total == 0 ? 0 : value / total; } enum OrderMetricUnit { @@ -532,9 +529,10 @@ enum OrderMetricUnit { } enum OrderMetricType { - price('SUM', 'price', 'singlePrice * count', OrderMetricUnit.money), + revenue('SUM', 'price', 'singlePrice * count', OrderMetricUnit.money), cost('SUM', 'cost', 'singleCost * count', OrderMetricUnit.money), - revenue('SUM', 'revenue', '(singlePrice - singleCost) * count', OrderMetricUnit.money), + // profit = price - cost, we use `revenue` for historical reason. + profit('SUM', 'revenue', '(singlePrice - singleCost) * count', OrderMetricUnit.money), count('COUNT', 'price', '*', OrderMetricUnit.count); /// The method to calculate the value in DB. diff --git a/lib/my_app.dart b/lib/my_app.dart index 8745ab0b..974465a0 100644 --- a/lib/my_app.dart +++ b/lib/my_app.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; import 'constants/app_themes.dart'; import 'routes.dart'; @@ -33,12 +35,7 @@ class MyApp extends StatelessWidget { ], ); - final SettingsProvider settings; - - const MyApp({ - super.key, - required this.settings, - }); + const MyApp({super.key}); // This widget is the root of your application. @override @@ -48,7 +45,7 @@ class MyApp extends StatelessWidget { // The AnimatedBuilder Widget listens to the SettingsController for changes. // Whenever the user updates their settings, the MaterialApp is rebuilt. return AnimatedBuilder( - animation: settings, + animation: SettingsProvider.instance, builder: (_, __) { return MaterialApp.router( routerConfig: router, @@ -58,6 +55,9 @@ class MyApp extends StatelessWidget { final localizations = AppLocalizations.of(context)!; S = localizations; + Intl.systemLocale = S.localeName; + Intl.defaultLocale = S.localeName; + initializeDateFormatting(S.localeName); FlutterNativeSplash.remove(); @@ -68,7 +68,7 @@ class MyApp extends StatelessWidget { // Provide the generated AppLocalizations to the MaterialApp. This // allows descendant Widgets to display the correct translations // depending on the user's locale. - locale: settings.getSetting().value, + locale: LanguageSetting.instance.value.locale, supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -77,7 +77,7 @@ class MyApp extends StatelessWidget { // SettingsController to display the correct theme. theme: AppThemes.lightTheme, darkTheme: AppThemes.darkTheme, - themeMode: settings.getSetting().value, + themeMode: ThemeSetting.instance.value, ); }, ); diff --git a/lib/routes.dart b/lib/routes.dart index 36970d7f..a39bf6ac 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:possystem/models/analysis/analysis.dart'; import 'package:possystem/ui/analysis/history_page.dart'; -import 'package:possystem/ui/analysis/widgets/chart_order_modal.dart'; +import 'package:possystem/ui/analysis/widgets/chart_modal.dart'; import 'package:possystem/ui/analysis/widgets/chart_reorder.dart'; import 'package:possystem/ui/stock/widgets/replenishment_apply.dart'; @@ -28,7 +28,7 @@ import 'ui/menu/widgets/product_ingredient_modal.dart'; import 'ui/menu/widgets/product_modal.dart'; import 'ui/menu/widgets/product_quantity_modal.dart'; import 'ui/menu/widgets/product_reorder.dart'; -import 'ui/order/cashier/order_details_page.dart'; +import 'ui/order/order_checkout_page.dart'; import 'ui/order/order_page.dart'; import 'ui/order_attr/order_attribute_page.dart'; import 'ui/order_attr/widgets/order_attribute_modal.dart'; @@ -78,6 +78,8 @@ String? Function(BuildContext, GoRouterState) _redirectIfMissed({ class Routes { static const base = '/pos'; + static getRoute(String path) => 'https://evan361425.github.io$base/$path'; + static final home = GoRoute( name: 'home', path: base, @@ -119,12 +121,17 @@ class Routes { ), ), GoRoute( - name: chartOrderModal, + name: chartNew, + path: 'chart/new', + builder: (ctx, state) => const ChartModal(), + ), + GoRoute( + name: chartModal, path: 'chart/o/:id/modal', builder: (ctx, state) { - final id = state.pathParameters['id'] ?? '0'; + final id = state.pathParameters['id']!; final chart = Analysis.instance.getItem(id); - return ChartOrderModal(chart: chart); + return ChartModal(chart: chart); }, ), GoRoute( @@ -157,15 +164,15 @@ class Routes { TransitMethod.plainText, ); final type = _findEnum( - TransitType.values, + TransitCatalog.values, state.pathParameters['type'], - TransitType.basic, + TransitCatalog.model, ); final range = _parseRange(state.uri.queryParameters['range']); return TransitStation( method: method, - type: type, + catalog: type, range: range, ); }, @@ -185,7 +192,18 @@ class Routes { GoRoute( name: features, path: 'features', - builder: (ctx, state) => const FeaturesPage(), + builder: (ctx, state) => FeaturesPage(focus: state.uri.queryParameters['f']), + routes: [ + GoRoute( + name: featuresChoices, + path: ':feature', + builder: (ctx, state) { + final f = state.pathParameters['feature']; + final feature = Feature.values.firstWhereOrNull((e) => e.name == f) ?? Feature.theme; + return ItemListScaffold(feature: feature); + }, + ), + ], ), ]; @@ -445,7 +463,8 @@ class Routes { static const order = '/order'; static const orderDetails = '/order/details'; - static const chartOrderModal = '/chart/order/modal'; + static const chartNew = '/chart/order/new'; + static const chartModal = '/chart/order/modal'; static const chartReorder = '/chart/reorder'; static const transit = '/transit'; @@ -454,4 +473,5 @@ class Routes { static const featureRequest = '/feature_request'; static const imageGallery = '/image_gallery'; static const features = '/features'; + static const featuresChoices = '/features/choices'; } diff --git a/lib/services/image_dumper.dart b/lib/services/image_dumper.dart index a6880438..d3a73214 100644 --- a/lib/services/image_dumper.dart +++ b/lib/services/image_dumper.dart @@ -2,6 +2,7 @@ import 'package:image/image.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:image_picker/image_picker.dart' hide XFile; import 'package:possystem/models/xfile.dart'; +import 'package:possystem/translator.dart'; class ImageDumper { static ImageDumper instance = const ImageDumper._(); @@ -22,7 +23,7 @@ class ImageDumper { maxHeight: 512, maxWidth: 512, aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1), - uiSettings: [AndroidUiSettings(toolbarTitle: '裁切')], + uiSettings: [AndroidUiSettings(toolbarTitle: S.imageBtnCrop)], ); return result == null ? null : XFile(result.path); diff --git a/lib/settings/checkout_warning.dart b/lib/settings/checkout_warning.dart index 522eb846..f3ea4477 100644 --- a/lib/settings/checkout_warning.dart +++ b/lib/settings/checkout_warning.dart @@ -2,13 +2,21 @@ import 'package:possystem/models/repository/cart.dart'; import 'package:possystem/settings/setting.dart'; class CheckoutWarningSetting extends Setting { + static final instance = CheckoutWarningSetting._(); + + static const defaultValue = CheckoutWarningTypes.showAll; + + CheckoutWarningSetting._() { + value = defaultValue; + } + // history reason for calling cashier @override String get key => 'feat.cashierWarning'; @override void initialize() { - value = CheckoutWarningTypes.values[service.get(key) ?? 0]; + value = CheckoutWarningTypes.values[service.get(key) ?? defaultValue.index]; } @override @@ -30,7 +38,14 @@ class CheckoutWarningSetting extends Setting { } enum CheckoutWarningTypes { + /// show all warning + /// + /// when using small amount of money, it will show warning showAll, + + /// only show when cashier has not enough money onlyNotEnough, + + /// hide all warning hideAll, } diff --git a/lib/settings/collect_events_setting.dart b/lib/settings/collect_events_setting.dart index 0bfa3d9d..c8241f84 100644 --- a/lib/settings/collect_events_setting.dart +++ b/lib/settings/collect_events_setting.dart @@ -2,12 +2,20 @@ import 'package:possystem/helpers/logger.dart'; import 'package:possystem/settings/setting.dart'; class CollectEventsSetting extends Setting { + static final instance = CollectEventsSetting._(); + + static const defaultValue = true; + + CollectEventsSetting._() { + value = defaultValue; + } + @override String get key => 'feat.collectEvents'; @override void initialize() { - value = service.get(key) ?? true; + value = service.get(key) ?? defaultValue; } @override diff --git a/lib/settings/currency_setting.dart b/lib/settings/currency_setting.dart index 2d153a82..57ced475 100644 --- a/lib/settings/currency_setting.dart +++ b/lib/settings/currency_setting.dart @@ -1,26 +1,31 @@ +import 'package:intl/intl.dart'; +import 'package:possystem/settings/language_setting.dart'; import 'package:possystem/settings/setting.dart'; class CurrencySetting extends Setting { - static late CurrencySetting instance; + static CurrencySetting instance = CurrencySetting._(); - static const defaultCurrency = CurrencyTypes.twd; + static const defaultValue = CurrencyTypes.twd; - static const supports = { + static const supports = >{ CurrencyTypes.twd: [1, 5, 10, 50, 100, 500, 1000], CurrencyTypes.usd: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 5, 10, 20, 50, 100], }; /// Current available unit of money - late List unitList; + List unitList = CurrencySetting.supports[CurrencyTypes.twd]!; /// Is this currency all int? - late bool isInt; + bool isInt = true; /// Index of integer in [unitList] - late int intIndex; + int intIndex = 0; - CurrencySetting() { - instance = this; + CurrencySetting._() { + value = defaultValue; + LanguageSetting.instance.addListener(() { + formatter = NumberFormat.compact(locale: LanguageSetting.instance.value.locale.toString()); + }); } @override @@ -28,6 +33,8 @@ class CurrencySetting extends Setting { String get recordName => '新台幣'; + NumberFormat formatter = NumberFormat.compact(locale: LanguageSetting.instance.value.locale.toString()); + /// Ceiling [value] to currency least value /// /// 1~4 => 5 @@ -75,7 +82,7 @@ class CurrencySetting extends Setting { @override void initialize() { - value = CurrencyTypes.values[service.get(key) ?? 0]; + value = CurrencyTypes.values[service.get(key) ?? defaultValue.index]; _setMetadata(value); } @@ -106,7 +113,21 @@ enum CurrencyTypes { extension ToCurrency on num { /// Parse value to int or double string, decided by [CurrencySetting.isInt] String toCurrency() { - return CurrencySetting.instance.isInt ? round().toString() : toString(); + return CurrencySetting.instance.formatter.format(toCurrencyNum()); + } + + String toCurrencyLong() { + if (CurrencySetting.instance.isInt) { + return round().toString(); + } + + // if it has decimal, show it, else show int + final rounded = round(); + if (this == rounded) { + return round().toString(); + } + + return toString(); } num toCurrencyNum() { diff --git a/lib/settings/language_setting.dart b/lib/settings/language_setting.dart index 76b2322c..b89682d2 100644 --- a/lib/settings/language_setting.dart +++ b/lib/settings/language_setting.dart @@ -3,19 +3,14 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:possystem/settings/setting.dart'; -class LanguageSetting extends Setting { - // Make SettingsService a private variable so it is not used directly. - static const defaultLanguage = Locale.fromSubtags( - languageCode: 'zh', - countryCode: 'TW', - ); +class LanguageSetting extends Setting { + static final instance = LanguageSetting._(); - static const supported = [ - defaultLanguage, - Locale('en'), - ]; + static const defaultValue = Language.en; - static const supportedNames = ['繁體中文', 'English']; + LanguageSetting._() { + value = defaultValue; + } @override final String key = 'language'; @@ -25,22 +20,34 @@ class LanguageSetting extends Setting { @override void initialize() { - value = parseLanguage(service.get(key)) ?? defaultLanguage; + value = parseLanguage(service.get(key)) ?? defaultValue; + notifyListeners(); } @override - Future updateRemotely(Locale data) { - return service.set(key, data.toString()); + Future updateRemotely(Language data) { + return service.set(key, data.locale.toString()); } - Locale? parseLanguage(String? value) { + Language? parseLanguage(String? value) { if (value == null || value.isEmpty) return null; final codes = value.split('_'); - return supported.firstWhere( - (e) => e.languageCode == codes[0], - orElse: () => defaultLanguage, + return Language.values.firstWhere( + (e) => e.locale.languageCode == codes[0], + orElse: () => defaultValue, ); } } + +enum Language { + zhTW(Locale('zh', 'TW'), '繁體中文'), + en(Locale('en'), 'English'); + + final Locale locale; + + final String title; + + const Language(this.locale, this.title); +} diff --git a/lib/settings/order_awakening_setting.dart b/lib/settings/order_awakening_setting.dart index 4d14135e..db459fe7 100644 --- a/lib/settings/order_awakening_setting.dart +++ b/lib/settings/order_awakening_setting.dart @@ -1,12 +1,20 @@ import 'package:possystem/settings/setting.dart'; class OrderAwakeningSetting extends Setting { + static final instance = OrderAwakeningSetting._(); + + static const defaultValue = true; + + OrderAwakeningSetting._() { + value = defaultValue; + } + @override String get key => 'feat.orderAwakening'; @override void initialize() { - value = service.get(key) ?? true; + value = service.get(key) ?? defaultValue; } @override diff --git a/lib/settings/order_outlook_setting.dart b/lib/settings/order_outlook_setting.dart index 8fd7641e..4297a537 100644 --- a/lib/settings/order_outlook_setting.dart +++ b/lib/settings/order_outlook_setting.dart @@ -1,12 +1,20 @@ import 'package:possystem/settings/setting.dart'; class OrderOutlookSetting extends Setting { + static final instance = OrderOutlookSetting._(); + + static const defaultValue = OrderOutlookTypes.slidingPanel; + + OrderOutlookSetting._() { + value = defaultValue; + } + @override String get key => 'feat.orderOutlook'; @override void initialize() { - value = OrderOutlookTypes.values[service.get(key) ?? 0]; + value = OrderOutlookTypes.values[service.get(key) ?? defaultValue.index]; } @override @@ -16,6 +24,9 @@ class OrderOutlookSetting extends Setting { } enum OrderOutlookTypes { + /// show order in sliding panel, recommended for mobile phone slidingPanel, + + /// show order in single view, recommended for tablet singleView, } diff --git a/lib/settings/order_product_axis_count_setting.dart b/lib/settings/order_product_axis_count_setting.dart index 010d526d..adef95ec 100644 --- a/lib/settings/order_product_axis_count_setting.dart +++ b/lib/settings/order_product_axis_count_setting.dart @@ -1,12 +1,20 @@ import 'package:possystem/settings/setting.dart'; class OrderProductAxisCountSetting extends Setting { + static final instance = OrderProductAxisCountSetting._(); + + static const defaultValue = 2; + + OrderProductAxisCountSetting._() { + value = defaultValue; + } + @override String get key => 'feat.orderProductAxisCount'; @override void initialize() { - value = service.get(key) ?? 2; + value = service.get(key) ?? defaultValue; } @override diff --git a/lib/settings/setting.dart b/lib/settings/setting.dart index 1c8070a2..cd8cfde4 100644 --- a/lib/settings/setting.dart +++ b/lib/settings/setting.dart @@ -9,7 +9,9 @@ abstract class Setting extends ChangeNotifier { String get logKey => key.replaceAll('.', '_'); - // 設定異動時,是否重建 app + /// Whether the app should be rebuilt when the setting is changed + /// + /// e.g. theme, language bool get registryForApp => false; Cache get service => Cache.instance; diff --git a/lib/settings/settings_provider.dart b/lib/settings/settings_provider.dart index c21f3c1c..b407f6d4 100644 --- a/lib/settings/settings_provider.dart +++ b/lib/settings/settings_provider.dart @@ -11,23 +11,22 @@ import 'setting.dart'; import 'theme_setting.dart'; class SettingsProvider extends ChangeNotifier { - static late SettingsProvider instance; + static SettingsProvider instance = SettingsProvider._(); - static final allSettings = List.from([ - LanguageSetting(), - ThemeSetting(), - CurrencySetting(), - OrderAwakeningSetting(), - OrderOutlookSetting(), - OrderProductAxisCountSetting(), - CheckoutWarningSetting(), - CollectEventsSetting(), + final settings = List.from([ + LanguageSetting.instance, + ThemeSetting.instance, + CurrencySetting.instance, + OrderAwakeningSetting.instance, + OrderOutlookSetting.instance, + OrderProductAxisCountSetting.instance, + CheckoutWarningSetting.instance, + CollectEventsSetting.instance, ], growable: false); - final List settings; + SettingsProvider._(); - SettingsProvider(this.settings) { - instance = this; + void initialize() { for (var setting in settings) { setting.initialize(); if (setting.registryForApp) { @@ -35,10 +34,4 @@ class SettingsProvider extends ChangeNotifier { } } } - - T getSetting() { - return settings.firstWhere((setting) => setting is T) as T; - } - - static T of() => instance.getSetting(); } diff --git a/lib/settings/theme_setting.dart b/lib/settings/theme_setting.dart index ea8fe9db..1f0709bd 100644 --- a/lib/settings/theme_setting.dart +++ b/lib/settings/theme_setting.dart @@ -2,6 +2,14 @@ import 'package:flutter/material.dart'; import 'package:possystem/settings/setting.dart'; class ThemeSetting extends Setting { + static final instance = ThemeSetting._(); + + static const defaultValue = ThemeMode.system; + + ThemeSetting._() { + value = defaultValue; + } + @override String get key => 'theme'; @@ -10,7 +18,7 @@ class ThemeSetting extends Setting { @override void initialize() { - value = ThemeMode.values[service.get(key) ?? 0]; + value = ThemeMode.values[service.get(key) ?? defaultValue.index]; } @override diff --git a/lib/ui/analysis/analysis_view.dart b/lib/ui/analysis/analysis_view.dart index 67c921be..2ea00c72 100644 --- a/lib/ui/analysis/analysis_view.dart +++ b/lib/ui/analysis/analysis_view.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:intl/intl.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/style/route_circular_button.dart'; import 'package:possystem/components/tutorial.dart'; @@ -12,7 +10,6 @@ import 'package:possystem/translator.dart'; import 'package:possystem/ui/analysis/widgets/chart_card_view.dart'; import 'package:possystem/ui/analysis/widgets/chart_range_page.dart'; import 'package:possystem/ui/analysis/widgets/goals_card_view.dart'; -import 'package:spotlight_ant/spotlight_ant.dart'; class AnalysisView extends StatefulWidget { final int? tabIndex; @@ -58,26 +55,36 @@ class _AnalysisViewState extends State with AutomaticKeepAliveClie ), ]); }, - child: SliverList.list(children: const [ + child: SliverList.list(children: [ + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - RouteCircularButton( - key: Key('anal.order'), - icon: Icons.store_sharp, - route: Routes.order, - text: '點餐', + Expanded( + child: Tutorial( + id: 'anal.add_chart', + title: S.analysisChartTutorialTitle, + message: S.analysisChartTutorialContent, + child: RouteCircularButton( + key: const Key('anal.add_chart'), + route: Routes.chartNew, + icon: KIcons.add, + text: S.analysisChartTitleCreate, + ), + ), ), - SizedBox.square(dimension: 96.0), - RouteCircularButton( - key: Key('anal.history'), - icon: Icons.calendar_month_sharp, - route: Routes.history, - text: '紀錄', + const Spacer(), + Expanded( + child: RouteCircularButton( + key: const Key('anal.history'), + icon: Icons.calendar_month_sharp, + route: Routes.history, + text: S.analysisHistoryBtn, + ), ), ], ), - GoalsCardView(), + const GoalsCardView(), ]), ), ); @@ -86,26 +93,8 @@ class _AnalysisViewState extends State with AutomaticKeepAliveClie SliverAppBar _buildChartHeader() { return SliverAppBar( pinned: true, - title: const Text('圖表分析'), + title: Text(S.analysisChartTitle), toolbarHeight: kToolbarHeight - 8, // hide shadow of action when pinned - actions: [ - Tutorial( - id: 'anal.add_chart', - message: '開始設計圖表追蹤你的銷售狀況吧!', - spotlightBuilder: const SpotlightRectBuilder(borderRadius: 28), - child: ElevatedButton.icon( - key: const Key('anal.add_chart'), - onPressed: () => context.pushNamed( - Routes.chartOrderModal, - pathParameters: { - 'id': '0', - }, - ), - icon: const Icon(KIcons.add), - label: const Text('新增圖表'), - ), - ), - ], bottom: AppBar( primary: false, centerTitle: false, @@ -117,7 +106,7 @@ class _AnalysisViewState extends State with AutomaticKeepAliveClie key: const Key('anal.chart_range'), onPressed: _goToChartRange, child: Text( - range.value.format(DateFormat.MMMd(S.localeName)), + range.value.format(S.localeName), ), ), ), @@ -136,7 +125,7 @@ class _AnalysisViewState extends State with AutomaticKeepAliveClie onPressed: _showActions, enableFeedback: true, iconSize: 16, - tooltip: '設定', + tooltip: MaterialLocalizations.of(context).moreButtonTooltip, icon: const Icon(Icons.settings_sharp), ), ], @@ -186,11 +175,16 @@ class _AnalysisViewState extends State with AutomaticKeepAliveClie await showCircularBottomSheet( context, actions: >[ - const BottomSheetAction( - title: Text('排序圖表'), - leading: Icon(KIcons.reorder), + BottomSheetAction( + title: Text(S.analysisChartTitleReorder), + leading: const Icon(KIcons.reorder), route: Routes.chartReorder, ), + BottomSheetAction( + title: Text(S.analysisChartTitleCreate), + leading: const Icon(KIcons.add), + route: Routes.chartNew, + ), ], ); } diff --git a/lib/ui/analysis/history_page.dart b/lib/ui/analysis/history_page.dart index 59d6e2e2..1aff514a 100644 --- a/lib/ui/analysis/history_page.dart +++ b/lib/ui/analysis/history_page.dart @@ -8,8 +8,8 @@ import 'package:possystem/translator.dart'; import 'package:possystem/ui/transit/transit_station.dart'; import 'package:spotlight_ant/spotlight_ant.dart'; +import 'widgets/history_calendar_view.dart'; import 'widgets/history_order_list.dart'; -import 'widgets/calendar_view.dart'; class HistoryPage extends StatefulWidget { const HistoryPage({super.key}); @@ -27,21 +27,21 @@ class _HistoryPageState extends State { child: Scaffold( appBar: AppBar( leading: const PopButton(), - title: const Text('訂單記錄'), + title: Text(S.analysisHistoryTitle), actions: [ Tutorial( id: 'history.export', - title: '訂單資料匯出', - message: '把訂單匯出到外部,讓你可以做進一步分析或保存。\n你可以到「設定」去匯出多日訂單。', + title: S.analysisHistoryExportTutorialTitle, + message: S.analysisHistoryExportTutorialContent, spotlightBuilder: const SpotlightRectBuilder(borderRadius: 8.0), child: PopupMenuButton( key: const Key('history.export'), icon: const Icon(Icons.upload_file_sharp), - tooltip: '匯出', + tooltip: S.analysisHistoryExportBtn, itemBuilder: (context) => TransitMethod.values .map((TransitMethod value) => PopupMenuItem( value: value, - child: Text(S.transitMethod(value.name)), + child: Text(S.transitMethodName(value.name)), )) .toList(), onSelected: (value) { @@ -78,10 +78,10 @@ class _HistoryPageState extends State { Widget _buildCalendar({required bool isPortrait}) { return Tutorial( id: 'history.calendar', - title: '日曆格式', - message: '上下滑動可以調整週期單位,如月或週。\n左右滑動可以調整日期起訖。', + title: S.analysisHistoryCalendarTutorialTitle, + message: S.analysisHistoryCalendarTutorialContent, spotlightBuilder: const SpotlightRectBuilder(), - child: CalendarView( + child: HistoryCalendarView( isPortrait: isPortrait, notifier: notifier, ), diff --git a/lib/ui/analysis/widgets/chart_card_view.dart b/lib/ui/analysis/widgets/chart_card_view.dart index 670be2d6..11b22211 100644 --- a/lib/ui/analysis/widgets/chart_card_view.dart +++ b/lib/ui/analysis/widgets/chart_card_view.dart @@ -2,9 +2,8 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; -import 'package:possystem/components/style/more_button.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/constants/icons.dart'; -import 'package:possystem/helpers/util.dart'; import 'package:possystem/models/analysis/chart.dart'; import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/routes.dart'; @@ -57,10 +56,10 @@ class ChartCardView extends StatelessWidget { Widget buildChart(BuildContext context, List metrics) { if (metrics.isEmpty) { - return const SizedBox( + return SizedBox( width: 128, height: 128, - child: Center(child: Text('沒有資料')), + child: Center(child: Text(S.analysisChartCardEmptyData)), ); } @@ -68,7 +67,7 @@ class ChartCardView extends StatelessWidget { case AnalysisChartType.cartesian: return _CartesianChart( chart: chart, - metrics: metrics as List, + metrics: metrics as List, interval: MetricsIntervalType.fromDays(range.value.duration.inDays), ); case AnalysisChartType.circular: @@ -87,9 +86,9 @@ class ChartCardView extends StatelessWidget { warningContent: Text(S.dialogDeletionContent(chart.name, '')), actions: >[ BottomSheetAction( - title: const Text('編輯圖表'), + title: Text(S.analysisChartCardTitleUpdate), leading: const Icon(KIcons.modal), - route: Routes.chartOrderModal, + route: Routes.chartModal, routePathParameters: {'id': chart.id}, ), ], @@ -100,7 +99,7 @@ class ChartCardView extends StatelessWidget { class _CartesianChart extends StatelessWidget { final Chart chart; - final List metrics; + final List metrics; final MetricsIntervalType interval; @@ -150,7 +149,7 @@ class _CartesianChart extends StatelessWidget { return LineSeries( animationDuration: 0, markerSettings: const MarkerSettings(isVisible: true), - name: chart.target == OrderMetricTarget.order ? S.analysisChartMetric(keyUnit.key) : keyUnit.key, + name: chart.target == OrderMetricTarget.order ? S.analysisChartMetricName(keyUnit.key) : keyUnit.key, yAxisName: keyUnit.value.name, xValueMapper: (v, i) => v.at, yValueMapper: (v, i) => v.value(keyUnit.key), @@ -204,6 +203,7 @@ class _CircularChart extends StatelessWidget { ); } + final percentFormat = NumberFormat.percentPattern(S.localeName); return SfCircularChart( tooltipBehavior: TooltipBehavior( enable: true, @@ -222,7 +222,7 @@ class _CircularChart extends StatelessWidget { xValueMapper: (v, i) => v.name, yValueMapper: (v, i) => v.value, dataSource: metrics, - dataLabelMapper: (v, i) => '${v.percent.prettyString()}%', + dataLabelMapper: (v, i) => percentFormat.format(v.percent), dataLabelSettings: const DataLabelSettings( isVisible: true, labelPosition: ChartDataLabelPosition.inside, diff --git a/lib/ui/analysis/widgets/chart_order_modal.dart b/lib/ui/analysis/widgets/chart_modal.dart similarity index 86% rename from lib/ui/analysis/widgets/chart_order_modal.dart rename to lib/ui/analysis/widgets/chart_modal.dart index 9d6b2673..6f8b88ee 100644 --- a/lib/ui/analysis/widgets/chart_order_modal.dart +++ b/lib/ui/analysis/widgets/chart_modal.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; import 'package:possystem/components/style/text_divider.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/analysis/analysis.dart'; @@ -10,16 +10,16 @@ import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/translator.dart'; import 'package:syncfusion_flutter_charts/charts.dart'; -class ChartOrderModal extends StatefulWidget { +class ChartModal extends StatefulWidget { final Chart? chart; - const ChartOrderModal({super.key, required this.chart}); + const ChartModal({super.key, this.chart}); @override - State createState() => _ChartOrderModalState(); + State createState() => _ChartModalState(); } -class _ChartOrderModalState extends State with ItemModal { +class _ChartModalState extends State with ItemModal { final _nameController = TextEditingController(); final _nameFocusNode = FocusNode(); @@ -30,7 +30,7 @@ class _ChartOrderModalState extends State with ItemModal[]; @override - String get title => widget.chart?.name ?? S.analysisChartCreate; + String get title => widget.chart?.name ?? S.analysisChartTitleCreate; @override List buildFormFields() { @@ -42,12 +42,12 @@ class _ChartOrderModalState extends State with ItemModal with ItemModal with ItemModal with ItemModal with ItemModal with ItemModal with ItemModal with SingleTickerProvid late final Map<_TabType, Map> ranges; - final format = DateFormat.MMMd(S.localeName); - @override Widget build(BuildContext context) { final local = MaterialLocalizations.of(context); @@ -37,17 +35,17 @@ class _ChartRangePageState extends State with SingleTickerProvid const SizedBox(width: 8), ], bottom: TabBar(controller: _controller, tabs: [ - for (final tab in _TabType.values) Tab(child: Text(tab.title, softWrap: true)), + for (final tab in _TabType.values) Tab(child: Text(S.analysisChartRangeTabName(tab.name), softWrap: true)), ]), ), body: TabBarView(controller: _controller, children: [ - for (final tab in [_TabType.date, _TabType.week, _TabType.month]) + for (final tab in [_TabType.day, _TabType.week, _TabType.month]) ListView( children: [ for (final e in ranges[tab]!.entries) RadioListTile( title: Text(e.key), - subtitle: Text(e.value.format(format)), + subtitle: Text(e.value.format(S.localeName)), value: e.value, groupValue: select, onChanged: (value) => setState(() => select = value!), @@ -57,22 +55,12 @@ class _ChartRangePageState extends State with SingleTickerProvid ListView( children: [ ListTile( - title: Text(select.format(format)), + title: Text(select.format(S.localeName)), onTap: () async { - final value = await showDateRangePicker( - context: context, - firstDate: DateTime(2021), - lastDate: DateTime.now(), - initialDateRange: DateTimeRange( - start: select.start, - end: select.end.subtract(const Duration(days: 1)), - ), - ); + final value = await showMyDateRangePicker(context, select); + if (value != null) { - setState(() => select = DateTimeRange( - start: value.start, - end: value.end.add(const Duration(days: 1)), - )); + setState(() => select = value); } }, ), @@ -91,40 +79,40 @@ class _ChartRangePageState extends State with SingleTickerProvid final thisWeek = today.subtract(Duration(days: today.weekday - 1)); final thisMonth = DateTime(today.year, today.month); ranges = { - _TabType.date: { - '昨日': DateTimeRange( + _TabType.day: { + S.analysisChartRangeYesterday: DateTimeRange( start: today.subtract(const Duration(days: 1)), end: today, ), - '今日': DateTimeRange( + S.analysisChartRangeToday: DateTimeRange( start: today, end: today.add(const Duration(days: 1)), ), }, _TabType.week: { - '最近7日': DateTimeRange( + S.analysisChartRangeLast7Days: DateTimeRange( start: today.subtract(const Duration(days: 7)), end: today, ), - '本週': DateTimeRange( + S.analysisChartRangeThisWeek: DateTimeRange( start: thisWeek, end: thisWeek.add(const Duration(days: 7)), ), - '上週': DateTimeRange( + S.analysisChartRangeLastWeek: DateTimeRange( start: thisWeek.subtract(const Duration(days: 7)), end: thisWeek, ), }, _TabType.month: { - '最近30日': DateTimeRange( + S.analysisChartRangeLast30Days: DateTimeRange( start: today.subtract(const Duration(days: 30)), end: today, ), - '本月': DateTimeRange( + S.analysisChartRangeThisMonth: DateTimeRange( start: thisMonth, end: DateTime(now.year, now.month + 1), ), - '上月': DateTimeRange( + S.analysisChartRangeLastMonth: DateTimeRange( start: DateTime(now.year, now.month - 1), end: thisMonth, ), @@ -142,12 +130,8 @@ class _ChartRangePageState extends State with SingleTickerProvid } enum _TabType { - date('日期'), - week('週'), - month('月'), - custom('自訂'); - - final String title; - - const _TabType(this.title); + day, + week, + month, + custom; } diff --git a/lib/ui/analysis/widgets/chart_reorder.dart b/lib/ui/analysis/widgets/chart_reorder.dart index 5b671dee..9b4f1857 100644 --- a/lib/ui/analysis/widgets/chart_reorder.dart +++ b/lib/ui/analysis/widgets/chart_reorder.dart @@ -11,7 +11,7 @@ class ChartReorder extends StatelessWidget { Widget build(BuildContext context) { return ReorderableScaffold( items: Analysis.instance.itemList, - title: S.menuCatalogReorder, + title: S.analysisChartTitleReorder, handleSubmit: (List items) => Analysis.instance.reorderItems(items), ); } diff --git a/lib/ui/analysis/widgets/goals_card_view.dart b/lib/ui/analysis/widgets/goals_card_view.dart index 72a26174..539d005d 100644 --- a/lib/ui/analysis/widgets/goals_card_view.dart +++ b/lib/ui/analysis/widgets/goals_card_view.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:possystem/components/style/info_popup.dart'; +import 'package:intl/intl.dart'; import 'package:possystem/helpers/analysis/ema_calculator.dart'; import 'package:possystem/helpers/util.dart'; import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/services/cache.dart'; +import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; import 'package:possystem/ui/analysis/widgets/reloadable_card.dart'; class GoalsCardView extends StatefulWidget { @@ -20,13 +22,15 @@ class GoalsCardView extends StatefulWidget { } class _GoalsCardViewState extends State { - OrderDataPerDay? goal; + OrderSummary? goal; + + final formatter = NumberFormat.percentPattern(); @override Widget build(BuildContext context) { - return ReloadableCard( + return ReloadableCard( id: 'goals', - title: '本日總結', + title: S.analysisGoalsTitle, notifiers: [Seller.instance], builder: _builder, loader: _loader, @@ -39,46 +43,46 @@ class _GoalsCardViewState extends State { // If the user disabled the goals, we don't need to load the data. // which is currently only option(goals is a beta feature). if (enabled != true) { - goal = OrderDataPerDay(at: DateTime(0)); + goal = OrderSummary(at: DateTime(0)); } super.initState(); } - Widget _builder(BuildContext context, OrderDataPerDay metric) { - final style = Theme.of(context).textTheme.bodyLarge; + Widget _builder(BuildContext context, OrderSummary metric) { + final style = Theme.of(context).textTheme.bodyLarge?.copyWith( + overflow: TextOverflow.ellipsis, + ); final goals = [ _GoalItem( type: OrderMetricType.count, current: metric.count, goal: goal!.count, style: style, - name: '訂單數', - desc: - '訂單數反映了產品對顧客的吸引力。它代表了市場對你產品的需求程度,能幫助你了解何種產品或時段最受歡迎。高訂單數可能意味著你的定價策略或行銷活動取得成功,是商業模型有效性的指標之一。但要注意,單純追求高訂單數可能會忽略盈利能力。', - ), - _GoalItem( - type: OrderMetricType.price, - current: metric.price, - goal: goal!.price, - style: style, - name: '營收', - desc: '營收代表總銷售額,是業務規模的指標。高營收可能顯示了你的產品受歡迎且銷售良好,但營收無法反映出業務的可持續性和盈利能力。它不考慮成本和利潤,因此單純追求高營收可能會忽視實際利潤狀況。', + name: S.analysisGoalsCountTitle, + desc: S.analysisGoalsCountDescription, ), _GoalItem( type: OrderMetricType.revenue, current: metric.revenue, goal: goal!.revenue, style: style, - name: '盈利', - desc: - '盈利是店家能否持續經營的關鍵。盈利直接反映了營運效率和成本管理能力。不同於營收,盈利考慮了生意的開支,包括原料成本、人力、租金等,這是一個更實際的指標,能幫助你評估經營是否有效且可持續。即使有高營收,但如果成本高於營收,最終可能面臨經營困境。', + name: S.analysisGoalsRevenueTitle, + desc: S.analysisGoalsRevenueDescription, + ), + _GoalItem( + type: OrderMetricType.profit, + current: metric.profit, + goal: goal!.profit, + style: style, + name: S.analysisGoalsProfitTitle, + desc: S.analysisGoalsProfitDescription, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('成本', style: style), - Text(metric.cost.prettyString(), style: style), + Text(S.analysisGoalsCostTitle, style: style), + Text(metric.cost.toCurrency(), style: style), ], ), ]; @@ -91,7 +95,7 @@ class _GoalsCardViewState extends State { children: goals, ), ), - if (goal!.revenue != 0) + if (goal!.profit != 0) Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), @@ -99,7 +103,7 @@ class _GoalsCardViewState extends State { AspectRatio( aspectRatio: 1.0, child: CircularProgressIndicator( - value: metric.revenue / goal!.revenue, + value: metric.profit / goal!.profit, color: Colors.pink, backgroundColor: Colors.grey.withOpacity(0.2), strokeWidth: 20, @@ -108,7 +112,7 @@ class _GoalsCardViewState extends State { Positioned.fill( child: Center( child: Text( - '利潤達成\n${(metric.revenue / goal!.revenue * 100).prettyString()}%', + S.analysisGoalsAchievedRate(formatter.format(metric.profit / goal!.profit)), style: Theme.of(context).textTheme.titleMedium, ), ), @@ -120,7 +124,7 @@ class _GoalsCardViewState extends State { ); } - Future _loader() async { + Future _loader() async { final range = Util.getDateRange(); final result = await Seller.instance.getMetricsInPeriod( // If there is no data, we need to calculate the EMA from the last 20 data points withing 40 days. @@ -128,8 +132,8 @@ class _GoalsCardViewState extends State { range.end, types: [ OrderMetricType.count, - OrderMetricType.price, OrderMetricType.revenue, + OrderMetricType.profit, OrderMetricType.cost, ], ignoreEmpty: true, @@ -138,15 +142,15 @@ class _GoalsCardViewState extends State { ); // Remove the first data, which is the today's data. - final todayData = result.isEmpty ? OrderDataPerDay(at: range.start) : result.removeAt(0); + final todayData = result.isEmpty ? OrderSummary(at: range.start) : result.removeAt(0); final reversed = result.reversed; - goal ??= OrderDataPerDay( + goal ??= OrderSummary( at: DateTime(0), // this is dummy data, we don't need the date. values: { 'count': widget.calculator.calculate(reversed.map((e) => e.count)), - 'price': widget.calculator.calculate(reversed.map((e) => e.price)), 'revenue': widget.calculator.calculate(reversed.map((e) => e.revenue)), + 'profit': widget.calculator.calculate(reversed.map((e) => e.profit)), }, ); @@ -181,19 +185,17 @@ class _GoalItem extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Text(name, style: style), - const SizedBox(width: 4.0), - InfoPopup(desc), - ]), + Text(name, style: style), + // TODO: tap to show the description + // InfoPopup(desc), RichText( text: TextSpan( - text: current.prettyString(), + text: current.toCurrency(), style: style, children: [ if (goal != 0) TextSpan( - text: '/${goal.prettyString()}', + text: '/${goal.toCurrency()}', style: const TextStyle(color: Colors.grey), ), ], diff --git a/lib/ui/analysis/widgets/calendar_view.dart b/lib/ui/analysis/widgets/history_calendar_view.dart similarity index 88% rename from lib/ui/analysis/widgets/calendar_view.dart rename to lib/ui/analysis/widgets/history_calendar_view.dart index 2d169bc5..54915535 100644 --- a/lib/ui/analysis/widgets/calendar_view.dart +++ b/lib/ui/analysis/widgets/history_calendar_view.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:possystem/helpers/util.dart'; import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/settings/language_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; import 'package:provider/provider.dart'; import 'package:table_calendar/table_calendar.dart'; @@ -12,22 +11,22 @@ import 'package:table_calendar/table_calendar.dart'; int _hashDate(DateTime e) => e.day + e.month * 100 + e.year * 10000; int _hashMonth(DateTime e) => e.month + e.year * 100; -class CalendarView extends StatefulWidget { +class HistoryCalendarView extends StatefulWidget { final ValueNotifier notifier; final bool isPortrait; - const CalendarView({ + const HistoryCalendarView({ super.key, required this.notifier, required this.isPortrait, }); @override - State createState() => _CalendarViewState(); + State createState() => _HistoryCalendarViewState(); } -class _CalendarViewState extends State { +class _HistoryCalendarViewState extends State { final List _loadedMonths = []; final LinkedHashMap _loadedCounts = LinkedHashMap( @@ -53,16 +52,16 @@ class _CalendarViewState extends State { shouldFillViewport: widget.isPortrait ? false : true, startingDayOfWeek: StartingDayOfWeek.monday, rangeSelectionMode: RangeSelectionMode.disabled, - locale: SettingsProvider.instance.getSetting().value.toString(), + locale: LanguageSetting.instance.value.locale.toString(), // header // chinese will be hidden if using default value daysOfWeekHeight: 20.0, headerStyle: const HeaderStyle(formatButtonShowsNext: false), - // show next format + // show next format, so k/v are not matching availableCalendarFormats: { - CalendarFormat.month: S.analysisCalendarTwoWeek, - CalendarFormat.twoWeeks: S.analysisCalendarWeek, - CalendarFormat.week: S.analysisCalendarMonth, + CalendarFormat.month: S.twoWeeks, + CalendarFormat.twoWeeks: S.singleWeek, + CalendarFormat.week: S.singleMonth, }, // no need holiday/weekend days holidayPredicate: (day) => false, @@ -129,7 +128,7 @@ class _CalendarViewState extends State { ); } - /// the day is UTC!!! + /// the day is UTC formatted void _onDaySelected(DateTime day) { widget.notifier.value = Util.getDateRange(now: day.toLocal()); setState(() { @@ -137,7 +136,7 @@ class _CalendarViewState extends State { }); } - /// the [day] is UTC!!! + /// the [day] is UTC formatted void _searchPageData(DateTime day) { // make calender page stay in current page _focusedDay = day; @@ -147,7 +146,7 @@ class _CalendarViewState extends State { } } - /// the [day] is UTC!!! + /// the [day] is UTC formatted void _searchCountInMonth(DateTime day) async { final local = day.toLocal(); // add/sub 7 days for first/last few days on next/last month diff --git a/lib/ui/analysis/widgets/history_order_list.dart b/lib/ui/analysis/widgets/history_order_list.dart index 10e767dd..801114c2 100644 --- a/lib/ui/analysis/widgets/history_order_list.dart +++ b/lib/ui/analysis/widgets/history_order_list.dart @@ -25,9 +25,9 @@ class HistoryOrderList extends StatelessWidget { Widget _buildOrder(BuildContext context, OrderObject order) { final subtitle = MetaBlock.withString(context, [ - S.analysisOrderListItemMetaPaid(order.paid), - S.analysisOrderListItemMetaPrice(order.price), - S.analysisOrderListItemMetaIncome(order.revenue), + S.analysisHistoryOrderListMetaPaid(order.paid), + S.analysisHistoryOrderListMetaPrice(order.price), + S.analysisHistoryOrderListMetaProfit(order.profit), ]); return ListTile( diff --git a/lib/ui/analysis/widgets/history_order_modal.dart b/lib/ui/analysis/widgets/history_order_modal.dart index 2b4b0ef5..1904e9b6 100644 --- a/lib/ui/analysis/widgets/history_order_modal.dart +++ b/lib/ui/analysis/widgets/history_order_modal.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/meta_block.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/components/style/hint_text.dart'; -import 'package:possystem/components/style/more_button.dart'; import 'package:possystem/components/style/pop_button.dart'; import 'package:possystem/components/style/snackbar.dart'; import 'package:possystem/helpers/util.dart'; @@ -29,6 +29,7 @@ class _HistoryOrderModalState extends State { return Scaffold( appBar: AppBar( leading: const PopButton(), + title: Text(S.analysisHistoryOrderTitle), actions: [ MoreButton( key: const Key('order_modal.more'), @@ -40,7 +41,7 @@ class _HistoryOrderModalState extends State { future: Seller.instance.getOrder(widget.orderId), builder: Util.handleSnapshot((context, order) { if (order == null) { - return const Center(child: Text('找不到相關訂單')); + return Center(child: Text(S.analysisHistoryOrderNotFound)); } createdAt = DateFormat.MMMEd(S.localeName).format(order.createdAt) + @@ -72,14 +73,7 @@ class _HistoryOrderModalState extends State { context, 'analysis_delete_error', ), - warningContent: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('確定要刪除 $createdAt 的訂單嗎?'), - const Text('\n將不會復原收銀機和庫存資料。'), - const Text('\n此動作無法復原。'), - ], - ), + warningContent: Text(S.analysisHistoryOrderDeleteDialog(createdAt!)), ); } } diff --git a/lib/ui/cashier/cashier_view.dart b/lib/ui/cashier/cashier_view.dart index e1a5b159..a973e373 100644 --- a/lib/ui/cashier/cashier_view.dart +++ b/lib/ui/cashier/cashier_view.dart @@ -28,44 +28,50 @@ class _CashierViewState extends State with AutomaticKeepAliveClient return TutorialWrapper( tab: tab, - child: ListView(padding: const EdgeInsets.only(bottom: 76), children: [ + child: ListView(padding: const EdgeInsets.only(bottom: 76, top: 16), children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Tutorial( - id: 'cashier.default', - index: 2, - title: S.orderCashierDefaultTutorialTitle, - message: S.orderCashierDefaultTutorialMessage, - child: RouteCircularButton( - key: const Key('cashier.defaulter'), - onTap: handleSetDefault, - icon: Icons.upload_sharp, - text: S.orderCashierDefaultButton, + Expanded( + child: Tutorial( + id: 'cashier.default', + index: 2, + title: S.cashierToDefaultTutorialTitle, + message: S.cashierToDefaultTutorialContent, + child: RouteCircularButton( + key: const Key('cashier.defaulter'), + onTap: handleSetDefault, + icon: Icons.upload_sharp, + text: S.cashierToDefaultTitle, + ), ), ), - Tutorial( - index: 1, - id: 'cashier.change', - title: S.orderCashierChangeTutorialTitle, - message: S.orderCashierChangeTutorialMessage, - child: RouteCircularButton( - key: const Key('cashier.changer'), - route: Routes.cashierChanger, - icon: Icons.sync_alt_sharp, - text: S.orderCashierChangeButton, - popTrueShowSuccess: true, + Expanded( + child: Tutorial( + index: 1, + id: 'cashier.change', + title: S.cashierChangerTutorialTitle, + message: S.cashierChangerTutorialContent, + child: RouteCircularButton( + key: const Key('cashier.changer'), + route: Routes.cashierChanger, + icon: Icons.sync_alt_sharp, + text: S.cashierChangerTitle, + popTrueShowSuccess: true, + ), ), ), - Tutorial( - index: 0, - id: 'cashier.surplus', - title: S.orderCashierSurplusTutorialTitle, - message: S.orderCashierSurplusTutorialMessage, - child: RouteCircularButton( - key: const Key('cashier.surplus'), - icon: Icons.coffee_sharp, - text: S.orderCashierSurplusButton, - popTrueShowSuccess: true, - onTap: handleSurplus, + Expanded( + child: Tutorial( + index: 0, + id: 'cashier.surplus', + title: S.cashierSurplusTutorialTitle, + message: S.cashierSurplusTutorialContent, + child: RouteCircularButton( + key: const Key('cashier.surplus'), + icon: Icons.coffee_sharp, + text: S.cashierSurplusTitle, + popTrueShowSuccess: true, + onTap: handleSurplus, + ), ), ), ]), @@ -88,8 +94,8 @@ class _CashierViewState extends State with AutomaticKeepAliveClient if (!Cashier.instance.defaultNotSet) { final result = await ConfirmDialog.show( context, - title: '調整收銀臺預設?', - content: '此動作將會覆蓋掉先前的設定。', + title: S.cashierToDefaultDialogTitle, + content: S.cashierToDefaultDialogContent, ); if (!result) { @@ -106,7 +112,7 @@ class _CashierViewState extends State with AutomaticKeepAliveClient void handleSurplus() async { if (Cashier.instance.defaultNotSet) { - return showSnackBar(context, '尚未設定,請點選右上角「設為預設」'); + return showSnackBar(context, S.cashierSurplusErrorEmptyDefault); } final result = await context.pushNamed(Routes.cashierSurplus); diff --git a/lib/ui/cashier/changer_page.dart b/lib/ui/cashier/changer_page.dart index 5270b75a..c54a86aa 100644 --- a/lib/ui/cashier/changer_page.dart +++ b/lib/ui/cashier/changer_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:possystem/components/style/pop_button.dart'; +import 'package:possystem/translator.dart'; import 'widgets/changer_custom_view.dart'; import 'widgets/changer_favorite_view.dart'; @@ -22,9 +23,9 @@ class _ChangerModalState extends State with TickerProviderStateMix // tab widgets final tabBar = TabBar( controller: controller, - tabs: const [ - Tab(key: Key('changer.favorite'), text: '常用'), - Tab(key: Key('changer.custom'), text: '手動'), + tabs: [ + Tab(key: const Key('changer.favorite'), text: S.cashierChangerFavoriteTab), + Tab(key: const Key('changer.custom'), text: S.cashierChangerCustomTab), ], ); final tabBarView = TabBarView(controller: controller, children: [ @@ -42,12 +43,12 @@ class _ChangerModalState extends State with TickerProviderStateMix resizeToAvoidBottomInset: false, appBar: AppBar( leading: const PopButton(), - title: const Text('換錢'), + title: Text(S.cashierChangerTitle), actions: [ TextButton( key: const Key('changer.apply'), onPressed: handleApply, - child: const Text('套用'), + child: Text(S.cashierChangerButton), ), ], ), diff --git a/lib/ui/cashier/surplus_page.dart b/lib/ui/cashier/surplus_page.dart index 71e1a676..10142f2e 100644 --- a/lib/ui/cashier/surplus_page.dart +++ b/lib/ui/cashier/surplus_page.dart @@ -6,6 +6,7 @@ import 'package:possystem/components/style/info_popup.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/repository/cashier.dart'; import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; import 'package:provider/provider.dart'; class CashierSurplus extends StatelessWidget { @@ -15,11 +16,11 @@ class CashierSurplus extends StatelessWidget { Widget build(BuildContext context) { final cashier = context.watch(); - const columns = [ - DataColumn(label: Text('單位'), numeric: true), - DataColumn(label: Text('現有'), numeric: true), - DataColumn(label: Text('差異')), - DataColumn(label: Text('預設'), numeric: true), + final columns = [ + DataColumn(label: Text(S.cashierSurplusColumnName('unit')), numeric: true), + DataColumn(label: Text(S.cashierSurplusColumnName('currentCount')), numeric: true), + DataColumn(label: Text(S.cashierSurplusColumnName('diffCount'))), + DataColumn(label: Text(S.cashierSurplusColumnName('defaultCount')), numeric: true), ]; final rows = [ @@ -35,7 +36,7 @@ class CashierSurplus extends StatelessWidget { return Scaffold( appBar: AppBar( leading: const CloseButton(key: Key('pop')), - title: const Text('結餘'), + title: Text(S.cashierSurplusButton), actions: [ TextButton( key: const Key('cashier_surplus.confirm'), @@ -52,21 +53,18 @@ class CashierSurplus extends StatelessWidget { body: Column(children: [ _DataWithLabel( data: cashier.currentTotal.toCurrency(), - label: '現有總額', - helper: '現在收銀機應該要有的總額\n若你發現現金和這值對不上,想一想今天有沒有用收銀機的錢買東西?', + label: S.cashierSurplusCurrentTotalLabel, + helper: S.cashierSurplusCurrentTotalHelper, ), _DataWithLabel( data: (cashier.currentTotal - cashier.defaultTotal).toCurrency(), - label: '差額', - helper: '和收銀機最一開始的總額的差額\n這可以快速幫你了解今天收銀機多了多少錢唷。', + label: S.cashierSurplusDiffTotalLabel, + helper: S.cashierSurplusDiffTotalHelper, ), const Divider(), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: HintText( - '若你確認收銀機的金錢都沒問題之後就可以完成結餘囉!', - textAlign: TextAlign.center, - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: HintText(S.cashierSurplusTableHint, textAlign: TextAlign.center), ), Expanded( child: Padding( @@ -102,11 +100,11 @@ class CashierSurplus extends StatelessWidget { final result = await showDialog( context: context, builder: (BuildContext context) => SingleTextDialog( - validator: Validator.positiveInt('數量'), + validator: Validator.positiveInt(S.cashierSurplusCounterShortLabel), keyboardType: TextInputType.number, selectAll: true, initialValue: item.currentCount.toString(), - title: Text('幣值${item.unit.toCurrency()}的數量'), + title: Text(S.cashierSurplusCounterLabel(item.unit.toCurrency())), ), ); diff --git a/lib/ui/cashier/widgets/changer_custom_view.dart b/lib/ui/cashier/widgets/changer_custom_view.dart index 62e83bd6..9c31338a 100644 --- a/lib/ui/cashier/widgets/changer_custom_view.dart +++ b/lib/ui/cashier/widgets/changer_custom_view.dart @@ -6,6 +6,7 @@ import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/objects/cashier_object.dart'; import 'package:possystem/models/repository/cashier.dart'; import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; class ChangerCustomView extends StatefulWidget { final VoidCallback afterFavoriteAdded; @@ -46,7 +47,7 @@ class ChangerCustomViewState extends State { FilledButton( key: const Key('changer.custom.add_favorite'), onPressed: handleAddFavorite, - child: const Text('新增常用'), + child: Text(S.cashierChangerCustomAddBtn), ), ]); @@ -56,16 +57,16 @@ class ChangerCustomViewState extends State { controller: sourceCount, keyboardType: TextInputType.number, onChanged: handleCountChanged, - decoration: const InputDecoration(labelText: '數量'), - validator: Validator.positiveInt('數量', minimum: 1), + decoration: InputDecoration(labelText: S.cashierChangerCustomCountLabel), + validator: Validator.positiveInt(S.cashierChangerCustomCountLabel, minimum: 1), ), DropdownButtonFormField( key: const Key('changer.custom.source.unit'), value: sourceUnit, - hint: const Text('幣值'), + hint: Text(S.cashierChangerCustomUnitLabel), isDense: true, style: Theme.of(context).textTheme.bodyMedium, - validator: Validator.positiveNumber('幣值'), + validator: Validator.positiveNumber(S.cashierChangerCustomUnitLabel), onChanged: handleUnitChanged, items: _unitDropdownMenuItems(), autovalidateMode: AutovalidateMode.onUserInteraction, @@ -81,14 +82,14 @@ class ChangerCustomViewState extends State { controller: entry.key == 0 ? targetController : null, initialValue: entry.key == 0 ? null : entry.value.count?.toString(), keyboardType: TextInputType.number, - decoration: const InputDecoration(labelText: '數量'), + decoration: InputDecoration(labelText: S.cashierChangerCustomCountLabel), validator: Validator.positiveInt('', allowNull: true), onSaved: (value) => entry.value.count = int.tryParse(value ?? ''), ), DropdownButtonFormField( key: Key('changer.custom.target.${entry.key}.unit'), value: entry.value.unit, - hint: const Text('幣值'), + hint: Text(S.cashierChangerCustomCountLabel), style: Theme.of(context).textTheme.bodyMedium, onChanged: (value) => setState(() => entry.value.unit = value), onSaved: (value) => entry.value.unit = value, @@ -116,9 +117,9 @@ class ChangerCustomViewState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ actions, - const TextDivider(label: '從收銀機中拿出'), + TextDivider(label: S.cashierChangerCustomDividerFrom), sourceEntry, - const TextDivider(label: '換'), + TextDivider(label: S.cashierChangerCustomDividerTo), Focus( focusNode: errorFocus, child: Builder(builder: (context) { @@ -140,7 +141,7 @@ class ChangerCustomViewState extends State { targets.add(CashierChangeEntryObject()); }), icon: const Icon(KIcons.add), - label: const Text('新增幣種'), + label: Text(S.cashierChangerCustomUnitAddBtn), ) ], ), @@ -198,7 +199,7 @@ class ChangerCustomViewState extends State { }); return true; } else { - _setError('$sourceUnit 元不夠換'); + _setError(S.cashierChangerErrorNotEnough(sourceUnit!.toCurrency())); return false; } } @@ -232,10 +233,10 @@ class ChangerCustomViewState extends State { return true; } - var msg = '$count 個 $sourceUnit 元沒辦法換'; + var msg = S.cashierChangerErrorInvalidHead(count, sourceUnit!.toCurrency()); for (var target in targets) { if (!target.isEmpty) { - msg += '\n- ${target.count} 個 ${target.unit} 元'; + msg += '\n • ${S.cashierChangerErrorInvalidBody(target.count!, target.unit!.toCurrency())}'; } } _setError(msg); diff --git a/lib/ui/cashier/widgets/changer_favorite_view.dart b/lib/ui/cashier/widgets/changer_favorite_view.dart index 5bc39fef..fb6c2f92 100644 --- a/lib/ui/cashier/widgets/changer_favorite_view.dart +++ b/lib/ui/cashier/widgets/changer_favorite_view.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/slidable_item_list.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/components/style/empty_body.dart'; import 'package:possystem/components/style/hint_text.dart'; -import 'package:possystem/components/style/more_button.dart'; import 'package:possystem/components/style/snackbar.dart'; import 'package:possystem/constants/constant.dart'; import 'package:possystem/models/objects/cashier_object.dart'; import 'package:possystem/models/repository/cashier.dart'; +import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; import 'package:provider/provider.dart'; class ChangerFavoriteView extends StatefulWidget { @@ -37,7 +39,7 @@ class ChangerFavoriteViewState extends State { Widget build(BuildContext context) { if (Cashier.instance.favoriteIsEmpty) { return EmptyBody( - helperText: '這裡可以幫助你快速轉換不同幣值', + helperText: S.cashierChangerFavoriteEmptyBody, onPressed: widget.emptyAction, ); } @@ -51,10 +53,10 @@ class ChangerFavoriteViewState extends State { child: RadioListTile( key: Key('changer.favorite.$index'), value: item, - title: Text('用 ${item.source.count} 個 ${item.source.unit} 元換'), + title: Text(S.cashierChangerFavoriteItemFrom(item.source.count!, item.source.unit!.toCurrency())), subtitle: MetaBlock.withString( context, - item.targets.map((e) => '${e.count} 個 ${e.unit} 元'), + item.targets.map((e) => S.cashierChangerFavoriteItemTo(e.count!, e.unit!.toCurrency())), textOverflow: TextOverflow.visible, ), secondary: EntryMoreButton(onPressed: showActions), @@ -66,9 +68,9 @@ class ChangerFavoriteViewState extends State { ); return Column(children: [ - const Padding( - padding: EdgeInsets.all(kSpacing1), - child: HintText('選完後請點選「套用」來使用該組合'), + Padding( + padding: const EdgeInsets.all(kSpacing1), + child: HintText(S.cashierChangerFavoriteHint), ), Expanded(child: SlidableItemList(delegate: delegate)), ]); @@ -76,14 +78,14 @@ class ChangerFavoriteViewState extends State { Future handleApply() async { if (selected == null) { - showSnackBar(context, '請選擇要套用的組合'); + showSnackBar(context, S.cashierChangerErrorNoSelection); return false; } final isValid = await Cashier.instance.applyFavorite(selected!.item); if (!isValid && mounted) { - showSnackBar(context, '${selected!.source.unit} 元不夠換'); + showSnackBar(context, S.cashierChangerErrorNotEnough(selected!.source.unit?.toCurrency() ?? '')); } return isValid; diff --git a/lib/ui/cashier/widgets/unit_list_view.dart b/lib/ui/cashier/widgets/unit_list_view.dart index 9cae4b03..dfc6817e 100644 --- a/lib/ui/cashier/widgets/unit_list_view.dart +++ b/lib/ui/cashier/widgets/unit_list_view.dart @@ -4,6 +4,8 @@ import 'package:possystem/components/style/percentile_bar.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/objects/cashier_object.dart'; import 'package:possystem/models/repository/cashier.dart'; +import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; import 'package:provider/provider.dart'; class UnitListView extends StatelessWidget { @@ -22,7 +24,7 @@ class UnitListView extends StatelessWidget { Widget _itemWidget(BuildContext context, CashierUnitObject item, int index) { final max = Cashier.instance.defaultAt(index)?.count ?? 0; return ListTile( - title: Text('幣值:${item.unit}'), + title: Text(S.cashierUnitLabel(item.unit.toCurrencyLong())), subtitle: PercentileBar(item.count, max), onTap: () => _setUnitCount(context, item.unit, max, item.count), ); @@ -39,11 +41,9 @@ class UnitListView extends StatelessWidget { builder: (BuildContext context) => SliderTextDialog( value: value, max: max.toDouble(), - title: Text('幣值:$unit'), - validator: Validator.positiveInt('數量'), - decoration: const InputDecoration( - label: Text('數量'), - ), + title: Text(S.cashierUnitLabel(unit.toCurrency())), + validator: Validator.positiveInt(S.cashierCounterLabel), + decoration: InputDecoration(label: Text(S.cashierCounterLabel)), ), ); diff --git a/lib/ui/home/feature_request_page.dart b/lib/ui/home/feature_request_page.dart index eb21f5ba..822cb8fe 100644 --- a/lib/ui/home/feature_request_page.dart +++ b/lib/ui/home/feature_request_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/linkify.dart'; import 'package:possystem/components/style/pop_button.dart'; +import 'package:possystem/translator.dart'; class FeatureRequestPage extends StatelessWidget { const FeatureRequestPage({super.key}); @@ -25,9 +26,7 @@ class FeatureRequestPage extends StatelessWidget { ), const SizedBox(height: 14.0), Linkify.fromString( - '覺得這裡還少了什麼嗎?\n' - '歡迎[提供建議](https://forms.gle/R1vZDk9ztQLScUdb9)。\n' - '也可以來看看[排程中的功能](https://github.com/evan361425/flutter-pos-system/milestones)。', + S.settingElfContent, textAlign: TextAlign.center, ) ]), diff --git a/lib/ui/home/features_page.dart b/lib/ui/home/features_page.dart index 019e1fdd..da51a1d3 100644 --- a/lib/ui/home/features_page.dart +++ b/lib/ui/home/features_page.dart @@ -1,10 +1,12 @@ +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:possystem/components/scaffold/item_list_scaffold.dart'; import 'package:possystem/components/sign_in_button.dart'; import 'package:possystem/components/style/outlined_text.dart'; import 'package:possystem/components/style/pop_button.dart'; +import 'package:possystem/routes.dart'; import 'package:possystem/services/auth.dart'; import 'package:possystem/settings/checkout_warning.dart'; import 'package:possystem/settings/collect_events_setting.dart'; @@ -12,188 +14,239 @@ import 'package:possystem/settings/language_setting.dart'; import 'package:possystem/settings/order_awakening_setting.dart'; import 'package:possystem/settings/order_outlook_setting.dart'; import 'package:possystem/settings/order_product_axis_count_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/settings/theme_setting.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/home/widgets/feature_slider.dart'; import 'package:possystem/ui/home/widgets/feature_switch.dart'; -class FeaturesPage extends StatefulWidget { - const FeaturesPage({super.key}); +class FeaturesPage extends StatelessWidget { + final String? focus; - @override - State createState() => _FeaturesPageState(); -} - -class _FeaturesPageState extends State { - final theme = SettingsProvider.of(); - final language = SettingsProvider.of(); - final orderAwakening = SettingsProvider.of(); - final orderOutlook = SettingsProvider.of(); - final orderCount = SettingsProvider.of(); - final checkoutWarning = SettingsProvider.of(); - final collectEvents = SettingsProvider.of(); + const FeaturesPage({super.key, this.focus}); @override Widget build(BuildContext context) { - final selectedLanguage = LanguageSetting.supported.indexWhere((e) => e.languageCode == language.value.languageCode); const flavor = String.fromEnvironment('appFlavor'); + void navigateTo(Feature feature) { + context.pushNamed(Routes.featuresChoices, pathParameters: {'feature': feature.name}); + } + return Scaffold( appBar: AppBar(leading: const PopButton()), - body: ListView( - children: [ - const SizedBox(height: 8.0), - FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (context, snapshot) { - final info = snapshot.data; - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (info != null) Text('版本:${info.version}'), - const SizedBox(width: 8.0), - OutlinedText((kDebugMode ? '_' : '') + flavor.toUpperCase()), - ], - ); - }, - ), - const SizedBox(height: 8.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SignInButton( - signedInWidgetBuilder: (user) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('HI,${user?.displayName}'), - OutlinedButton( - key: const Key('feature.sign_out'), - onPressed: () async { - await Auth.instance.signOut(); - }, - child: const Text('登出'), - ), - ], - ), - ), - ), - ListTile( - key: const Key('feature.theme'), - leading: const Icon(Icons.palette_outlined), - title: Text(S.settingThemeTitle), - subtitle: Text(S.settingThemeTypes(theme.value.name)), - trailing: const Icon(Icons.arrow_forward_ios_sharp), - onTap: () => _buildChoiceList( - (index) => theme.update(ThemeMode.values[index]), - title: S.settingThemeTitle, - items: ThemeMode.values.map((e) => S.settingThemeTypes(e.name)).toList(), - selected: theme.value.index, - ), - ), - ListTile( - key: const Key('feature.language'), - leading: const Icon(Icons.language_outlined), - title: Text(S.settingLanguageTitle), - subtitle: Text(LanguageSetting.supportedNames[selectedLanguage]), - trailing: const Icon(Icons.arrow_forward_ios_sharp), - onTap: () => _buildChoiceList( - (index) => language.update(LanguageSetting.supported[index]), - title: S.settingLanguageTitle, - selected: selectedLanguage, - items: LanguageSetting.supportedNames, - ), - ), - const Divider(), - ListTile( - key: const Key('feature.outlook_order'), - leading: const Icon(Icons.library_books_outlined), - title: Text(S.settingOrderOutlookTitle), - subtitle: Text(S.settingOrderOutlookTypes(orderOutlook.value.name)), - trailing: const Icon(Icons.arrow_forward_ios_sharp), - onTap: () => _buildChoiceList( - (index) => orderOutlook.update(OrderOutlookTypes.values[index]), - title: S.settingOrderOutlookTitle, - selected: orderOutlook.value.index, - items: OrderOutlookTypes.values.map((e) => S.settingOrderOutlookTypes(e.name)).toList(), - tips: [ - '點餐時下方會有可拉動的面板,內含點餐中的資訊,適合小螢幕的手機', - '所有資訊顯示在單一螢幕中,適合大螢幕的平板', + body: ListView(children: [ + const SizedBox(height: 8.0), + FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + final info = snapshot.data; + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (info != null) Text(S.settingVersion(info.version)), + const SizedBox(width: 8.0), + OutlinedText((kDebugMode ? '_' : '') + flavor.toUpperCase()), ], - ), - ), - ListTile( - key: const Key('feature.checkout_warning'), - leading: const Icon(Icons.store_mall_directory_outlined), - title: Text(S.settingCheckoutWarningTitle), - subtitle: Text(S.settingCheckoutWarningTypes(checkoutWarning.value.name)), - trailing: const Icon(Icons.arrow_forward_ios_sharp), - onTap: () => _buildChoiceList( - (index) => checkoutWarning.update(CheckoutWarningTypes.values[index]), - title: S.settingCheckoutWarningTitle, - selected: checkoutWarning.value.index, - items: CheckoutWarningTypes.values.map((e) => S.settingCheckoutWarningTypes(e.name)).toList(), - tips: [ - '收銀機若使用小錢會出現提示,例如收銀機 5 塊錢不夠了並嘗試用 1 塊錢去找 5 塊錢', - null, - null, + ); + }, + ), + const SizedBox(height: 8.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: SignInButton( + signedInWidgetBuilder: (user) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(S.settingWelcome(user?.displayName ?? '')), + OutlinedButton( + key: const Key('feature.sign_out'), + onPressed: () async { + await Auth.instance.signOut(); + }, + child: Text(S.settingLogoutBtn), + ), ], ), ), - FeatureSlider( - sliderKey: const Key('feature.order_product_count'), - title: '點餐時每行顯示幾個產品', - value: orderCount.value, - max: 5, - minLabel: '純文字顯示', - hintText: '設定「零」則點餐時僅會以文字顯示', - onChanged: (value) => orderCount.update(value), + ), + ListTile( + key: const Key('feature.theme'), + leading: const Icon(Icons.palette_outlined), + title: Text(S.settingThemeTitle), + subtitle: Text(S.settingThemeName(ThemeSetting.instance.value.name)), + trailing: const Icon(Icons.arrow_forward_ios_sharp), + onTap: () => navigateTo(Feature.theme), + ), + ListTile( + key: const Key('feature.language'), + leading: const Icon(Icons.language_outlined), + title: Text(S.settingLanguageTitle), + subtitle: Text(LanguageSetting.instance.value.title), + trailing: const Icon(Icons.arrow_forward_ios_sharp), + onTap: () => navigateTo(Feature.language), + ), + const Divider(), + ListTile( + key: const Key('feature.order_outlook'), + leading: const Icon(Icons.library_books_outlined), + title: Text(S.settingOrderOutlookTitle), + subtitle: Text(S.settingOrderOutlookName(OrderOutlookSetting.instance.value.name)), + trailing: const Icon(Icons.arrow_forward_ios_sharp), + onTap: () => navigateTo(Feature.orderOutlook), + ), + ListTile( + key: const Key('feature.checkout_warning'), + leading: const Icon(Icons.store_mall_directory_outlined), + title: Text(S.settingCheckoutWarningTitle), + subtitle: Text(S.settingCheckoutWarningName(CheckoutWarningSetting.instance.value.name)), + trailing: const Icon(Icons.arrow_forward_ios_sharp), + onTap: () => navigateTo(Feature.checkoutWarning), + ), + // TODO: After using RWD, this feature is not necessary + FeatureSlider( + sliderKey: const Key('feature.order_product_count'), + title: S.settingOrderProductCountTitle, + value: OrderProductAxisCountSetting.instance.value, + max: 5, + autofocus: focus == 'orderProductCount', + minLabel: S.settingOrderProductCountMinLabel, + hintText: S.settingOrderProductCountHint, + onChanged: (value) => OrderProductAxisCountSetting.instance.update(value), + ), + ListTile( + leading: const Icon(Icons.remove_red_eye_outlined), + title: Text(S.settingOrderAwakeningTitle), + subtitle: Text(S.settingOrderAwakeningDescription), + trailing: FeatureSwitch( + key: const Key('feature.order_awakening'), + autofocus: focus == 'orderAwakening', + value: OrderAwakeningSetting.instance.value, + onChanged: (value) => OrderAwakeningSetting.instance.update(value), ), - ListTile( - leading: const Icon(Icons.remove_red_eye_outlined), - title: Text(S.settingOrderAwakeningTitle), - subtitle: const Text('是否根據系統設定時間關閉螢幕'), - trailing: FeatureSwitch( - key: const Key('feature.awake_ordering'), - value: orderAwakening.value, - onChanged: (value) => orderAwakening.update(value), - ), - ), - const Divider(), - ListTile( - leading: const Icon(Icons.report_outlined), - title: const Text('收集錯誤訊息和事件'), - subtitle: const Text('當應用程式發生錯誤時,寄送錯誤訊息,以幫助應用程式成長'), - trailing: FeatureSwitch( - key: const Key('feature.collect_events'), - value: collectEvents.value, - onChanged: (value) => collectEvents.update(value), - ), + ), + const Divider(), + ListTile( + leading: const Icon(Icons.report_outlined), + title: Text(S.settingReportTitle), + subtitle: Text(S.settingReportDescription), + trailing: FeatureSwitch( + key: const Key('feature.collect_events'), + autofocus: focus == 'collectEvents', + value: CollectEventsSetting.instance.value, + onChanged: (value) => CollectEventsSetting.instance.update(value), ), - ], - ), + ), + ]), ); } +} + +class ItemListScaffold extends StatelessWidget { + final Feature feature; - void _buildChoiceList( - Future Function(int) onChanged, { - required String title, - required List items, - required int selected, - List? tips, - }) async { - final newSelected = await Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ItemListScaffold( - title: title, - items: items, - selected: selected, - tips: tips, - )), + const ItemListScaffold({ + super.key, + required this.feature, + }); + + @override + Widget build(BuildContext context) { + final hintStyle = TextStyle(color: Theme.of(context).hintColor); + + final selected = feature.selected; + return Scaffold( + appBar: AppBar( + title: Text(feature.title), + leading: const PopButton(), + ), + body: ListView( + children: IterableZip([feature.itemTitles, feature.itemSubtitles]) + .mapIndexed((index, pair) => ListTile( + title: Text(pair[0]), + trailing: selected == index ? const Icon(Icons.check_sharp) : null, + subtitle: Text(pair[1], style: hintStyle), + onTap: () async { + if (selected != index) { + await feature.update(index); + } + }, + )) + .toList(), + ), ); + } +} + +enum Feature { + theme(), + language(), + orderOutlook(), + checkoutWarning(); + + const Feature(); + + Iterable get itemTitles { + switch (this) { + case Feature.theme: + return ThemeMode.values.map((e) => S.settingThemeName(e.name)); + case Feature.language: + return Language.values.map((e) => e.title); + case Feature.orderOutlook: + return OrderOutlookTypes.values.map((e) => S.settingOrderOutlookName(e.name)); + case Feature.checkoutWarning: + return CheckoutWarningTypes.values.map((e) => S.settingCheckoutWarningName(e.name)); + } + } + + Iterable get itemSubtitles { + switch (this) { + case Feature.theme: + return ThemeMode.values.map((e) => ''); + case Feature.language: + return Language.values.map((e) => ''); + case Feature.orderOutlook: + return OrderOutlookTypes.values.map((e) => S.settingOrderOutlookTip(e.name)); + case Feature.checkoutWarning: + return CheckoutWarningTypes.values.map((e) => S.settingCheckoutWarningTip(e.name)); + } + } + + String get title { + switch (this) { + case Feature.theme: + return S.settingThemeTitle; + case Feature.language: + return S.settingLanguageTitle; + case Feature.orderOutlook: + return S.settingOrderOutlookTitle; + case Feature.checkoutWarning: + return S.settingCheckoutWarningTitle; + } + } + + int get selected { + switch (this) { + case Feature.theme: + return ThemeSetting.instance.value.index; + case Feature.language: + return LanguageSetting.instance.value.index; + case Feature.orderOutlook: + return OrderOutlookSetting.instance.value.index; + case Feature.checkoutWarning: + return CheckoutWarningSetting.instance.value.index; + } + } - if (newSelected != null) { - await onChanged(newSelected); - setState(() {}); + Future update(int index) { + switch (this) { + case Feature.theme: + return ThemeSetting.instance.update(ThemeMode.values[index]); + case Feature.language: + return LanguageSetting.instance.update(Language.values[index]); + case Feature.orderOutlook: + return OrderOutlookSetting.instance.update(OrderOutlookTypes.values[index]); + case Feature.checkoutWarning: + return CheckoutWarningSetting.instance.update(CheckoutWarningTypes.values[index]); } } } diff --git a/lib/ui/home/home_page.dart b/lib/ui/home/home_page.dart index b5adfb9d..98437811 100644 --- a/lib/ui/home/home_page.dart +++ b/lib/ui/home/home_page.dart @@ -15,10 +15,10 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - goOrderPage() => context.pushNamed(Routes.order); - // 如果使用 stateful 並另外建立 tabController, - // 則會在 push page 時造成 Home 頁面重建, - // 進而導致底下的頁面也重建,可能造成 tutorial 重複出現。 + // Using DefaultTabController so descendant widgets can access the controller. + // This allow building constant tab views, otherwise after push page, + // the home page will rebuild(cause by go_route) and cause the tutorial to show again. + // see https://github.com/flutter/flutter/issues/132049 return DefaultTabController( length: 4, initialIndex: tab.index, @@ -26,9 +26,9 @@ class HomePage extends StatelessWidget { floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, floatingActionButton: FloatingActionButton.extended( key: const Key('home.order'), - onPressed: goOrderPage, + onPressed: () => context.pushNamed(Routes.order), icon: const Icon(Icons.store_sharp), - label: const Text('點餐'), + label: Text(S.orderBtn), ), body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { @@ -52,24 +52,11 @@ class HomePage extends StatelessWidget { // disable shadow after scrolled scrolledUnderElevation: 0, bottom: TabBar(tabs: [ - _Tab(key: const Key('home.analysis'), text: S.homeTabAnalysis), - _Tab(key: const Key('home.stock'), text: S.homeTabStock), - _Tab(key: const Key('home.cashier'), text: S.homeTabCashier), - _Tab(key: const Key('home.setting'), text: S.homeTabSetting), + _Tab(key: const Key('home.analysis'), text: S.analysisTab), + _Tab(key: const Key('home.stock'), text: S.stockTab), + _Tab(key: const Key('home.cashier'), text: S.cashierTab), + _Tab(key: const Key('home.setting'), text: S.settingTab), ]), - actions: [ - TextButton( - onPressed: goOrderPage, - child: const Text('點餐'), - ), - const Tooltip( - message: '未來這裡的按鈕將會移除,請使用右下角的點餐按鈕。', - triggerMode: TooltipTriggerMode.tap, - showDuration: Duration(seconds: 30), - margin: EdgeInsets.symmetric(horizontal: 16.0), - child: Icon(Icons.info_outline), - ) - ], ), ]; }, diff --git a/lib/ui/home/setting_view.dart b/lib/ui/home/setting_view.dart index e79df455..27b12b7b 100644 --- a/lib/ui/home/setting_view.dart +++ b/lib/ui/home/setting_view.dart @@ -4,12 +4,11 @@ import 'package:possystem/components/linkify.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/tutorial.dart'; import 'package:possystem/constants/app_themes.dart'; -import 'package:possystem/debug/random_gen_order.dart'; -import 'package:possystem/debug/rerun_migration.dart'; +import 'package:possystem/constants/constant.dart'; +import 'package:possystem/debug/debug_page.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/routes.dart'; -import 'package:possystem/services/cache.dart'; import 'package:possystem/translator.dart'; import 'package:provider/provider.dart'; import 'package:spotlight_ant/spotlight_ant.dart'; @@ -29,89 +28,82 @@ class _SettingViewState extends State with AutomaticKeepAliveClient @override Widget build(BuildContext context) { super.build(context); - const isProd = String.fromEnvironment('appFlavor') == 'prod'; return TutorialWrapper( tab: tab, child: ListView(padding: const EdgeInsets.only(bottom: 76), children: [ const _HeaderInfoList(), if (!isProd) - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row(children: [ - const RandomGenerateOrderButton(), - ElevatedButton.icon( - onPressed: Cache.instance.reset, - label: const Text('清除快取'), - icon: const Icon(Icons.clear_all_sharp), - ), - const RerunMigration(), - ]), + ListTile( + key: const Key('setting.debug'), + leading: const Icon(Icons.bug_report_sharp), + title: const Text('Debug'), + subtitle: const Text('For developer only'), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const DebugPage()), + ), ), Tutorial( id: 'home.menu', - index: 2, - message: '現在就趕緊來設定菜單吧!', + title: S.menuTutorialTitle, + message: S.menuTutorialContent, spotlightBuilder: const SpotlightRectBuilder(), disable: Menu.instance.isNotEmpty, + route: Routes.menu, child: _buildRouteTile( id: 'menu', icon: Icons.collections_sharp, route: Routes.menu, title: S.menuTitle, - subtitle: '產品種類、產品', + subtitle: S.menuSubtitle, ), ), Tutorial( id: 'home.exporter', - index: 1, - title: '資料轉移', - message: '這裡是用來匯入匯出菜單、庫存、訂單記錄等資訊的地方。', + title: S.transitTutorialTitle, + message: S.transitTutorialContent, spotlightBuilder: const SpotlightRectBuilder(), child: _buildRouteTile( id: 'exporter', icon: Icons.upload_file_sharp, route: Routes.transit, title: S.transitTitle, - subtitle: '匯入、匯出資料', + subtitle: S.transitDescription, ), ), Tutorial( id: 'home.order_attr', - index: 0, - title: '顧客設定', - message: '這裡可以設定顧客資訊,例如:\n' - '內用,加價一成;\n' - '外帶,維持原價。', + title: S.orderAttributeTutorialTitle, + message: S.orderAttributeTutorialContent, spotlightBuilder: const SpotlightRectBuilder(), child: _buildRouteTile( id: 'order_attrs', icon: Icons.assignment_ind_sharp, route: Routes.orderAttr, title: S.orderAttributeTitle, - subtitle: '內用、外帶等等', + subtitle: S.orderAttributeDescription, ), ), _buildRouteTile( id: 'quantity', icon: Icons.exposure_sharp, route: Routes.quantity, - title: S.quantityTitle, - subtitle: '半糖、微糖等等', + title: S.stockQuantityTitle, + subtitle: S.stockQuantityDescription, ), _buildRouteTile( id: 'feature_request', icon: Icons.lightbulb_sharp, route: Routes.featureRequest, - title: S.featureRequestTitle, - subtitle: '使用 Google 表單提供回饋', + title: S.settingElfTitle, + subtitle: S.settingElfDescription, ), _buildRouteTile( id: 'setting', icon: Icons.settings_sharp, route: Routes.features, - title: S.settingTitle, - subtitle: '外觀、語言、提示', + title: S.settingFeatureTitle, + subtitle: S.settingFeatureDescription, ), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( @@ -175,7 +167,7 @@ class _HeaderInfoList extends StatelessWidget { id: 'menu1', context: context, title: menu.items.fold(0, (v, e) => e.length + v), - subtitle: '產品', + subtitle: S.menuProductHeaderInfo, route: Routes.menu, query: {'mode': 'products'}, ), @@ -184,7 +176,7 @@ class _HeaderInfoList extends StatelessWidget { id: 'menu2', context: context, title: menu.length, - subtitle: '種類', + subtitle: S.menuCatalogHeaderInfo, route: Routes.menu, ), const SizedBox(width: 16), @@ -192,7 +184,7 @@ class _HeaderInfoList extends StatelessWidget { id: 'order_attrs', context: context, title: attrs.length, - subtitle: '顧客設定', + subtitle: S.orderAttributeHeaderInfo, route: Routes.orderAttr, ), ], diff --git a/lib/ui/home/widgets/feature_slider.dart b/lib/ui/home/widgets/feature_slider.dart index d055e077..035478f5 100644 --- a/lib/ui/home/widgets/feature_slider.dart +++ b/lib/ui/home/widgets/feature_slider.dart @@ -20,6 +20,8 @@ class FeatureSlider extends StatefulWidget { final Key? sliderKey; + final bool autofocus; + const FeatureSlider({ super.key, this.sliderKey, @@ -31,6 +33,7 @@ class FeatureSlider extends StatefulWidget { this.minLabel, this.maxLabel, this.hintText, + this.autofocus = false, }); @override @@ -55,6 +58,7 @@ class _FeatureSliderState extends State { ), Slider( key: widget.sliderKey, + autofocus: widget.autofocus, value: value.toDouble(), min: widget.min.toDouble(), max: widget.max.toDouble(), diff --git a/lib/ui/home/widgets/feature_switch.dart b/lib/ui/home/widgets/feature_switch.dart index 07b4a5e6..187d4d33 100644 --- a/lib/ui/home/widgets/feature_switch.dart +++ b/lib/ui/home/widgets/feature_switch.dart @@ -5,10 +5,13 @@ class FeatureSwitch extends StatefulWidget { final Function(bool) onChanged; + final bool autofocus; + const FeatureSwitch({ super.key, required this.value, required this.onChanged, + this.autofocus = false, }); @override @@ -22,6 +25,7 @@ class _FeatureSwitchState extends State { Widget build(BuildContext context) { return Switch( value: isEnable, + autofocus: widget.autofocus, onChanged: (value) { widget.onChanged(value); setState(() => isEnable = value); diff --git a/lib/ui/image_gallery_page.dart b/lib/ui/image_gallery_page.dart index d7fc6dc6..7e774453 100644 --- a/lib/ui/image_gallery_page.dart +++ b/lib/ui/image_gallery_page.dart @@ -45,7 +45,6 @@ class ImageGalleryPageState extends State { } Widget buildSelectingScaffold() { - final local = MaterialLocalizations.of(context); return Scaffold( appBar: AppBar( leading: CloseButton( @@ -58,16 +57,15 @@ class ImageGalleryPageState extends State { onPressed: () { DeleteDialog.show( context, - warningContent: Text('將會刪除 ${selectedImages.length} 個圖片\n' - '刪除之後會讓相關產品顯示不到圖片'), + warningContent: Text(S.imageGallerySelectionDeleteConfirm(selectedImages.length)), finishMessage: false, deleteCallback: deleteImages, ); }, - child: Text(local.deleteButtonTooltip), + child: Text(S.imageGalleryActionDelete), ), ], - title: const Text('刪除所選'), + title: Text(S.imageGallerySelectionTitle), ), body: buildBody(), ); @@ -77,11 +75,12 @@ class ImageGalleryPageState extends State { return Scaffold( appBar: AppBar( leading: const BackButton(), - title: const Text('圖片管理'), + title: Text(S.imageGalleryTitle), ), floatingActionButton: FloatingActionButton( key: const Key('image_gallery.add'), onPressed: createImage, + tooltip: S.imageGalleryActionCreate, child: const Icon(KIcons.add), ), body: buildBody(), @@ -97,7 +96,7 @@ class ImageGalleryPageState extends State { return Center( child: EmptyBody( onPressed: createImage, - helperText: '點擊開始匯入你的第一張照片!', + helperText: S.imageGalleryEmpty, ), ); } @@ -165,8 +164,9 @@ class ImageGalleryPageState extends State { await baseDir.create(); final imageList = await baseDir.list().map((e) => e.path).where((e) => !e.endsWith('-avator')).toList(); - // 因為照著時間產生的在最後面,但他應該在最前面,所以反序排列。 - // 除此之外,最新的圖片應該在最上面。 + // Because the image is generated by time, it should be at the front, + // so reverse the order. + // In another word, the newest image should be at the top. imageList.sort((a, b) => a.compareTo(b) * -1); setState(() => images = imageList); } @@ -177,10 +177,10 @@ class ImageGalleryPageState extends State { if (image != null) { // 2023-01-01T01:23:45.123 - // G20230101T012345123 + // 20230101T012345123 final name = DateTime.now().toIso8601String().replaceAll('-', '').replaceAll(':', '').replaceFirst('.', ''); - // 原本檔名是 uuid v4 產生,前綴為 [0-9A-F], - // 為了做區別而設計成這樣。 + // Default name is uuid v4, prefix with [0-9A-F] + // to avoid conflict with origin one we add 'g' prefix final dst = XFile.fs.path.join(baseDir.path, 'g$name'); Log.ger('save_image', 'start', dst); @@ -215,7 +215,7 @@ class ImageGalleryPageState extends State { } } catch (e) { if (mounted) { - showSnackBar(context, '有一個或多個圖片沒有刪成功。'); + showSnackBar(context, S.imageGallerySnackbarDeleteFailed); } Log.out(e.toString(), 'delete_image_error'); } finally { diff --git a/lib/ui/menu/menu_page.dart b/lib/ui/menu/menu_page.dart index 98d0c234..eae9473d 100644 --- a/lib/ui/menu/menu_page.dart +++ b/lib/ui/menu/menu_page.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:possystem/components/search_bar_wrapper.dart'; import 'package:possystem/components/style/empty_body.dart'; import 'package:possystem/components/style/pop_button.dart'; +import 'package:possystem/components/tutorial.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/models/menu/product.dart'; @@ -38,58 +39,53 @@ class _MenuPageState extends State { return PopScope( canPop: selected == null, onPopInvoked: _onPopInvoked, - child: Scaffold( - appBar: AppBar( - title: Text(selected?.name ?? S.menuTitle), - leading: PopButton( - onPressed: () { - if (_onPopInvoked(selected == null)) { - if (context.mounted && context.canPop()) { - context.pop(); + child: TutorialWrapper( + child: Scaffold( + appBar: AppBar( + title: Text(selected?.name ?? S.menuTitle), + leading: PopButton( + onPressed: () { + if (_onPopInvoked(selected == null)) { + if (context.mounted && context.canPop()) { + context.pop(); + } } - } - }, - ), - actions: [ - if (!widget.productOnly) - IconButton( - tooltip: selected == null ? S.menuCatalogReorder : S.menuProductReorder, - onPressed: () { - selected == null - ? context.pushNamed(Routes.menuReorder) - : context.pushNamed( - Routes.menuCatalogReorder, - pathParameters: {'id': selected!.id}, - ); - }, - icon: const Icon(KIcons.reorder), - ), - SearchBarWrapper( - key: const Key('menu.search'), - hintText: S.menuSearchProductHint, - initData: Menu.instance.searchProducts(), - search: (text) async => Menu.instance.searchProducts(text: text), - itemBuilder: _searchItemBuilder, - emptyBuilder: _searchEmptyBuilder, + }, ), - ], - ), - floatingActionButton: widget.productOnly - ? null - : FloatingActionButton( - key: const Key('menu.add'), - onPressed: _handleCreate, - tooltip: S.menuCatalogCreate, - child: const Icon(KIcons.add), + actions: [ + if (!widget.productOnly) + IconButton( + tooltip: selected == null ? S.menuCatalogTitleReorder : S.menuProductTitleReorder, + onPressed: () { + selected == null + ? context.pushNamed(Routes.menuReorder) + : context.pushNamed( + Routes.menuCatalogReorder, + pathParameters: {'id': selected!.id}, + ); + }, + icon: const Icon(KIcons.reorder), + ), + SearchBarWrapper( + key: const Key('menu.search'), + hintText: S.menuSearchHint, + initData: Menu.instance.searchProducts(), + search: (text) async => Menu.instance.searchProducts(text: text), + itemBuilder: _searchItemBuilder, + emptyBuilder: _searchEmptyBuilder, ), - body: PageView( - controller: controller, - // disable scrolling, only control by program - physics: const NeverScrollableScrollPhysics(), - children: [ - firstView, - secondView, - ], + ], + ), + floatingActionButton: widget.productOnly ? null : fab, + body: PageView( + controller: controller, + // disable scrolling, only control by program + physics: const NeverScrollableScrollPhysics(), + children: [ + firstView, + secondView, + ], + ), ), ), ); @@ -117,13 +113,27 @@ class _MenuPageState extends State { super.dispose(); } + Widget get fab { + return Tutorial( + id: 'add_menu', + disable: Menu.instance.isNotEmpty, + title: S.menuCatalogTutorialTitle, + message: S.menuCatalogEmptyBody, + route: Routes.menuNew, + child: FloatingActionButton( + key: const Key('menu.add'), + onPressed: _handleCreate, + tooltip: selected == null ? S.menuCatalogTitleCreate : S.menuProductTitleCreate, + child: const Icon(KIcons.add), + ), + ); + } + Widget get firstView { if (Menu.instance.isEmpty) { return Center( child: EmptyBody( - helperText: '我們會把相似「產品」放在「產品種類」中,\n到時候點餐會比較方便,例如:\n' - '「起司漢堡」、「蔬菜漢堡」整合進「漢堡」\n' - '「塑膠袋」、「環保杯」整合進「其他」', + helperText: S.menuCatalogEmptyBody, onPressed: _handleCreate, ), ); @@ -148,9 +158,7 @@ class _MenuPageState extends State { return Center( child: EmptyBody( key: const Key('catalog.empty'), - title: S.menuCatalogEmptyBody, - helperText: '「產品」是菜單裡的基本單位,例如:\n' - '「起司漢堡」、「可樂」', + helperText: S.menuProductEmptyBody, onPressed: _handleCreate, ), ); @@ -178,7 +186,7 @@ class _MenuPageState extends State { Widget _searchEmptyBuilder(BuildContext context, String text) { return ListTile( - title: Text(S.menuSearchProductNotFound), + title: Text(S.menuSearchNotFound), leading: const Icon(KIcons.warn), ); } diff --git a/lib/ui/menu/product_page.dart b/lib/ui/menu/product_page.dart index fe89096d..1b5ed0c9 100644 --- a/lib/ui/menu/product_page.dart +++ b/lib/ui/menu/product_page.dart @@ -3,9 +3,9 @@ import 'package:go_router/go_router.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/slivers/sliver_image_app_bar.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/components/style/empty_body.dart'; import 'package:possystem/components/style/hint_text.dart'; -import 'package:possystem/components/style/more_button.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/models/menu/product.dart'; import 'package:possystem/models/repository/quantities.dart'; @@ -35,7 +35,7 @@ class _ProductPageState extends State { floatingActionButton: FloatingActionButton( key: const Key('product.add'), onPressed: _handleCreateIng, - tooltip: S.menuIngredientCreate, + tooltip: S.menuIngredientTitleCreate, child: const Icon(KIcons.add), ), body: CustomScrollView(slivers: [ @@ -57,7 +57,7 @@ class _ProductPageState extends State { Widget get metadata { return SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(16.0), child: MetaBlock.withString(context, [ S.menuProductMetaTitle, S.menuProductMetaPrice(widget.product.price), @@ -72,9 +72,7 @@ class _ProductPageState extends State { return [ SliverToBoxAdapter( child: EmptyBody( - title: S.menuProductEmptyBody, - helperText: '你可以在產品中設定成分等資訊,例如:\n' - '「起司漢堡」有「起司」、「麵包」等成分', + helperText: S.menuIngredientEmptyBody, onPressed: _handleCreateIng, ), ) @@ -127,14 +125,14 @@ class _ProductPageState extends State { warningContent: Text(S.dialogDeletionContent(widget.product.name, '')), actions: >[ BottomSheetAction( - title: Text(S.menuProductUpdate), + title: Text(S.menuProductTitleUpdate), leading: const Icon(KIcons.modal), route: Routes.menuProductModal, routePathParameters: {'id': widget.product.id}, ), - const BottomSheetAction( - title: Text('更新照片'), - leading: Icon(KIcons.image), + BottomSheetAction( + title: Text(S.menuProductTitleUpdateImage), + leading: const Icon(KIcons.image), returnValue: _Action.changeImage, ), ], diff --git a/lib/ui/menu/widgets/catalog_modal.dart b/lib/ui/menu/widgets/catalog_modal.dart index 8f3fbce1..5d092d49 100644 --- a/lib/ui/menu/widgets/catalog_modal.dart +++ b/lib/ui/menu/widgets/catalog_modal.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; import 'package:possystem/components/style/image_holder.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/models/objects/menu_object.dart'; @@ -26,7 +26,7 @@ class _CatalogModalState extends State with ItemModal widget.catalog?.name ?? S.menuCatalogCreate; + String get title => widget.catalog?.name ?? S.menuCatalogTitleCreate; @override List buildFormFields() { @@ -53,7 +53,7 @@ class _CatalogModalState extends State with ItemModal items) => Menu.instance.reorderItems(items), ); } diff --git a/lib/ui/menu/widgets/menu_catalog_list.dart b/lib/ui/menu/widgets/menu_catalog_list.dart index 986054b6..874c3c67 100644 --- a/lib/ui/menu/widgets/menu_catalog_list.dart +++ b/lib/ui/menu/widgets/menu_catalog_list.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/slidable_item_list.dart'; -import 'package:possystem/components/style/more_button.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/routes.dart'; @@ -29,13 +29,13 @@ class MenuCatalogList extends StatelessWidget { warningContentBuilder: _warningContentBuilder, actionBuilder: (Catalog catalog) => >[ BottomSheetAction( - title: Text(S.menuCatalogUpdate), + title: Text(S.menuCatalogTitleUpdate), leading: const Icon(KIcons.modal), routePathParameters: {'id': catalog.id}, route: Routes.menuCatalogModal, ), BottomSheetAction( - title: Text(S.menuProductReorder), + title: Text(S.menuProductTitleReorder), leading: const Icon(KIcons.reorder), route: Routes.menuCatalogReorder, routePathParameters: {'id': catalog.id}, @@ -60,7 +60,7 @@ class MenuCatalogList extends StatelessWidget { subtitle: MetaBlock.withString( context, catalog.itemList.map((product) => product.name), - emptyText: S.menuCatalogListEmptyProduct, + emptyText: S.menuCatalogEmptyProducts, ), onLongPress: showActions, onTap: () => onSelected(catalog), @@ -68,8 +68,8 @@ class MenuCatalogList extends StatelessWidget { } Widget _warningContentBuilder(BuildContext context, Catalog catalog) { - final moreCtx = S.menuCatalogDialogDeletionContent(catalog.length); - return Text(S.dialogDeletionContent(catalog.name, moreCtx)); + final more = S.menuCatalogDialogDeletionContent(catalog.length); + return Text(S.dialogDeletionContent(catalog.name, '$more\n\n')); } } diff --git a/lib/ui/menu/widgets/menu_product_list.dart b/lib/ui/menu/widgets/menu_product_list.dart index d91e4bfb..4c62027d 100644 --- a/lib/ui/menu/widgets/menu_product_list.dart +++ b/lib/ui/menu/widgets/menu_product_list.dart @@ -3,7 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/slidable_item_list.dart'; -import 'package:possystem/components/style/more_button.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/models/menu/product.dart'; @@ -36,7 +36,7 @@ class MenuProductList extends StatelessWidget { Iterable> _actionBuilder(Product product) { return >[ BottomSheetAction( - title: Text(S.menuProductUpdate), + title: Text(S.menuProductTitleUpdate), leading: const Icon(KIcons.modal), route: Routes.menuProductModal, routePathParameters: {'id': product.id}, @@ -58,7 +58,7 @@ class MenuProductList extends StatelessWidget { subtitle: MetaBlock.withString( context, product.items.map((e) => e.name), - emptyText: S.menuProductListEmptyIngredient, + emptyText: S.menuProductEmptyIngredients, ), onLongPress: showActions, onTap: () => context.pushNamed( diff --git a/lib/ui/menu/widgets/product_ingredient_modal.dart b/lib/ui/menu/widgets/product_ingredient_modal.dart index e50d88cf..49cc9020 100644 --- a/lib/ui/menu/widgets/product_ingredient_modal.dart +++ b/lib/ui/menu/widgets/product_ingredient_modal.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; import 'package:possystem/components/search_bar_wrapper.dart'; -import 'package:possystem/components/style/more_button.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/menu/product.dart'; import 'package:possystem/models/menu/product_ingredient.dart'; @@ -37,7 +37,7 @@ class _ProductIngredientModalState extends State with It String ingredientName = ''; @override - String get title => widget.ingredient?.name ?? S.menuIngredientCreate; + String get title => widget.ingredient?.name ?? S.menuIngredientTitleCreate; @override List buildFormFields() { @@ -49,7 +49,6 @@ class _ProductIngredientModalState extends State with It text: ingredientName, labelText: S.menuIngredientSearchLabel, hintText: S.menuIngredientSearchHint, - // TODO: merge into one validator, and custom [initData] validator: Validator.textLimit(S.menuIngredientSearchLabel, 30), formValidator: _validateIngredient, initData: Stock.instance.itemList, @@ -139,11 +138,11 @@ class _ProductIngredientModalState extends State with It String? _validateIngredient(String? name) { if (ingredientId.isEmpty) { - return S.menuIngredientSearchEmptyError; + return S.menuIngredientSearchErrorEmpty; } if (widget.ingredient?.ingredient.id != ingredientId && widget.product.hasIngredient(ingredientId)) { - return S.menuIngredientRepeatError; + return S.menuIngredientSearchErrorRepeat; } return null; diff --git a/lib/ui/menu/widgets/product_ingredient_view.dart b/lib/ui/menu/widgets/product_ingredient_view.dart index c75d17d2..5fe0c5fe 100644 --- a/lib/ui/menu/widgets/product_ingredient_view.dart +++ b/lib/ui/menu/widgets/product_ingredient_view.dart @@ -2,12 +2,13 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/meta_block.dart'; -import 'package:possystem/components/style/more_button.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/components/style/slide_to_delete.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/models/menu/product_ingredient.dart'; import 'package:possystem/models/menu/product_quantity.dart'; import 'package:possystem/routes.dart'; +import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/translator.dart'; class ProductIngredientView extends StatelessWidget { @@ -30,7 +31,7 @@ class ProductIngredientView extends StatelessWidget { ListTile( key: Key('$key.add'), leading: const CircleAvatar(child: Icon(KIcons.add)), - title: Text(S.menuQuantityCreate), + title: Text(S.menuQuantityTitleCreate), onTap: () => context.pushNamed( Routes.menuProductDetails, pathParameters: {'id': ingredient.product.id}, @@ -52,7 +53,7 @@ class ProductIngredientView extends StatelessWidget { deleteValue: 0, actions: >[ BottomSheetAction( - title: Text(S.menuIngredientUpdate), + title: Text(S.menuIngredientTitleUpdate), leading: const Icon(KIcons.modal), route: Routes.menuProductDetails, routePathParameters: {'id': ingredient.product.id}, @@ -81,8 +82,8 @@ class _QuantityTile extends StatelessWidget { title: Text(quantity.name), subtitle: MetaBlock.withString(context, [ S.menuQuantityMetaAmount(quantity.amount), - S.menuQuantityMetaPrice(quantity.additionalPrice), - S.menuQuantityMetaCost(quantity.additionalCost), + S.menuQuantityMetaAdditionalPrice(quantity.additionalPrice.toCurrency()), + S.menuQuantityMetaAdditionalCost(quantity.additionalCost.toCurrency()), ]), onLongPress: () => BottomSheetActions.withDelete( context, diff --git a/lib/ui/menu/widgets/product_modal.dart b/lib/ui/menu/widgets/product_modal.dart index 254522ed..f1efb6ff 100644 --- a/lib/ui/menu/widgets/product_modal.dart +++ b/lib/ui/menu/widgets/product_modal.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; import 'package:possystem/components/style/image_holder.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/models/menu/product.dart'; @@ -36,7 +36,7 @@ class _ProductModalState extends State with ItemModal widget.product?.name ?? S.menuProductCreate; + String get title => widget.product?.name ?? S.menuProductTitleCreate; @override List buildFormFields() { @@ -63,7 +63,7 @@ class _ProductModalState extends State with ItemModal with ItemModal with ItemModal with ItemMo String quantityId = ''; @override - String get title => widget.quantity?.name ?? S.menuQuantityCreate; + String get title => widget.quantity?.name ?? S.menuQuantityTitleCreate; @override List buildFormFields() { @@ -187,10 +187,10 @@ class _ProductQuantityModalState extends State with ItemMo String? _validateQuantity(String? name) { if (quantityId.isEmpty) { - return S.menuQuantitySearchEmptyError; + return S.menuQuantitySearchErrorEmpty; } if (widget.quantity?.quantity.id != quantityId && widget.ingredient.hasQuantity(quantityId)) { - return S.menuQuantityRepeatError; + return S.menuQuantitySearchErrorRepeat; } return null; diff --git a/lib/ui/menu/widgets/product_reorder.dart b/lib/ui/menu/widgets/product_reorder.dart index 73c937b9..2081499d 100644 --- a/lib/ui/menu/widgets/product_reorder.dart +++ b/lib/ui/menu/widgets/product_reorder.dart @@ -13,7 +13,7 @@ class ProductReorder extends StatelessWidget { Widget build(BuildContext context) { return ReorderableScaffold( items: catalog.itemList, - title: S.menuProductReorder, + title: S.menuProductTitleReorder, handleSubmit: (List items) => catalog.reorderItems(items), ); } diff --git a/lib/ui/order/cart/cart_actions.dart b/lib/ui/order/cart/cart_actions.dart index fc858f02..2cf8789b 100644 --- a/lib/ui/order/cart/cart_actions.dart +++ b/lib/ui/order/cart/cart_actions.dart @@ -11,31 +11,31 @@ class CartActions extends StatelessWidget { BottomSheetAction( key: const Key('cart.action.discount'), leading: const Icon(Icons.loyalty_sharp), - title: Text(S.orderCartActionsDiscount), + title: Text(S.orderCartActionDiscount), returnValue: CartActionTypes.discount, ), BottomSheetAction( key: const Key('cart.action.price'), leading: const Icon(Icons.attach_money_sharp), - title: Text(S.orderCartActionsChangePrice), + title: Text(S.orderCartActionChangePrice), returnValue: CartActionTypes.price, ), BottomSheetAction( key: const Key('cart.action.count'), leading: const Icon(Icons.exposure_sharp), - title: Text(S.orderCartActionsChangeCount), + title: Text(S.orderCartActionChangeCount), returnValue: CartActionTypes.count, ), BottomSheetAction( key: const Key('cart.action.free'), leading: const Icon(Icons.free_breakfast_sharp), - title: Text(S.orderCartActionsFree), + title: Text(S.orderCartActionFree), returnValue: CartActionTypes.free, ), BottomSheetAction( key: const Key('cart.action.delete'), leading: const Icon(KIcons.delete), - title: Text(S.orderCartActionsDelete), + title: Text(S.orderCartActionDelete), returnValue: CartActionTypes.delete, ), ]; @@ -52,7 +52,7 @@ class CartActions extends StatelessWidget { ), ), onPressed: () => showActions(context), - child: Text(S.orderCartActionsBtn), + child: Text(S.orderCartActionBulkify), ); } @@ -62,14 +62,14 @@ class CartActions extends StatelessWidget { case CartActionTypes.discount: item = _DialogItem( validator: Validator.positiveInt( - S.orderCartActionsDiscountLabel, + S.orderCartActionDiscountLabel, maximum: 1000, ), decoration: InputDecoration( - hintText: S.orderCartActionsDiscountHint, - helperText: S.orderCartActionsDiscountHelper, + hintText: S.orderCartActionDiscountHint, + helperText: S.orderCartActionDiscountHelper, helperMaxLines: 4, - suffix: Text(S.orderCartActionsDiscountSuffix), + suffix: Text(S.orderCartActionDiscountSuffix), ), action: (result) { Cart.instance.selectedUpdateDiscount(int.tryParse(result)); @@ -78,10 +78,11 @@ class CartActions extends StatelessWidget { break; case CartActionTypes.price: item = _DialogItem( - validator: Validator.positiveNumber(S.orderCartActionsChangePriceLabel), + validator: Validator.positiveNumber(S.orderCartActionChangePriceLabel), decoration: InputDecoration( - hintText: S.orderCartActionsChangePriceHint, - suffix: Text(S.orderCartActionsChangePriceSuffix), + hintText: S.orderCartActionChangePriceHint, + prefix: Text(S.orderCartActionChangePricePrefix), + suffix: Text(S.orderCartActionChangePriceSuffix), ), action: (result) { Cart.instance.selectedUpdatePrice(num.tryParse(result)); @@ -91,14 +92,14 @@ class CartActions extends StatelessWidget { case CartActionTypes.count: item = _DialogItem( validator: Validator.positiveInt( - S.orderCartActionsChangeCountLabel, + S.orderCartActionChangeCountLabel, maximum: 10000, minimum: 1, ), decoration: InputDecoration( - hintText: S.orderCartActionsChangeCountHint, + hintText: S.orderCartActionChangeCountHint, helperMaxLines: 4, - suffix: Text(S.orderCartActionsChangeCountSuffix), + suffix: Text(S.orderCartActionChangeCountSuffix), ), action: (result) { Cart.instance.selectedUpdateCount(int.tryParse(result)); diff --git a/lib/ui/order/cart/cart_metadata_view.dart b/lib/ui/order/cart/cart_metadata_view.dart index 2bcda506..86bd1ee7 100644 --- a/lib/ui/order/cart/cart_metadata_view.dart +++ b/lib/ui/order/cart/cart_metadata_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/models/repository/cart.dart'; +import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/order/cart/cart_actions.dart'; import 'package:provider/provider.dart'; @@ -19,8 +20,8 @@ class CartMetadataView extends StatelessWidget { Expanded( key: const Key('cart.metadata'), child: MetaBlock.withString(context, [ - S.orderMetaTotalCount(cart.productCount), - S.orderMetaTotalPrice(cart.productsPrice), + S.orderCartMetaTotalCount(cart.productCount), + S.orderCartMetaTotalPrice(cart.productsPrice.toCurrency()), ])!, ), const SizedBox(width: 16.0), diff --git a/lib/ui/order/cart/cart_product_list.dart b/lib/ui/order/cart/cart_product_list.dart index ab736655..0cdcd88f 100644 --- a/lib/ui/order/cart/cart_product_list.dart +++ b/lib/ui/order/cart/cart_product_list.dart @@ -5,6 +5,7 @@ import 'package:possystem/components/style/slide_to_delete.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/models/order/cart_product.dart'; import 'package:possystem/models/repository/cart.dart'; +import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/translator.dart'; import 'package:provider/provider.dart'; @@ -123,20 +124,20 @@ class _CartProductListTile extends StatelessWidget { IconButton( key: Key('cart.product.$index.add'), icon: const Icon(KIcons.entryAdd), - tooltip: '數量加一', + tooltip: S.orderCartProductIncrease, onPressed: () { product.increment(); Cart.instance.priceChanged(); }, ), Text( - S.orderCartItemPrice(product.totalPrice), + S.orderCartProductPrice(product.totalPrice.toCurrency()), key: Key('cart.product.$index.price'), ), ], ); - final subtitle = product.quantities.map((e) => S.orderProductIngredientName( + final subtitle = product.quantities.map((e) => S.orderCartProductIngredient( e.ingredient.name, e.name, )); @@ -155,7 +156,7 @@ class _CartProductListTile extends StatelessWidget { subtitle, textOverflow: TextOverflow.visible, ) ?? - const HintText('預設份量'), + HintText(S.orderCartProductDefaultQuantity), trailing: trailing, onTap: () => Cart.instance.toggleAll(false, except: product), onLongPress: () { diff --git a/lib/ui/order/cart/cart_product_selector.dart b/lib/ui/order/cart/cart_product_selector.dart index 587e4481..2e7640c9 100644 --- a/lib/ui/order/cart/cart_product_selector.dart +++ b/lib/ui/order/cart/cart_product_selector.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:possystem/translator.dart'; +/// Select all or toggle all products in the cart. class CartProductSelector extends StatelessWidget { const CartProductSelector({super.key}); @override Widget build(BuildContext context) { - final local = MaterialLocalizations.of(context); return Row(children: [ const SizedBox(width: 16.0), Expanded( @@ -19,7 +19,7 @@ class CartProductSelector extends StatelessWidget { ), ), onPressed: () => Cart.instance.toggleAll(true), - child: Text(local.selectAllButtonLabel), + child: Text(S.orderCartActionSelectAll), ), ), const SizedBox(width: 4.0), @@ -32,7 +32,7 @@ class CartProductSelector extends StatelessWidget { ), ), onPressed: () => Cart.instance.toggleAll(null), - child: Text(S.orderCartToggleSelection), + child: Text(S.orderCartActionToggle), ), ), const SizedBox(width: 16.0), diff --git a/lib/ui/order/cart/cart_product_state_selector.dart b/lib/ui/order/cart/cart_product_state_selector.dart index b317b37f..ea17095a 100644 --- a/lib/ui/order/cart/cart_product_state_selector.dart +++ b/lib/ui/order/cart/cart_product_state_selector.dart @@ -72,14 +72,14 @@ class _CartProductStateSelectorState extends State { key: const Key('order.quantity.default'), onSelected: (_) => _changeQuantity(null), selected: quantityId == null, - label: Text(S.orderCartQuantityDefault(ingredient.amount)), + label: Text(S.orderCartQuantityDefaultLabel(ingredient.amount)), ), for (final q in ingredient.items) ChoiceChip( key: Key('order.quantity.${q.id}'), onSelected: (_) => _changeQuantity(q.id), selected: quantityId == q.id, - label: Text('${q.name}(${q.amount.toStringAsFixed(1)})'), + label: Text(S.orderCartQuantityLabel(q.name, q.amount)), ), ], ), diff --git a/lib/ui/order/cart/cart_snapshot.dart b/lib/ui/order/cart/cart_snapshot.dart index 3736bb04..ad84d2ce 100644 --- a/lib/ui/order/cart/cart_snapshot.dart +++ b/lib/ui/order/cart/cart_snapshot.dart @@ -1,12 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:possystem/components/scrollable_draggable_sheet.dart'; import 'package:possystem/components/style/hint_text.dart'; import 'package:possystem/components/style/outlined_text.dart'; import 'package:possystem/models/repository/cart.dart'; +import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/translator.dart'; import 'package:provider/provider.dart'; class CartSnapshot extends StatelessWidget { - const CartSnapshot({super.key}); + final ScrollableDraggableController controller; + + const CartSnapshot({super.key, required this.controller}); @override Widget build(BuildContext context) { @@ -26,18 +30,24 @@ class CartSnapshot extends StatelessWidget { itemCount: cart.products.length, itemBuilder: (context, index) { final product = cart.products[index]; - return OutlinedText( - product.name, - key: Key('cart_snapshot.$index'), - margin: const EdgeInsets.only(right: 8), - badge: product.count > 9 ? '9+' : product.count.toString(), + return GestureDetector( + onTap: () { + cart.toggleAll(false, except: product); + controller.jumpTo(controller.snapSizes[1]); + }, + child: OutlinedText( + product.name, + key: Key('cart_snapshot.$index'), + margin: const EdgeInsets.only(right: 8), + badge: product.count > 9 ? '9+' : product.count.toString(), + ), ); }, ), ), const SizedBox(width: 16.0), Text( - cart.productsPrice.toString(), + cart.productsPrice.toCurrency(), key: const Key('cart_snapshot.price'), style: const TextStyle(fontWeight: FontWeight.bold), ), diff --git a/lib/ui/order/cashier/order_setting_view.dart b/lib/ui/order/checkout/checkout_attribute_view.dart similarity index 83% rename from lib/ui/order/cashier/order_setting_view.dart rename to lib/ui/order/checkout/checkout_attribute_view.dart index d833f7bc..4362375c 100644 --- a/lib/ui/order/cashier/order_setting_view.dart +++ b/lib/ui/order/checkout/checkout_attribute_view.dart @@ -5,10 +5,10 @@ import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:possystem/models/repository/order_attributes.dart'; -class OderSettingView extends StatelessWidget { +class CheckoutAttributeView extends StatelessWidget { final ValueNotifier price; - const OderSettingView({ + const CheckoutAttributeView({ super.key, required this.price, }); @@ -18,24 +18,24 @@ class OderSettingView extends StatelessWidget { return ListView( padding: const EdgeInsets.fromLTRB(16, 8, 16, 428), children: [ - for (final item in OrderAttributes.instance.notEmptyItems) _OrderAttributeGroup(item, price), + for (final item in OrderAttributes.instance.notEmptyItems) _CheckoutAttributeGroup(item, price), ], ); } } -class _OrderAttributeGroup extends StatefulWidget { +class _CheckoutAttributeGroup extends StatefulWidget { final ValueNotifier price; final OrderAttribute attribute; - const _OrderAttributeGroup(this.attribute, this.price); + const _CheckoutAttributeGroup(this.attribute, this.price); @override - State<_OrderAttributeGroup> createState() => _OrderAttributeGroupState(); + State<_CheckoutAttributeGroup> createState() => _CheckoutAttributeGroupState(); } -class _OrderAttributeGroupState extends State<_OrderAttributeGroup> { +class _CheckoutAttributeGroupState extends State<_CheckoutAttributeGroup> { late String? selectedId; @override diff --git a/lib/ui/order/cashier/order_cashier_calculator.dart b/lib/ui/order/checkout/checkout_cashier_calculator.dart similarity index 92% rename from lib/ui/order/cashier/order_cashier_calculator.dart rename to lib/ui/order/checkout/checkout_cashier_calculator.dart index ec894f13..2d87155c 100644 --- a/lib/ui/order/cashier/order_cashier_calculator.dart +++ b/lib/ui/order/checkout/checkout_cashier_calculator.dart @@ -4,14 +4,14 @@ import 'package:possystem/translator.dart'; const _operators = ['+', '-', 'x']; -class OrderCashierCalculator extends StatefulWidget { +class CheckoutCashierCalculator extends StatefulWidget { final VoidCallback onSubmit; final ValueNotifier price; final ValueNotifier paid; - const OrderCashierCalculator({ + const CheckoutCashierCalculator({ super.key, required this.onSubmit, required this.price, @@ -19,10 +19,10 @@ class OrderCashierCalculator extends StatefulWidget { }); @override - State createState() => _OrderCashierCalculatorState(); + State createState() => _CheckoutCashierCalculatorState(); } -class _OrderCashierCalculatorState extends State { +class _CheckoutCashierCalculatorState extends State { final paidState = GlobalKey<_SingleFieldState>(); final changeState = GlobalKey<_SingleFieldState>(); @@ -41,7 +41,7 @@ class _OrderCashierCalculatorState extends State { _SingleField( key: paidState, id: 'cashier.calculator.paid', - prefix: S.orderCashierPaidLabel, + prefix: S.orderCheckoutCashierCalculatorLabelPaid, defaultText: widget.price.value.toCurrency(), errorText: '', ), @@ -49,9 +49,9 @@ class _OrderCashierCalculatorState extends State { _SingleField( key: changeState, id: 'cashier.calculator.change', - prefix: S.orderCashierChangeLabel, + prefix: S.orderCheckoutCashierCalculatorLabelChange, defaultText: '0', - errorText: S.orderCashierCalculatorChangeNotEnough, + errorText: S.orderCheckoutSnackbarPaidFailed, ), const Divider(), ]), @@ -156,7 +156,7 @@ class _OrderCashierCalculatorState extends State { final paid = _calc(value); final change = paid - widget.price.value; - changeText = change >= 0 ? change.toCurrency() : null; + changeText = change >= 0 ? change.toCurrencyLong() : null; widget.paid.value = paid; } else { widget.paid.value = widget.price.value; @@ -168,7 +168,7 @@ class _OrderCashierCalculatorState extends State { void _addOperator(String operator) { if (text.isNotEmpty) { - text = _calc(text).toCurrency() + operator; + text = _calc(text).toCurrencyLong() + operator; setState(() { isOperating = true; }); @@ -178,7 +178,7 @@ class _OrderCashierCalculatorState extends State { void _execCeil() { final price = _calc(text, widget.price.value.toInt()); final ceilPrice = CurrencySetting.instance.ceil(price); - text = ceilPrice.toCurrency(); + text = ceilPrice.toCurrencyLong(); } void _execBack() { @@ -202,7 +202,7 @@ class _OrderCashierCalculatorState extends State { setState(() { isOperating = false; }); - text = _calc(text).toCurrency(); + text = _calc(text).toCurrencyLong(); } else { widget.onSubmit(); } @@ -244,7 +244,7 @@ class _OrderCashierCalculatorState extends State { } _onNotify() { - text = _calc(widget.paid.value.toCurrency()).toCurrency(); + text = _calc(widget.paid.value.toCurrencyLong()).toCurrencyLong(); } } diff --git a/lib/ui/order/cashier/order_cashier_snapshot.dart b/lib/ui/order/checkout/checkout_cashier_snapshot.dart similarity index 86% rename from lib/ui/order/cashier/order_cashier_snapshot.dart rename to lib/ui/order/checkout/checkout_cashier_snapshot.dart index dd1cf5e9..8fa4dfa0 100644 --- a/lib/ui/order/cashier/order_cashier_snapshot.dart +++ b/lib/ui/order/checkout/checkout_cashier_snapshot.dart @@ -2,22 +2,22 @@ import 'package:flutter/material.dart'; import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/translator.dart'; -class OrderCashierSnapshot extends StatefulWidget { +class CheckoutCashierSnapshot extends StatefulWidget { final ValueNotifier price; final ValueNotifier paid; - const OrderCashierSnapshot({ + const CheckoutCashierSnapshot({ super.key, required this.price, required this.paid, }); @override - State createState() => _OrderCashierSnapshotState(); + State createState() => _CheckoutCashierSnapshotState(); } -class _OrderCashierSnapshotState extends State { +class _CheckoutCashierSnapshotState extends State { num? customValue; late num change; late List paidOptions; @@ -52,7 +52,7 @@ class _OrderCashierSnapshotState extends State { child: SizedBox( height: double.infinity, child: Center( - child: Text(S.orderCashierSnapshotChangeField(change)), + child: Text(S.orderCheckoutCashierSnapshotLabelChange(change.toCurrency())), ), ), ), diff --git a/lib/ui/order/cashier/stashed_order_list_view.dart b/lib/ui/order/checkout/stashed_order_list_view.dart similarity index 85% rename from lib/ui/order/cashier/stashed_order_list_view.dart rename to lib/ui/order/checkout/stashed_order_list_view.dart index d777e797..11e80e68 100644 --- a/lib/ui/order/cashier/stashed_order_list_view.dart +++ b/lib/ui/order/checkout/stashed_order_list_view.dart @@ -5,15 +5,15 @@ import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/dialog/confirm_dialog.dart'; import 'package:possystem/components/item_loader.dart'; import 'package:possystem/components/meta_block.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/components/style/hint_text.dart'; -import 'package:possystem/components/style/more_button.dart'; import 'package:possystem/components/style/snackbar.dart'; import 'package:possystem/models/objects/order_object.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/stashed_orders.dart'; import 'package:possystem/translator.dart'; -import 'package:possystem/ui/order/cashier/order_cashier_calculator.dart'; +import 'package:possystem/ui/order/checkout/checkout_cashier_calculator.dart'; import 'package:possystem/ui/order/order_page.dart'; class StashedOrderListView extends StatelessWidget { @@ -33,9 +33,9 @@ class StashedOrderListView extends StatelessWidget { ), metricsLoader: StashedOrders.instance.getMetrics, metricsBuilder: (metrics) { - return Center(child: Text(S.orderListMetaCount(metrics.count))); + return Center(child: Text(S.totalCount(metrics.count))); }, - emptyChild: const Center(child: HintText('目前無任何暫存餐點。')), + emptyChild: Center(child: HintText(S.orderCheckoutStashEmpty)), padding: const EdgeInsets.only(bottom: 428), ); } @@ -72,7 +72,7 @@ class StashedOrderListView extends StatelessWidget { child: ListTile( key: Key('stashed_order.${order.id}'), title: Text(title), - subtitle: MetaBlock.withString(context, products, emptyText: '沒有任何產品'), + subtitle: MetaBlock.withString(context, products, emptyText: S.orderCheckoutStashNoProducts), trailing: MoreButton(onPressed: () => _showActions(context, order)), onTap: () => _act(_Action.checkout, context, order), onLongPress: () => _showActions(context, order), @@ -83,20 +83,20 @@ class StashedOrderListView extends StatelessWidget { void _showActions(BuildContext context, OrderObject order) async { final action = await BottomSheetActions.withDelete( context, - actions: const [ + actions: [ BottomSheetAction( - title: Text('結帳'), - leading: Icon(Icons.price_check_sharp), + title: Text(S.orderCheckoutStashActionCheckout), + leading: const Icon(Icons.price_check_sharp), returnValue: _Action.checkout, ), BottomSheetAction( - title: Text('復原'), - leading: Icon(Icons.file_upload), + title: Text(S.orderCheckoutStashActionRestore), + leading: const Icon(Icons.file_upload), returnValue: _Action.restore, ), ], deleteValue: _Action.delete, - warningContent: Text(S.dialogDeletionContent('訂單', '')), + warningContent: Text(S.dialogDeletionContent(S.orderCheckoutStashDialogDeleteName, '')), deleteCallback: () => _act(_Action.delete, context, order), ); @@ -118,8 +118,8 @@ class StashedOrderListView extends StatelessWidget { if (!Cart.instance.isEmpty) { ok = await ConfirmDialog.show( context, - title: '復原暫存訂單?', - content: '此動作將會覆蓋掉現在購物車內的訂單', + title: S.orderCheckoutStashDialogRestoreTitle, + content: S.orderCheckoutStashDialogRestoreContent, ); } @@ -163,11 +163,11 @@ class StashedOrderListView extends StatelessWidget { vertical: 16.0, horizontal: 8.0, ), - semanticLabel: '結帳計算機', + semanticLabel: S.orderCheckoutStashDialogCalculator, children: [ SizedBox( height: 360.0, - child: OrderCashierCalculator( + child: CheckoutCashierCalculator( onSubmit: () => Navigator.of(context).pop(true), price: price, paid: paid, @@ -181,7 +181,7 @@ class StashedOrderListView extends StatelessWidget { final status = await cart.checkout(price.value, paid.value); if (status == CheckoutStatus.paidNotEnough) { if (context.mounted) { - showSnackBar(context, S.orderCashierPaidFailed); + showSnackBar(context, S.orderCheckoutSnackbarPaidFailed); } return; } diff --git a/lib/ui/order/cashier/order_details_page.dart b/lib/ui/order/order_checkout_page.dart similarity index 83% rename from lib/ui/order/cashier/order_details_page.dart rename to lib/ui/order/order_checkout_page.dart index 6e3777da..b4ee8330 100644 --- a/lib/ui/order/cashier/order_details_page.dart +++ b/lib/ui/order/order_checkout_page.dart @@ -7,12 +7,12 @@ import 'package:possystem/components/style/snackbar.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/translator.dart'; -import 'package:possystem/ui/order/cashier/order_cashier_calculator.dart'; -import 'package:possystem/ui/order/cashier/order_cashier_snapshot.dart'; -import 'package:possystem/ui/order/cashier/stashed_order_list_view.dart'; +import 'package:possystem/ui/order/checkout/checkout_cashier_calculator.dart'; +import 'package:possystem/ui/order/checkout/checkout_cashier_snapshot.dart'; +import 'package:possystem/ui/order/checkout/stashed_order_list_view.dart'; import 'package:possystem/ui/order/widgets/order_object_view.dart'; -import 'order_setting_view.dart'; +import 'checkout/checkout_attribute_view.dart'; class OrderDetailsPage extends StatefulWidget { const OrderDetailsPage({super.key}); @@ -53,12 +53,12 @@ class _OrderDetailsPageState extends State with SingleTickerPr ), ), onPressed: _stash, - child: const Text('暫存'), + child: Text(S.orderCheckoutActionStash), ), TextButton( key: const Key('order.details.confirm'), onPressed: _checkout, - child: Text(MaterialLocalizations.of(context).okButtonLabel), + child: Text(S.orderCheckoutActionConfirm), ), ], // disable shadow after scrolled @@ -75,8 +75,8 @@ class _OrderDetailsPageState extends State with SingleTickerPr Widget buildBody(BuildContext context) { if (Cart.instance.isEmpty) { return TabBarView(controller: _controller, children: [ - if (hasAttr) OderSettingView(price: price), - const Center(child: HintText('請先進行點單。')), + if (hasAttr) CheckoutAttributeView(price: price), + Center(child: HintText(S.orderCheckoutEmptyCart)), const StashedOrderListView(), ]); } @@ -86,9 +86,8 @@ class _OrderDetailsPageState extends State with SingleTickerPr child: GestureDetector( onTap: () => draggableController?.reset(), child: TabBarView(controller: _controller, children: [ - if (hasAttr) OderSettingView(price: price), + if (hasAttr) CheckoutAttributeView(price: price), ValueListenableBuilder( - key: const Key('evan'), valueListenable: paid, builder: (context, value, child) => OrderObjectView( order: Cart.instance.toObject(paid: value), @@ -110,14 +109,14 @@ class _OrderDetailsPageState extends State with SingleTickerPr height: snapshotHeight, baseline: -2 * snapshotHeight, valueScalar: -1, - child: OrderCashierSnapshot(price: price, paid: paid), + child: CheckoutCashierSnapshot(price: price, paid: paid), ), Expanded( child: SingleChildScrollView( controller: scroll, child: SizedBox( height: calculatorHeight, - child: OrderCashierCalculator( + child: CheckoutCashierCalculator( onSubmit: _checkout, price: price, paid: paid, @@ -156,9 +155,9 @@ class _OrderDetailsPageState extends State with SingleTickerPr ); tabs = [ - if (hasAttr) Tab(key: const Key('order.details.attr'), text: S.orderSetAttributeTitle), - Tab(key: const Key('order.details.order'), text: S.orderCashierTitle), - const Tab(key: Key('order.details.stashed'), text: '暫存訂單'), + if (hasAttr) Tab(key: const Key('order.details.attr'), text: S.orderCheckoutAttributeTab), + Tab(key: const Key('order.details.order'), text: S.orderCheckoutCashierTab), + Tab(key: const Key('order.details.stashed'), text: S.orderCheckoutStashTab), ]; } @@ -176,7 +175,7 @@ class _OrderDetailsPageState extends State with SingleTickerPr // send success message if (mounted) { if (status == CheckoutStatus.paidNotEnough) { - showSnackBar(context, S.orderCashierPaidFailed); + showSnackBar(context, S.orderCheckoutSnackbarPaidFailed); } else if (context.canPop()) { context.pop(status); } diff --git a/lib/ui/order/order_page.dart b/lib/ui/order/order_page.dart index da8d6a4b..dd08b050 100644 --- a/lib/ui/order/order_page.dart +++ b/lib/ui/order/order_page.dart @@ -9,7 +9,6 @@ import 'package:possystem/routes.dart'; import 'package:possystem/settings/checkout_warning.dart'; import 'package:possystem/settings/order_awakening_setting.dart'; import 'package:possystem/settings/order_outlook_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/order/cart/cart_metadata_view.dart'; import 'package:possystem/ui/order/cart/cart_product_list.dart'; @@ -53,7 +52,7 @@ class _OrderPageState extends State { }, ); - final outlook = SettingsProvider.of(); + final outlook = OrderOutlookSetting.instance.value; return TutorialWrapper( child: Scaffold( @@ -66,11 +65,11 @@ class _OrderPageState extends State { TextButton( key: const Key('order.checkout'), onPressed: () => _handleCheckout(), - child: Text(S.orderActionsCheckout), + child: Text(S.orderActionCheckout), ), ], ), - body: outlook.value == OrderOutlookTypes.slidingPanel + body: outlook == OrderOutlookTypes.slidingPanel ? DraggableSheetView( row1: orderCatalogListView, row2: orderProductListView, @@ -107,7 +106,7 @@ class _OrderPageState extends State { @override void initState() { - if (SettingsProvider.of().value) { + if (OrderAwakeningSetting.instance.value) { Wakelock.enable(); } // rebind menu/attributes if changed @@ -128,7 +127,7 @@ class _OrderPageState extends State { } void handleCheckoutStatus(BuildContext context, CheckoutStatus status) { - status = SettingsProvider.of().shouldShow(status); + status = CheckoutWarningSetting.instance.shouldShow(status); switch (status) { case CheckoutStatus.ok: @@ -137,13 +136,13 @@ void handleCheckoutStatus(BuildContext context, CheckoutStatus status) { showSnackBar(context, S.actSuccess); break; case CheckoutStatus.cashierNotEnough: - showSnackBar(context, S.orderCashierPaidNotEnough); + showSnackBar(context, S.orderSnackbarCashierNotEnough); break; case CheckoutStatus.cashierUsingSmall: showMoreInfoSnackBar( context, - S.orderCashierPaidUsingSmallMoney, - Text(S.orderCashierPaidUsingSmallMoneyHint), + S.orderSnackbarCashierUsingSmallMoney, + Text(S.orderSnackbarCashierUsingSmallMoneyHelper(Routes.getRoute('features/checkoutWarning'))), ); break; default: diff --git a/lib/ui/order/widgets/draggable_sheet_view.dart b/lib/ui/order/widgets/draggable_sheet_view.dart index dd8c25b9..f350c860 100644 --- a/lib/ui/order/widgets/draggable_sheet_view.dart +++ b/lib/ui/order/widgets/draggable_sheet_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/scrollable_draggable_sheet.dart'; import 'package:possystem/components/tutorial.dart'; import 'package:possystem/models/repository/cart.dart'; +import 'package:possystem/routes.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/order/cart/cart_snapshot.dart'; import 'package:spotlight_ant/spotlight_ant.dart'; @@ -78,8 +79,7 @@ class _DraggableSheetViewState extends State { height: snapshotHeight, baselineSize: -2 * controller.snapSizes[0], valueScalar: -1, - // TODO: wrap with gesture detector to go up when tap - child: const CartSnapshot(), + child: CartSnapshot(controller: controller), ), FixedHeightClipper( controller: controller, @@ -92,7 +92,7 @@ class _DraggableSheetViewState extends State { id: 'order.sliding_collapsed', padding: const EdgeInsets.fromLTRB(-4, snapshotHeight + DraggableIndicator.height, -4, 0), title: S.orderCartSnapshotTutorialTitle, - message: S.orderCartSnapshotTutorialMessage, + message: S.orderCartSnapshotTutorialContent(Routes.getRoute('features/orderOutlook')), spotlightBuilder: const SpotlightRectBuilder(borderRadius: 16), child: widget.row3_2Builder(scroll, scrollable), ), @@ -123,7 +123,6 @@ class _DraggableSheetViewState extends State { controller = ScrollableDraggableController(const [ snapshotHeight, base, - // base + buttonHeight * 2, 1.0, ]); diff --git a/lib/ui/order/widgets/order_actions.dart b/lib/ui/order/widgets/order_actions.dart index 17379863..48d34c06 100644 --- a/lib/ui/order/widgets/order_actions.dart +++ b/lib/ui/order/widgets/order_actions.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; -import 'package:possystem/components/style/more_button.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/components/style/snackbar.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:possystem/routes.dart'; @@ -18,22 +18,22 @@ class OrderActions extends StatelessWidget { context, actions: [ BottomSheetAction( - key: const Key('order.action.changer'), - title: Text(S.orderActionsOpenChanger), + key: const Key('order.action.exchange'), + title: Text(S.orderActionExchange), leading: const Icon(Icons.change_circle_sharp), returnValue: const OrderAction(route: Routes.cashierChanger), ), BottomSheetAction( key: const Key('order.action.stash'), - title: Text(S.orderActionsStash), + title: Text(S.orderActionStash), leading: const Icon(Icons.file_download_sharp), returnValue: OrderAction(action: () => _stash(context)), ), - const BottomSheetAction( - key: Key('order.action.history'), - title: Text('訂單記錄'), - leading: Icon(Icons.history_sharp), - returnValue: OrderAction(route: Routes.history), + BottomSheetAction( + key: const Key('order.action.history'), + title: Text(S.orderActionReview), + leading: const Icon(Icons.history_sharp), + returnValue: const OrderAction(route: Routes.history), ), ], ); diff --git a/lib/ui/order/widgets/order_catalog_list_view.dart b/lib/ui/order/widgets/order_catalog_list_view.dart index e095b6e7..838b1665 100644 --- a/lib/ui/order/widgets/order_catalog_list_view.dart +++ b/lib/ui/order/widgets/order_catalog_list_view.dart @@ -30,7 +30,7 @@ class _OrderCatalogListViewState extends State { return SingleRowWrap(children: [ ChoiceChip( selected: false, - label: Text(S.orderCartEmptyCatalog), + label: Text(S.orderCatalogListEmpty), ), ]); } diff --git a/lib/ui/order/widgets/order_object_view.dart b/lib/ui/order/widgets/order_object_view.dart index 71cb42de..870be988 100644 --- a/lib/ui/order/widgets/order_object_view.dart +++ b/lib/ui/order/widgets/order_object_view.dart @@ -21,26 +21,26 @@ class OrderObjectView extends StatelessWidget { @override Widget build(BuildContext context) { final priceWidget = ExpansionTile( - title: Text(S.orderObjectTotalPrice(order.price.toCurrency())), + title: Text(S.orderObjectViewPriceTotal(order.price.toCurrency())), children: [ HeadTailTile( - head: S.orderObjectProductsPrice, + head: S.orderObjectViewPriceProducts, tail: order.productsPrice.toCurrency(), ), HeadTailTile( - head: S.orderObjectAttributesPrice, + head: S.orderObjectViewPriceAttributes, tail: order.attributesPrice.toCurrency(), ), HeadTailTile( - head: S.orderObjectProductsCost, + head: S.orderObjectViewCost, tail: order.cost.toCurrency(), ), HeadTailTile( - head: S.orderObjectRevenue, - tail: order.revenue.toCurrency(), + head: S.orderObjectViewProfit, + tail: order.profit.toCurrency(), ), HeadTailTile( - head: S.orderObjectPaid, + head: S.orderObjectViewPaid, tail: order.paid.toCurrency(), ), ], @@ -50,18 +50,15 @@ class OrderObjectView extends StatelessWidget { ? const SizedBox.shrink() : ExpansionTile( key: const Key('order.attributes'), - title: Text(S.orderObjectAttributeTitle), + title: Text(S.orderObjectViewDividerAttribute), subtitle: Text( - S.orderObjectAttributeCount(order.attributes.length), + S.totalCount(order.attributes.length), ), children: [ for (final attribute in order.attributes) ListTile( title: Text(attribute.name.toString()), - subtitle: OrderAttributeValueWidget( - attribute.mode, - attribute.modeValue, - ), + subtitle: OrderAttributeValueWidget.build(attribute.mode, attribute.modeValue), trailing: OutlinedText(attribute.optionName.toString()), ), ], @@ -71,8 +68,8 @@ class OrderObjectView extends StatelessWidget { child: Column(children: [ priceWidget, attrWidget, - TextDivider(label: S.orderObjectProductTitle), - HintText(S.orderObjectProductsCount(order.productsCount)), + TextDivider(label: S.orderObjectViewDividerProduct), + HintText(S.totalCount(order.productsCount)), for (final product in order.products) _ProductTile(product), // padding for ScrollableDraggableSheet on OrderDetailsPage const SizedBox(height: 428), @@ -91,8 +88,8 @@ class _ProductTile extends StatelessWidget { return ExpansionTile( title: Text(data.productName), subtitle: MetaBlock.withString(context, [ - '${S.orderObjectProductPrice}:${data.totalPrice.toCurrency()}', - '${S.orderObjectProductCost}:${data.totalCost.toCurrency()}', + '${S.orderObjectViewProductPrice}:${data.totalPrice.toCurrency()}', + '${S.orderObjectViewProductCost}:${data.totalCost.toCurrency()}', ]), leading: Menu.instance.getProductByName(data.productName)?.avator ?? (data.productName != '' @@ -104,35 +101,35 @@ class _ProductTile extends StatelessWidget { childrenPadding: const EdgeInsets.all(8.0), children: [ HeadTailTile( - head: S.orderObjectProductPrice, + head: S.orderObjectViewProductPrice, tail: data.totalPrice.toCurrency(), ), HeadTailTile( - head: S.orderObjectProductCost, + head: S.orderObjectViewProductCost, tail: data.totalCost.toCurrency(), ), HeadTailTile( - head: S.orderObjectProductCount, + head: S.orderObjectViewProductCount, tail: data.count.toString(), ), HeadTailTile( - head: S.orderObjectProductSinglePrice, + head: S.orderObjectViewProductSinglePrice, tail: data.singlePrice.toCurrency(), ), HeadTailTile( - head: S.orderObjectProductOriginalPrice, + head: S.orderObjectViewProductOriginalPrice, tail: data.originalPrice.toCurrency(), ), HeadTailTile( - head: S.orderObjectProductCatalog, + head: S.orderObjectViewProductCatalog, tail: data.catalogName, ), if (data.ingredients.isNotEmpty) const SizedBox(height: 8.0), - if (data.ingredients.isNotEmpty) HeadTailTile(head: S.orderObjectProductIngredient, tail: ''), + if (data.ingredients.isNotEmpty) HeadTailTile(head: S.orderObjectViewProductIngredient, tail: ''), for (final e in data.ingredients) HeadTailTile( head: e.ingredientName, - tailWidget: e.quantityName == null ? const HintText('預設') : null, + tailWidget: e.quantityName == null ? HintText(S.orderObjectViewProductDefaultQuantity) : null, tail: e.quantityName, ), ], diff --git a/lib/ui/order/widgets/order_product_list_view.dart b/lib/ui/order/widgets/order_product_list_view.dart index 9925bc00..275cb0c0 100644 --- a/lib/ui/order/widgets/order_product_list_view.dart +++ b/lib/ui/order/widgets/order_product_list_view.dart @@ -4,8 +4,9 @@ import 'package:possystem/components/tutorial.dart'; import 'package:possystem/constants/constant.dart'; import 'package:possystem/models/menu/product.dart'; import 'package:possystem/models/repository/cart.dart'; +import 'package:possystem/routes.dart'; import 'package:possystem/settings/order_product_axis_count_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; +import 'package:possystem/translator.dart'; import 'package:spotlight_ant/spotlight_ant.dart'; class OrderProductListView extends StatelessWidget { @@ -18,7 +19,7 @@ class OrderProductListView extends StatelessWidget { @override Widget build(BuildContext context) { - final count = SettingsProvider.of().value; + final count = OrderProductAxisCountSetting.instance.value; int index = 0; return Padding( @@ -43,10 +44,8 @@ class OrderProductListView extends StatelessWidget { for (final product in products) Tutorial( id: 'order.menu_product', - title: '開始點餐!', - message: '透過圖片點餐更方便!\n' - '你也可以到「設定」頁面,\n' - '設定「每行顯示幾個產品」或僅使用文字點餐', + title: S.orderProductListTutorialTitle, + message: S.orderProductListTutorialContent(Routes.getRoute('features?f=orderProductCount')), spotlightBuilder: const SpotlightRectBuilder(borderRadius: 16), disable: index++ != 0, child: ImageHolder( diff --git a/lib/ui/order_attr/order_attribute_page.dart b/lib/ui/order_attr/order_attribute_page.dart index a05f514d..e41c1fa6 100644 --- a/lib/ui/order_attr/order_attribute_page.dart +++ b/lib/ui/order_attr/order_attribute_page.dart @@ -26,7 +26,7 @@ class OrderAttributePage extends StatelessWidget { actions: [ IconButton( key: const Key('order_attributes.reorder'), - tooltip: S.orderAttributeReorder, + tooltip: S.orderAttributeTitleReorder, onPressed: () => context.pushNamed(Routes.orderAttrReorder), icon: const Icon(KIcons.reorder), ), @@ -34,14 +34,14 @@ class OrderAttributePage extends StatelessWidget { ), floatingActionButton: FloatingActionButton( onPressed: handleCreate, - tooltip: S.orderAttributeCreate, + tooltip: S.orderAttributeTitleCreate, child: const Icon(KIcons.add), ), body: attrs.isEmpty ? Center( child: EmptyBody( onPressed: handleCreate, - helperText: S.orderAttributeHint, + helperText: S.orderAttributeEmptyBody, )) : OrderAttributeList(attrs.itemList), ); diff --git a/lib/ui/order_attr/widgets/order_attribute_list.dart b/lib/ui/order_attr/widgets/order_attribute_list.dart index 7bbe1298..8e4a0433 100644 --- a/lib/ui/order_attr/widgets/order_attribute_list.dart +++ b/lib/ui/order_attr/widgets/order_attribute_list.dart @@ -3,8 +3,8 @@ import 'package:go_router/go_router.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/models/order_attribute_value_widget.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/components/style/hint_text.dart'; -import 'package:possystem/components/style/more_button.dart'; import 'package:possystem/components/style/outlined_text.dart'; import 'package:possystem/components/style/slide_to_delete.dart'; import 'package:possystem/constants/icons.dart'; @@ -42,23 +42,38 @@ class _OrderAttributeCard extends StatelessWidget { @override Widget build(BuildContext context) { final attr = context.watch(); - final mode = S.orderAttributeModeNames(attr.mode.name); - final defaultName = attr.defaultOption?.name ?? S.orderAttributeMetaNoDefault; + final mode = S.orderAttributeModeName(attr.mode.name); final key = 'order_attributes.${attr.id}'; + final theme = Theme.of(context); return ExpansionTile( key: Key(key), title: Text(attr.name), - subtitle: MetaBlock.withString(context, [ - S.orderAttributeMetaMode(mode), - S.orderAttributeMetaDefault(defaultName), - ]), + subtitle: RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan( + children: [ + TextSpan(text: S.orderAttributeMetaMode(mode)), + MetaBlock.span(), + attr.defaultOption?.name != null + ? TextSpan(text: S.orderAttributeMetaDefault(attr.defaultOption!.name)) + : TextSpan(text: S.orderAttributeMetaDefault(''), children: [ + TextSpan( + text: S.orderAttributeMetaNoDefault, + style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor), + ), + ]), + ], + // disable parent text style + style: theme.textTheme.bodyMedium, + ), + ), expandedCrossAxisAlignment: CrossAxisAlignment.stretch, children: [ ListTile( key: Key('$key.add'), leading: const CircleAvatar(child: Icon(KIcons.add)), - title: Text(S.orderAttributeOptionCreate), + title: Text(S.orderAttributeOptionTitleCreate), onTap: () => context.pushNamed( Routes.orderAttrNew, queryParameters: {'id': attr.id}, @@ -79,13 +94,13 @@ class _OrderAttributeCard extends StatelessWidget { deleteValue: 0, actions: >[ BottomSheetAction( - title: Text(S.orderAttributeUpdate), + title: Text(S.orderAttributeTitleUpdate), leading: const Icon(KIcons.modal), route: Routes.orderAttrModal, routePathParameters: {'id': attr.id}, ), BottomSheetAction( - title: Text(S.orderAttributeOptionReorder), + title: Text(S.orderAttributeOptionTitleReorder), leading: const Icon(KIcons.reorder), route: Routes.orderAttrOptionReorder, routePathParameters: {'id': attr.id}, @@ -111,8 +126,8 @@ class _OptionTile extends StatelessWidget { child: ListTile( key: Key('order_attributes.${option.repository.id}.${option.id}'), title: Text(option.name), - subtitle: OrderAttributeValueWidget(option.mode, option.modeValue), - trailing: option.isDefault ? OutlinedText(S.orderAttributeOptionIsDefault) : null, + subtitle: OrderAttributeValueWidget.build(option.mode, option.modeValue), + trailing: option.isDefault ? OutlinedText(S.orderAttributeOptionMetaDefault) : null, onLongPress: () => BottomSheetActions.withDelete( context, deleteValue: 0, diff --git a/lib/ui/order_attr/widgets/order_attribute_modal.dart b/lib/ui/order_attr/widgets/order_attribute_modal.dart index 391d4325..ffd7d069 100644 --- a/lib/ui/order_attr/widgets/order_attribute_modal.dart +++ b/lib/ui/order_attr/widgets/order_attribute_modal.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/choice_chip_with_help.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; import 'package:possystem/components/style/text_divider.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/objects/order_attribute_object.dart'; @@ -26,7 +26,7 @@ class _OrderAttributeModalState extends State with ItemModa final modeSelector = GlobalKey>(); @override - String get title => widget.attribute?.name ?? S.orderAttributeCreate; + String get title => widget.attribute?.name ?? S.orderAttributeTitleCreate; @override List buildFormFields() { @@ -49,18 +49,18 @@ class _OrderAttributeModalState extends State with ItemModa focusNode: _nameFocusNode, validator: (name) { return widget.attribute?.name != name && OrderAttributes.instance.hasName(name) - ? S.orderAttributeNameRepeatError + ? S.orderAttributeNameErrorRepeat : null; }, ), )), - TextDivider(label: S.orderAttributeModeTitle), + TextDivider(label: S.orderAttributeModeDivider), ChoiceChipWithHelp( key: modeSelector, values: OrderAttributeMode.values, selected: widget.isNew ? OrderAttributeMode.statOnly : widget.attribute!.mode, - labels: OrderAttributeMode.values.map((e) => S.orderAttributeModeNames(e.name)), - helpTexts: OrderAttributeMode.values.map((e) => S.orderAttributeModeDescriptions(e.name)).toList(), + labels: OrderAttributeMode.values.map((e) => S.orderAttributeModeName(e.name)), + helpTexts: OrderAttributeMode.values.map((e) => S.orderAttributeModeHelper(e.name)).toList(), ), ]; } diff --git a/lib/ui/order_attr/widgets/order_attribute_option_modal.dart b/lib/ui/order_attr/widgets/order_attribute_option_modal.dart index 7f9d2fd1..ff4dc816 100644 --- a/lib/ui/order_attr/widgets/order_attribute_option_modal.dart +++ b/lib/ui/order_attr/widgets/order_attribute_option_modal.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:possystem/components/dialog/confirm_dialog.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; import 'package:possystem/components/style/hint_text.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/objects/order_attribute_object.dart'; @@ -35,13 +35,13 @@ class _OrderAttributeModalState extends State with It late bool isDefault; @override - String get title => widget.option?.name ?? S.orderAttributeOptionCreateTitle(widget.attribute.name); + String get title => widget.option?.name ?? S.orderAttributeOptionTitleCreateWith(widget.attribute.name); @override List buildFormFields() { - final label = S.orderAttributeModeNames(widget.attribute.mode.name); - final helper = S.orderAttributeOptionsModeHelper(widget.attribute.mode.name); - final hint = S.orderAttributeOptionsModeHint(widget.attribute.mode.name); + final label = S.orderAttributeModeName(widget.attribute.mode.name); + final helper = S.orderAttributeOptionModeHelper(widget.attribute.mode.name); + final hint = S.orderAttributeOptionModeHint(widget.attribute.mode.name); final validator = widget.attribute.mode == OrderAttributeMode.changeDiscount ? Validator.positiveInt( label, @@ -74,7 +74,7 @@ class _OrderAttributeModalState extends State with It focusNode: _nameFocusNode, validator: (name) { return widget.option?.name != name && widget.attribute.hasName(name) - ? S.orderAttributeOptionNameRepeatError + ? S.orderAttributeOptionNameErrorRepeat : null; }, ), @@ -85,7 +85,7 @@ class _OrderAttributeModalState extends State with It value: isDefault, selected: isDefault, onChanged: _toggledDefault, - title: Text(S.orderAttributeOptionSetToDefault), + title: Text(S.orderAttributeOptionToDefaultLabel), ), const SizedBox(height: 12.0), p(widget.attribute.shouldHaveModeValue @@ -170,10 +170,8 @@ class _OrderAttributeModalState extends State with It if (value == true && defaultOption != null && defaultOption.id != widget.option?.id) { final confirmed = await ConfirmDialog.show( context, - title: S.orderAttributeOptionConfirmChangeDefaultTitle, - content: S.orderAttributeOptionConfirmChangeDefaultContent( - defaultOption.name, - ), + title: S.orderAttributeOptionToDefaultConfirmChangeTitle, + content: S.orderAttributeOptionToDefaultConfirmChangeContent(defaultOption.name), ); if (confirmed) { diff --git a/lib/ui/order_attr/widgets/order_attribute_option_reorder.dart b/lib/ui/order_attr/widgets/order_attribute_option_reorder.dart index 5dd3d6e5..ec717fa3 100644 --- a/lib/ui/order_attr/widgets/order_attribute_option_reorder.dart +++ b/lib/ui/order_attr/widgets/order_attribute_option_reorder.dart @@ -16,7 +16,7 @@ class OrderAttributeOptionReorder extends StatelessWidget { Widget build(BuildContext context) { return ReorderableScaffold( items: attribute.itemList, - title: S.orderAttributeOptionReorder, + title: S.orderAttributeOptionTitleReorder, handleSubmit: (items) => attribute.reorderItems(items), ); } diff --git a/lib/ui/order_attr/widgets/order_attribute_reorder.dart b/lib/ui/order_attr/widgets/order_attribute_reorder.dart index 7b0df8dd..c6178422 100644 --- a/lib/ui/order_attr/widgets/order_attribute_reorder.dart +++ b/lib/ui/order_attr/widgets/order_attribute_reorder.dart @@ -11,7 +11,7 @@ class OrderAttributeReorder extends StatelessWidget { Widget build(BuildContext context) { return ReorderableScaffold( items: OrderAttributes.instance.itemList, - title: S.orderAttributeReorder, + title: S.orderAttributeTitleReorder, handleSubmit: (List items) => OrderAttributes.instance.reorderItems(items), ); } diff --git a/lib/ui/stock/quantity_page.dart b/lib/ui/stock/quantity_page.dart index e18d7a44..275eee53 100644 --- a/lib/ui/stock/quantity_page.dart +++ b/lib/ui/stock/quantity_page.dart @@ -22,20 +22,20 @@ class QuantityPage extends StatelessWidget { final body = quantities.isEmpty ? Center( child: EmptyBody( - helperText: '份量可以快速調整成分的量,例如:\n半糖、微糖。', + helperText: S.stockQuantityEmptyBody, onPressed: handleCreate, )) : StockQuantityList(quantities: quantities.itemList); return Scaffold( appBar: AppBar( - title: Text(S.quantityTitle), + title: Text(S.stockQuantityTitle), leading: const PopButton(), ), floatingActionButton: FloatingActionButton( key: const Key('quantity.add'), onPressed: handleCreate, - tooltip: S.menuQuantityCreate, + tooltip: S.stockQuantityTitleCreate, child: const Icon(KIcons.add), ), body: body, diff --git a/lib/ui/stock/replenishment_page.dart b/lib/ui/stock/replenishment_page.dart index 6f752337..16359684 100644 --- a/lib/ui/stock/replenishment_page.dart +++ b/lib/ui/stock/replenishment_page.dart @@ -2,54 +2,63 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/slidable_item_list.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/components/style/empty_body.dart'; -import 'package:possystem/components/style/more_button.dart'; import 'package:possystem/components/style/pop_button.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/models/repository/replenisher.dart'; import 'package:possystem/models/stock/replenishment.dart'; import 'package:possystem/routes.dart'; import 'package:possystem/translator.dart'; -import 'package:provider/provider.dart'; -class ReplenishmentPage extends StatefulWidget { +class ReplenishmentPage extends StatelessWidget { const ReplenishmentPage({super.key}); - @override - State createState() => _ReplenishmentPageState(); -} - -class _ReplenishmentPageState extends State { @override Widget build(BuildContext context) { + void goToCreate() => context.pushNamed(Routes.replenishmentNew); + return Scaffold( appBar: AppBar( - title: Text(S.stockReplenishmentTitle), + title: Text(S.stockReplenishmentTitleList), leading: const PopButton(), ), floatingActionButton: FloatingActionButton( key: const Key('replenisher.add'), onPressed: goToCreate, - tooltip: S.stockReplenishmentCreate, + tooltip: S.stockReplenishmentTitleCreate, child: const Icon(KIcons.add), ), - body: body, + body: ListenableBuilder( + listenable: Replenisher.instance, + builder: (_, __) { + if (Replenisher.instance.isEmpty) { + return Center( + child: EmptyBody( + onPressed: goToCreate, + helperText: S.stockReplenishmentEmptyBody, + ), + ); + } + + return buildList(context); + }, + ), ); } - @override - void didChangeDependencies() { - context.watch(); - super.didChangeDependencies(); - } + Widget buildList(BuildContext context) { + void handler(Replenishment item, _Actions action) async { + if (action == _Actions.apply) { + final confirmed = await context.pushNamed( + Routes.replenishmentApply, + pathParameters: {'id': item.id}, + ); - Widget get body { - if (Replenisher.instance.isEmpty) { - return Center( - child: EmptyBody( - onPressed: goToCreate, - helperText: '採購可以幫你快速調整成分的庫存', - )); + if (confirmed == true && context.mounted && context.canPop()) { + context.pop(true); + } + } } return SlidableItemList( @@ -62,51 +71,32 @@ class _ReplenishmentPageState extends State { items: Replenisher.instance.itemList, actionBuilder: (item) => [ BottomSheetAction( - title: const Text('編輯採購'), + title: Text(S.stockReplenishmentTitleUpdate), leading: const Icon(KIcons.edit), route: Routes.replenishmentModal, routePathParameters: {'id': item.id}, ), - const BottomSheetAction( - key: Key('apply'), - title: Text('套用採購'), - leading: Icon(Icons.check_circle_outline_sharp), + BottomSheetAction( + key: const Key('apply'), + title: Text(S.stockReplenishmentApplyButton), + leading: const Icon(Icons.check_circle_outline_sharp), returnValue: _Actions.apply, ), ], - handleAction: handleAction, - tileBuilder: (context, item, index, showActions) => ListTile( - key: Key('replenisher.${item.id}'), - title: Text(item.name), - subtitle: Text(S.stockReplenishmentSubtitle(item.data.length)), - onTap: () => handleAction(item, _Actions.apply), - onLongPress: showActions, - trailing: EntryMoreButton(onPressed: showActions), - ), + handleAction: handler, + tileBuilder: (context, item, index, showActions) { + return ListTile( + key: Key('replenisher.${item.id}'), + title: Text(item.name), + subtitle: Text(S.stockReplenishmentMetaAffect(item.data.length)), + onTap: () => handler(item, _Actions.apply), + onLongPress: showActions, + trailing: EntryMoreButton(onPressed: showActions), + ); + }, ), ); } - - void goToCreate() { - context.pushNamed(Routes.replenishmentNew); - } - - void handleAction(Replenishment item, _Actions action) { - if (action == _Actions.apply) { - handleApply(item); - } - } - - Future handleApply(Replenishment item) async { - final confirmed = await context.pushNamed( - Routes.replenishmentApply, - pathParameters: {'id': item.id}, - ); - - if (mounted && confirmed == true && context.canPop()) { - context.pop(true); - } - } } enum _Actions { diff --git a/lib/ui/stock/stock_view.dart b/lib/ui/stock/stock_view.dart index 58bdc58a..1c41732d 100644 --- a/lib/ui/stock/stock_view.dart +++ b/lib/ui/stock/stock_view.dart @@ -31,7 +31,7 @@ class _StockViewState extends State with AutomaticKeepAliveClientMixi return Center( key: const Key('stock.empty'), child: EmptyBody( - helperText: '新增成份後,就可以開始追蹤這些成份的庫存囉!', + helperText: S.stockIngredientEmptyBody, onPressed: () => context.pushNamed(Routes.ingredientNew), ), ); @@ -39,33 +39,37 @@ class _StockViewState extends State with AutomaticKeepAliveClientMixi return TutorialWrapper( tab: tab, - child: ListView(padding: const EdgeInsets.only(bottom: 76), children: [ + child: ListView(padding: const EdgeInsets.only(bottom: 76, top: 16), children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Tutorial( - id: 'stock.replenishment', - index: 1, - title: S.stockReplenishmentTutorialTitle, - message: S.stockReplenishmentTutorialMessage, - child: RouteCircularButton( - key: const Key('stock.replenisher'), - icon: Icons.shopping_basket_sharp, - route: Routes.replenishment, - popTrueShowSuccess: true, - text: S.stockReplenishmentButton, + Expanded( + child: Tutorial( + id: 'stock.replenishment', + index: 1, + title: S.stockReplenishmentTutorialTitle, + message: S.stockReplenishmentTutorialContent, + child: RouteCircularButton( + key: const Key('stock.replenisher'), + icon: Icons.shopping_basket_sharp, + route: Routes.replenishment, + popTrueShowSuccess: true, + text: S.stockReplenishmentButton, + ), ), ), - const SizedBox.square(dimension: 96.0), - Tutorial( - id: 'stock.add', - index: 0, - disable: Stock.instance.isNotEmpty, - title: S.stockIngredientAddTutorialTitle, - message: S.stockIngredientAddTutorialMessage, - child: RouteCircularButton( - key: const Key('stock.add'), - route: Routes.ingredientNew, - icon: KIcons.add, - text: S.stockIngredientCreate, + const Spacer(), + Expanded( + child: Tutorial( + id: 'stock.add', + index: 0, + disable: Stock.instance.isNotEmpty, + title: S.stockIngredientTutorialTitle, + message: S.stockIngredientTutorialContent, + child: RouteCircularButton( + key: const Key('stock.add'), + route: Routes.ingredientNew, + icon: KIcons.add, + text: S.stockIngredientTitleCreate, + ), ), ), ]), diff --git a/lib/ui/stock/widgets/replenishment_apply.dart b/lib/ui/stock/widgets/replenishment_apply.dart index e10477f7..3adc45e4 100644 --- a/lib/ui/stock/widgets/replenishment_apply.dart +++ b/lib/ui/stock/widgets/replenishment_apply.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:possystem/components/style/card_info_text.dart'; import 'package:possystem/models/stock/replenishment.dart'; +import 'package:possystem/translator.dart'; class ReplenishmentApply extends StatelessWidget { final Replenishment item; @@ -22,17 +23,17 @@ class ReplenishmentApply extends StatelessWidget { context.pop(true); } }, - child: const Text('套用'), + child: Text(S.stockReplenishmentApplyConfirmButton), ), ], ), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8.0), child: ListView(children: [ - const CardInfoText(child: Text('選擇套用後,將會影響以下成分的庫存')), - DataTable(columns: const [ - DataColumn(label: Text('名稱')), - DataColumn(numeric: true, label: Text('數量')) + CardInfoText(child: Text(S.stockReplenishmentApplyConfirmHint)), + DataTable(columns: [ + DataColumn(label: Text(S.stockReplenishmentApplyConfirmColumn('name'))), + DataColumn(numeric: true, label: Text(S.stockReplenishmentApplyConfirmColumn('amount'))) ], rows: [ for (final entry in item.ingredientData.entries) DataRow(cells: [ diff --git a/lib/ui/stock/widgets/replenishment_modal.dart b/lib/ui/stock/widgets/replenishment_modal.dart index b1ca5edb..813daf89 100644 --- a/lib/ui/stock/widgets/replenishment_modal.dart +++ b/lib/ui/stock/widgets/replenishment_modal.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; +import 'package:possystem/components/style/hint_text.dart'; import 'package:possystem/components/style/text_divider.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/objects/stock_object.dart'; @@ -29,7 +30,7 @@ class _ReplenishmentModalState extends State with ItemModal< late FocusNode _nameFocusNode; @override - String get title => widget.replenishment?.name ?? S.stockReplenishmentCreate; + String get title => widget.replenishment?.name ?? S.stockReplenishmentTitleCreate; @override List buildFormFields() { @@ -55,12 +56,13 @@ class _ReplenishmentModalState extends State with ItemModal< focusNode: _nameFocusNode, validator: (name) { return widget.replenishment?.name != name && Replenisher.instance.hasName(name) - ? S.stockReplenishmentNameRepeatError + ? S.stockReplenishmentNameErrorRepeat : null; }, ), )), - TextDivider(label: S.stockReplenishmentIngredientListTitle), + TextDivider(label: S.stockReplenishmentIngredientsDivider), + HintText(S.stockReplenishmentIngredientsHelper), for (final ing in Stock.instance.itemList) _buildIngredientField(ing), ]; } diff --git a/lib/ui/stock/widgets/stock_ingredient_list.dart b/lib/ui/stock/widgets/stock_ingredient_list.dart index 7f7f09f1..ff61e729 100644 --- a/lib/ui/stock/widgets/stock_ingredient_list.dart +++ b/lib/ui/stock/widgets/stock_ingredient_list.dart @@ -25,7 +25,7 @@ class StockIngredientList extends StatelessWidget { children: [ Center( child: HintText([ - updatedAt == null ? S.stockHasNotReplenishEver : S.stockUpdatedAt(updatedAt), + updatedAt == null ? S.stockReplenishmentNever : S.stockUpdatedAt(updatedAt), S.totalCount(ingredients.length), ].join(MetaBlock.string)), ), @@ -64,7 +64,7 @@ class _IngredientTile extends StatelessWidget { onTap: () => editAmount(context), trailing: IconButton( key: Key('stock.${ingredient.id}.edit'), - tooltip: '編輯成分', + tooltip: S.stockIngredientTitleUpdate, onPressed: () => editIngredient(context), icon: const Icon(KIcons.edit), ), @@ -85,17 +85,17 @@ class _IngredientTile extends StatelessWidget { final result = await BottomSheetActions.withDelete<_Actions>( context, deleteValue: _Actions.delete, - warningContent: Text(S.dialogDeletionContent(ingredient.name, more)), + warningContent: Text(S.dialogDeletionContent(ingredient.name, '$more\n\n')), deleteCallback: delete, actions: [ - const BottomSheetAction( - title: Text('編輯庫存'), - leading: Icon(Icons.edit_square), + BottomSheetAction( + title: Text(S.stockIngredientTitleUpdateAmount), + leading: const Icon(Icons.edit_square), returnValue: _Actions.edit, ), BottomSheetAction( key: const Key('btn.edit'), - title: const Text('編輯成分'), + title: Text(S.stockIngredientTitleUpdate), leading: const Icon(KIcons.edit), route: Routes.ingredientModal, routePathParameters: {'id': ingredient.id}, @@ -122,7 +122,7 @@ class _IngredientTile extends StatelessWidget { max: ingredient.maxAmount, decoration: InputDecoration( label: Text(S.stockIngredientAmountLabel), - helperText: '若沒有設定最大庫存量,增加庫存會重設最大值。', + helperText: S.stockIngredientAmountShortHelper, helperMaxLines: 3, ), validator: Validator.positiveNumber(S.stockIngredientAmountLabel), diff --git a/lib/ui/stock/widgets/stock_ingredient_modal.dart b/lib/ui/stock/widgets/stock_ingredient_modal.dart index 3dc42f84..6ef1c065 100644 --- a/lib/ui/stock/widgets/stock_ingredient_modal.dart +++ b/lib/ui/stock/widgets/stock_ingredient_modal.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; import 'package:possystem/components/style/text_divider.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/menu/product.dart'; @@ -32,7 +32,7 @@ class _StockIngredientModalState extends State with ItemMo late FocusNode _totalAmountFocusNode; @override - String get title => widget.ingredient?.name ?? S.stockIngredientCreate; + String get title => widget.ingredient?.name ?? S.stockIngredientTitleCreate; @override Widget buildForm() { @@ -54,10 +54,7 @@ class _StockIngredientModalState extends State with ItemMo ); case 1: return TextDivider( - label: S.stockIngredientConnectedProductsCount( - length - 2, - widget.ingredient!.name, - ), + label: S.stockIngredientProductsCount(length - 2), ); default: final product = ingredients[index - 2].product; @@ -92,7 +89,7 @@ class _StockIngredientModalState extends State with ItemMo focusNode: _nameFocusNode, validator: (name) { return widget.ingredient?.name != name && Stock.instance.hasName(name) - ? S.stockIngredientNameRepeatError + ? S.stockIngredientNameErrorRepeat : null; }, ), @@ -120,13 +117,13 @@ class _StockIngredientModalState extends State with ItemMo keyboardType: TextInputType.number, focusNode: _totalAmountFocusNode, decoration: InputDecoration( - labelText: S.stockIngredientTotalAmountLabel, - helperText: S.stockIngredientTotalAmountHelper, + labelText: S.stockIngredientAmountMaxLabel, + helperText: S.stockIngredientAmountMaxHelper, helperMaxLines: 5, filled: false, ), validator: Validator.positiveNumber( - S.stockIngredientTotalAmountLabel, + S.stockIngredientAmountMaxLabel, allowNull: true, focusNode: _totalAmountFocusNode, ), diff --git a/lib/ui/stock/widgets/stock_quantity_list.dart b/lib/ui/stock/widgets/stock_quantity_list.dart index 9ad5be1a..bc971fa5 100644 --- a/lib/ui/stock/widgets/stock_quantity_list.dart +++ b/lib/ui/stock/widgets/stock_quantity_list.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/slidable_item_list.dart'; -import 'package:possystem/components/style/more_button.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/stock/quantity.dart'; @@ -26,7 +26,7 @@ class StockQuantityList extends StatelessWidget { actionBuilder: (quantity) => [ BottomSheetAction( key: const Key('btn.edit'), - title: const Text('編輯份量'), + title: Text(S.menuQuantityTitleUpdate), leading: const Icon(KIcons.edit), route: Routes.quantityModal, routePathParameters: {'id': quantity.id}, @@ -50,7 +50,7 @@ class StockQuantityList extends StatelessWidget { return ListTile( key: Key('quantities.${quantity.id}'), title: Text(quantity.name), - subtitle: Text(S.quantityMetaProportion(quantity.defaultProportion)), + subtitle: Text(S.stockQuantityMetaProportion(quantity.defaultProportion)), trailing: EntryMoreButton(onPressed: showActions), onLongPress: showActions, onTap: () => context.pushNamed( @@ -62,8 +62,8 @@ class StockQuantityList extends StatelessWidget { Widget _warningContentBuilder(BuildContext context, Quantity quantity) { final count = Menu.instance.getQuantities(quantity.id).length; - final moreCtx = S.quantityDialogDeletionContent(count); + final more = S.stockQuantityDialogDeletionContent(count); - return Text(S.dialogDeletionContent(quantity.name, moreCtx)); + return Text(S.dialogDeletionContent(quantity.name, '$more\n\n')); } } diff --git a/lib/ui/stock/widgets/stock_quantity_modal.dart b/lib/ui/stock/widgets/stock_quantity_modal.dart index f722311f..f6940752 100644 --- a/lib/ui/stock/widgets/stock_quantity_modal.dart +++ b/lib/ui/stock/widgets/stock_quantity_modal.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/models/objects/stock_object.dart'; import 'package:possystem/models/repository/quantities.dart'; @@ -25,7 +25,7 @@ class _StockQuantityModalState extends State with ItemModal< late FocusNode _proportionFocusNode; @override - String get title => widget.quantity?.name ?? S.quantityCreate; + String get title => widget.quantity?.name ?? S.stockQuantityTitleCreate; @override List buildFormFields() { @@ -36,19 +36,19 @@ class _StockQuantityModalState extends State with ItemModal< textCapitalization: TextCapitalization.words, textInputAction: TextInputAction.next, decoration: InputDecoration( - labelText: S.quantityNameLabel, - hintText: S.quantityNameHint, + labelText: S.stockQuantityNameLabel, + hintText: S.stockQuantityNameHint, filled: false, ), maxLength: 30, focusNode: _nameFocusNode, validator: Validator.textLimit( - S.quantityNameLabel, + S.stockQuantityNameLabel, 30, focusNode: _nameFocusNode, validator: (name) { return widget.quantity?.name != name && Quantities.instance.hasName(name) - ? S.quantityNameRepeatError + ? S.stockQuantityNameErrorRepeat : null; }, ), @@ -61,14 +61,14 @@ class _StockQuantityModalState extends State with ItemModal< focusNode: _proportionFocusNode, onFieldSubmitted: handleFieldSubmit, decoration: InputDecoration( - labelText: S.quantityProportionLabel, - helperText: S.quantityProportionHelper, + labelText: S.stockQuantityProportionLabel, + helperText: S.stockQuantityProportionHelper, helperMaxLines: 100, filled: false, ), // NOTE: do we need maximum? validator: Validator.positiveNumber( - S.quantityProportionLabel, + S.stockQuantityProportionLabel, maximum: 100, allowNull: true, focusNode: _proportionFocusNode, diff --git a/lib/ui/transit/google_sheet/export_basic_view.dart b/lib/ui/transit/google_sheet/export_basic_view.dart index 4467cddc..af9f5b63 100644 --- a/lib/ui/transit/google_sheet/export_basic_view.dart +++ b/lib/ui/transit/google_sheet/export_basic_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/sign_in_button.dart'; import 'package:possystem/components/style/snackbar.dart'; -import 'package:possystem/components/style/launcher_snackbar_action.dart'; +import 'package:possystem/components/style/snackbar_actions.dart'; import 'package:possystem/components/style/text_divider.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/helpers/exporter/google_sheet_exporter.dart'; @@ -49,24 +49,24 @@ class _ExportBasicViewState extends State { exporter: widget.exporter, notifier: widget.notifier, cacheKey: _cacheKey, - existLabel: '指定匯出', - existHint: '匯出至試算表「%name」', - emptyLabel: '建立匯出', - emptyHint: '建立新的試算表「${S.transitBasicTitle}」,並把資料匯出至此', - defaultName: S.transitBasicTitle, + existLabel: S.transitGSSpreadsheetExportExistLabel, + existHint: S.transitGSSpreadsheetExportExistHint, + emptyLabel: S.transitGSSpreadsheetExportEmptyLabel, + emptyHint: S.transitGSSpreadsheetExportEmptyHint(S.transitGSSpreadsheetModelDefaultName), + defaultName: S.transitGSSpreadsheetModelDefaultName, requiredSheetTitles: requiredSheetTitles, onUpdated: onSpreadsheetUpdate, onPrepared: exportData, ), ), ), - const TextDivider(label: '選擇欲匯出的種類'), + TextDivider(label: S.transitGSSpreadsheetModelExportDivider), for (final sheet in sheets) SheetNamer( prop: sheet, action: (prop) => previewData(prop.type), actionIcon: KIcons.preview, - actionTitle: S.transitPreviewExportTitle, + actionTitle: S.transitExportPreviewTitle, ), ]); } @@ -81,7 +81,7 @@ class _ExportBasicViewState extends State { SheetType.replenisher, SheetType.orderAttr, ].map((e) { - final name = Cache.instance.get('$_cacheKey.${e.name}') ?? S.transitType(e.name); + final name = Cache.instance.get('$_cacheKey.${e.name}') ?? S.transitModelName(e.name); final data = Formatter.getTarget(Formatter.nameToFormattable(e.name)); return SheetNamerProperties( @@ -112,18 +112,18 @@ class _ExportBasicViewState extends State { builder: (_) => SheetPreviewPage( source: SheetPreviewerDataTableSource(formatter.getRows(able)), header: formatter.getHeader(able), - title: S.transitType(able.name), + title: S.transitModelName(able.name), ), ), ); } - /// 用來讓 [SpreadsheetSelector] 幫忙建立表單。 + /// It is used to let [SpreadsheetSelector] help to create the form. Map requiredSheetTitles() => { for (var sheet in sheets.where((sheet) => sheet.checked)) sheet.type: sheet.name, }; - /// [SpreadsheetSelector] 檢查基礎資料後,真正開始匯出。 + /// [SpreadsheetSelector] will check the basic data before actually exporting. Future exportData( GoogleSpreadsheet ss, Map kv, @@ -154,7 +154,7 @@ class _ExportBasicViewState extends State { context, S.actSuccess, action: LauncherSnackbarAction( - label: '開啟表單', + label: S.transitGSSpreadsheetSnackbarAction, link: ss.toLink(), logCode: 'gs_export', ), @@ -167,7 +167,7 @@ class _ExportBasicViewState extends State { sheet.hints = other?.sheets.map((e) => e.title); } - // 同時更新用作 import 的試算表 + // auto set the title to import, to make user easier to import if (other != null && Cache.instance.get(_importKey) == null) { await Cache.instance.set(_importKey, other.toString()); } diff --git a/lib/ui/transit/google_sheet/export_order_view.dart b/lib/ui/transit/google_sheet/export_order_view.dart index 12e92b2a..4615757a 100644 --- a/lib/ui/transit/google_sheet/export_order_view.dart +++ b/lib/ui/transit/google_sheet/export_order_view.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/sign_in_button.dart'; -import 'package:possystem/components/style/launcher_snackbar_action.dart'; import 'package:possystem/components/style/snackbar.dart'; +import 'package:possystem/components/style/snackbar_actions.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/helpers/exporter/google_sheet_exporter.dart'; import 'package:possystem/helpers/logger.dart'; @@ -55,12 +54,12 @@ class _ExportOrderViewState extends State { notifier: widget.statusNotifier, exporter: widget.exporter, cacheKey: _cacheKey, + existLabel: S.transitGSSpreadsheetExportExistLabel, + existHint: S.transitGSSpreadsheetExportExistHint, + emptyLabel: S.transitGSSpreadsheetExportEmptyLabel, + emptyHint: S.transitGSSpreadsheetExportEmptyHint(S.transitGSSpreadsheetOrderDefaultName), fallbackCacheKey: 'exporter_google_sheet', - existLabel: '指定匯出', - existHint: '匯出至試算表「%name」', - emptyLabel: '建立匯出', - emptyHint: '建立新的試算表「${S.transitOrderTitle}」,並把訂單匯出至此', - defaultName: S.transitOrderTitle, + defaultName: S.transitGSSpreadsheetOrderDefaultName, requiredSheetTitles: requiredSheetTitles, onPrepared: exportData, ), @@ -69,13 +68,13 @@ class _ExportOrderViewState extends State { TransitOrderRange(notifier: widget.rangeNotifier), ListTile( key: const Key('edit_sheets'), - title: const Text('表單設定'), + title: Text(S.transitGSOrderSettingTitle), subtitle: MetaBlock.withString( context, [ - '${properties.isOverwrite ? '會' : '不會'}覆寫', - '${properties.withPrefix ? '有' : '沒有'}日期前綴', - // 這個資訊可能突破兩行的限制,所以放最後 + S.transitGSOrderMetaOverwrite(properties.isOverwrite.toString()), + S.transitGSOrderMetaTitlePrefix(properties.withPrefix.toString()), + // This message may break the two lines limit, so put it at the end. properties.requiredSheets.map((e) => e.name).join('、'), ], maxLines: 2, @@ -92,9 +91,7 @@ class _ExportOrderViewState extends State { notifier: widget.rangeNotifier, formatOrder: (order) => OrderTable(order: order), memoryPredictor: memoryPredictor, - warning: '這裡的容量代表網路傳輸所消耗的量,' - '實際佔用的雲端記憶體可能是此值的百分之一而已。' - '詳細容量限制說明可以參考[本文件](https://developers.google.com/sheets/api/limits#quota)。', + warning: S.transitGSOrderMetaMemoryWarning, ), ), ], @@ -108,7 +105,7 @@ class _ExportOrderViewState extends State { } Map requiredSheetTitles() { - final prefix = properties.withPrefix ? widget.rangeNotifier.value.format(DateFormat('MMdd ')) : ''; + final prefix = properties.withPrefix ? '${widget.rangeNotifier.value.formatCompact(S.localeName)} ' : ''; return { for (final sheet in properties.requiredSheets) @@ -116,12 +113,12 @@ class _ExportOrderViewState extends State { }; } - /// [SpreadsheetSelector] 檢查基礎資料後,真正開始匯出。 + /// [SpreadsheetSelector] validate the basic data before actually exporting. Future exportData( GoogleSpreadsheet ss, Map prepared, ) async { - widget.statusNotifier.value = '取得本地資料'; + widget.statusNotifier.value = S.transitGSProgressStatusFetchLocalOrders; final orders = await Seller.instance.getDetailedOrders( widget.rangeNotifier.value.start, widget.rangeNotifier.value.end, @@ -131,7 +128,7 @@ class _ExportOrderViewState extends State { final data = prepared.keys.map(chooseFormatter).map((method) => orders.expand((order) => method(order))); if (properties.isOverwrite) { - widget.statusNotifier.value = '覆寫訂單資料'; + widget.statusNotifier.value = S.transitGSProgressStatusOverwriteOrders; await widget.exporter.updateSheetValues( ss, prepared.values, @@ -142,8 +139,8 @@ class _ExportOrderViewState extends State { final it = data.iterator; for (final entry in prepared.entries) { it.moveNext(); - final name = S.transitType(entry.key.name); - widget.statusNotifier.value = '附加進 $name'; + final name = S.transitModelName(entry.key.name); + widget.statusNotifier.value = S.transitGSProgressStatusAppendOrders(name); await widget.exporter.appendSheetValues(ss, entry.value, it.current); } } @@ -154,7 +151,7 @@ class _ExportOrderViewState extends State { context, S.actSuccess, action: LauncherSnackbarAction( - label: '開啟表單', + label: S.transitGSSpreadsheetSnackbarAction, link: ss.toLink(), logCode: 'gs_export', ), @@ -181,12 +178,12 @@ class _ExportOrderViewState extends State { static List> Function(OrderObject) chooseFormatter(SheetType type) { switch (type) { - case SheetType.orderSetAttr: - return OrderFormatter.formatOrderSetAttr; - case SheetType.orderProduct: - return OrderFormatter.formatOrderProduct; - case SheetType.orderIngredient: - return OrderFormatter.formatOrderIngredient; + case SheetType.orderDetailsAttr: + return OrderFormatter.formatOrderDetailsAttr; + case SheetType.orderDetailsProduct: + return OrderFormatter.formatOrderDetailsProduct; + case SheetType.orderDetailsIngredient: + return OrderFormatter.formatOrderDetailsIngredient; default: return OrderFormatter.formatOrder; } @@ -194,29 +191,29 @@ class _ExportOrderViewState extends State { static List chooseHeaders(SheetType type) { switch (type) { - case SheetType.orderSetAttr: - return OrderFormatter.orderSetAttrHeaders; - case SheetType.orderProduct: - return OrderFormatter.orderProductHeaders; - case SheetType.orderIngredient: - return OrderFormatter.orderIngredientHeaders; + case SheetType.orderDetailsAttr: + return OrderFormatter.orderDetailsAttrHeaders; + case SheetType.orderDetailsProduct: + return OrderFormatter.orderDetailsProductHeaders; + case SheetType.orderDetailsIngredient: + return OrderFormatter.orderDetailsIngredientHeaders; default: return OrderFormatter.orderHeaders; } } - /// 這裡是一些實測輸出結果: + /// These values are based on the actual data: /// /// order: /// 1698067340,2023-10-28 14:51:23,356,295,356,115,241,5,4 /// attr: - /// 1698067340,用餐位置,內用 + /// 1698067340,place,takeout /// product: - /// 1698067340,起士漢堡,漢堡,1,60,30,60 + /// 1698067340,cheese burger,burger,1,60,30,60 /// ingredient: - /// 1698067340,起士,漢堡,,10 + /// 1698067340,cheese,burger,,10 /// - /// 後來考慮壓縮之後,上述的值應該再乘以 0.45 + /// After compression, the values should be multiplied by 0.5. static int memoryPredictor(OrderMetrics m) { return (m.count * 30 + m.attrCount! * 10 + m.productCount! * 13 + m.ingredientCount! * 8).toInt(); } diff --git a/lib/ui/transit/google_sheet/import_basic_view.dart b/lib/ui/transit/google_sheet/import_basic_view.dart index 331f6053..ecd90f59 100644 --- a/lib/ui/transit/google_sheet/import_basic_view.dart +++ b/lib/ui/transit/google_sheet/import_basic_view.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/dialog/confirm_dialog.dart'; -import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/sign_in_button.dart'; import 'package:possystem/components/style/snackbar.dart'; import 'package:possystem/components/style/text_divider.dart'; @@ -57,10 +56,10 @@ class _ImportBasicViewState extends State { exporter: widget.exporter, notifier: widget.notifier, cacheKey: _cacheKey, - existLabel: '確認表單名稱', - existHint: '從試算表中取得所有表單的名稱,並進行匯入', - emptyLabel: '選擇試算表', - emptyHint: '選擇要匯入的試算表後,就能開始匯入資料', + existLabel: S.transitGSSpreadsheetImportExistLabel, + existHint: (_) => S.transitGSSpreadsheetImportExistHint, + emptyLabel: S.transitGSSpreadsheetImportEmptyLabel, + emptyHint: S.transitGSSpreadsheetImportEmptyHint, onUpdated: reloadSheetHints, onChosen: reloadSheets, ), @@ -68,14 +67,14 @@ class _ImportBasicViewState extends State { ), ListTile( key: const Key('gs_export.import_all'), - title: const Text('匯入全部'), - subtitle: const Text('不會有任何預覽畫面,直接覆寫全部的資料。'), + title: Text(S.transitGSSpreadsheetImportAllBtn), + subtitle: Text(S.transitGSSpreadsheetImportAllHint), trailing: const Icon(Icons.download_for_offline_sharp), onTap: () async { final confirmed = await ConfirmDialog.show( context, - title: '匯入全部資料?', - content: '將會把所選表單的資料都下載,並完全覆蓋本地資料。\n此動作無法復原。', + title: S.transitGSSpreadsheetImportAllConfirmTitle, + content: S.transitGSSpreadsheetImportAllConfirmContent, ); if (confirmed) { @@ -83,7 +82,7 @@ class _ImportBasicViewState extends State { } }, ), - const TextDivider(label: '選擇欲匯入表單'), + TextDivider(label: S.transitGSSpreadsheetModelImportDivider), for (final entry in sheets.entries) Padding( padding: const EdgeInsets.all(12.0), @@ -97,7 +96,7 @@ class _ImportBasicViewState extends State { ), const SizedBox(width: 8.0), IconButton( - tooltip: '預覽結果並匯入', + tooltip: S.transitImportPreviewBtn, icon: const Icon(KIcons.preview), onPressed: () => import(entry.key), ), @@ -115,7 +114,7 @@ class _ImportBasicViewState extends State { super.dispose(); } - /// 重新讀取試算表的表單名稱 + /// Reload the sheet names for later import. Future reloadSheets(GoogleSpreadsheet ss) async { final exist = await widget.exporter.getSheets(ss); @@ -131,7 +130,7 @@ class _ImportBasicViewState extends State { Future import(Formattable? type) async { final ss = selector.currentState?.spreadsheet; if (ss == null) { - showSnackBar(context, S.transitGSImportError('emptySpreadsheet')); + showSnackBar(context, S.transitGSErrorImportEmptySpreadsheet); return; } @@ -141,7 +140,7 @@ class _ImportBasicViewState extends State { .map((e) => MapEntry(e.key, e.value.currentState!.selected!)) .toList(); if (selected.isEmpty) { - showSnackBar(context, S.transitGSImportError('emptySheet')); + showSnackBar(context, S.transitGSErrorImportEmptySheet); return; } @@ -156,7 +155,7 @@ class _ImportBasicViewState extends State { widget.notifier.value = '_finish'; } - /// 檢查基礎資料後,真正開始匯入。 + /// After verifying the basic data, start importing. Future _importSheets( GoogleSpreadsheet ss, List> ableSheets, @@ -165,7 +164,7 @@ class _ImportBasicViewState extends State { for (final entry in ableSheets) { final able = entry.key; final sheet = entry.value; - widget.notifier.value = S.transitGSUpdateModelStatus(able.name); + widget.notifier.value = S.transitGSModelStatus(able.name); if (!await _importData(ss, able, sheet, needPreview)) { return; @@ -183,11 +182,12 @@ class _ImportBasicViewState extends State { } } - /// 匯入指定表單。 + /// Import the specified form. /// - /// 1. 取得資料 - /// 2. 快取(如果可能的話,預覽) - /// 3. 解析資料並匯入 + /// The process is as follows: + /// 1. Get data + /// 2. Cache (if possible, preview) + /// 3. Parse data and import Future _importData( GoogleSpreadsheet ss, Formattable able, @@ -201,17 +201,8 @@ class _ImportBasicViewState extends State { if (mounted) { showMoreInfoSnackBar( context, - '找不到表單「${sheet.title}」的資料', - MetaBlock.withString( - context, - [ - '別擔心,通常都可以簡單解決!\n可能的原因有:\n', - '網路狀況不穩;\n', - '尚未進行授權;\n', - '表單 ID 打錯了,請嘗試複製整個網址後貼上;\n', - '該表單被刪除了。', - ], - textOverflow: TextOverflow.visible)!, + S.transitGSErrorImportNotFoundSheets(sheet.title), + Text(S.transitGSErrorImportNotFoundHelper), ); } return false; @@ -239,13 +230,13 @@ class _ImportBasicViewState extends State { return approved ?? false; } - /// 請求表單的資料 + /// Request the data from the sheet. Future>?> _requestData( Formattable able, GoogleSpreadsheet ss, GoogleSheetProperties sheet, ) async { - widget.notifier.value = '驗證身份中'; + widget.notifier.value = S.transitGSProgressStatusVerifyUser; await widget.exporter.auth(); const formatter = GoogleSheetFormatter(); @@ -276,11 +267,11 @@ class _ImportBasicViewState extends State { builder: (context) => SheetPreviewPage( source: SheetPreviewerDataTableSource(source), header: formatter.getHeader(able), - title: S.transitType(able.name), + title: S.transitModelName(able.name), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(true), - child: Text(S.transitPreviewImportTitle), + child: Text(S.transitImportPreviewBtn), ), ], ), diff --git a/lib/ui/transit/google_sheet/order_formatter.dart b/lib/ui/transit/google_sheet/order_formatter.dart index 9d5344e5..a61b1f71 100644 --- a/lib/ui/transit/google_sheet/order_formatter.dart +++ b/lib/ui/transit/google_sheet/order_formatter.dart @@ -1,6 +1,7 @@ import 'package:possystem/helpers/util.dart'; import 'package:possystem/models/objects/order_object.dart'; import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; class OrderFormatter { static List> formatOrder(OrderObject order) { @@ -12,14 +13,14 @@ class OrderFormatter { order.productsPrice, order.paid, order.cost, - order.revenue, + order.profit, order.productsCount, order.products.length, ] ]; } - static List> formatOrderSetAttr(OrderObject order) { + static List> formatOrderDetailsAttr(OrderObject order) { return [ for (final attr in order.attributes) [ @@ -30,7 +31,7 @@ class OrderFormatter { ]; } - static List> formatOrderProduct(OrderObject order) { + static List> formatOrderDetailsProduct(OrderObject order) { return [ for (final product in order.products) [ @@ -45,7 +46,7 @@ class OrderFormatter { ]; } - static List> formatOrderIngredient(OrderObject order) { + static List> formatOrderDetailsIngredient(OrderObject order) { final createdAt = order.createdAt.millisecondsSinceEpoch ~/ 1000; return [ for (final product in order.products) @@ -59,47 +60,47 @@ class OrderFormatter { ]; } - static const orderHeaders = [ - '時間戳記', - '時間', - '總價', - '產品價錢', - '付額', - '成本', - '收入', - '產品總數', - '產品種類', - ]; + static List get orderHeaders => [ + S.transitGSOrderHeaderTs, + S.transitGSOrderHeaderTime, + S.transitGSOrderHeaderPrice, + S.transitGSOrderHeaderProductPrice, + S.transitGSOrderHeaderPaid, + S.transitGSOrderHeaderCost, + S.transitGSOrderHeaderProfit, + S.transitGSOrderHeaderItemCount, + S.transitGSOrderHeaderTypeCount, + ]; - /// 顧客設定位於第幾個欄位,0-index - static const orderSetAttrIndex = 8; + /// Order's attributes at which index, 0-index + static const orderDetailsAttrIndex = 8; - /// 產品細項位於第幾個欄位,0-index - static const orderProductIndex = 9; + /// Order's products detail at which index, 0-index + static const orderDetailsProductIndex = 9; - static const orderSetAttrHeaders = [ - '時間戳記', - '設定類別', - '選項', - ]; + static List get orderDetailsAttrHeaders => [ + S.transitGSOrderAttributeHeaderTs, + S.transitGSOrderAttributeHeaderName, + S.transitGSOrderAttributeHeaderOption, + ]; - static const orderProductHeaders = [ - '時間戳記', - '產品', - '種類', - '數量', - '單一售價', - '單一成本', - '單一原價', - ]; + static List get orderDetailsProductHeaders => [ + S.transitGSOrderProductHeaderTs, + S.transitGSOrderProductHeaderName, + S.transitGSOrderProductHeaderCatalog, + S.transitGSOrderProductHeaderCount, + S.transitGSOrderProductHeaderPrice, + S.transitGSOrderProductHeaderCost, + S.transitGSOrderProductHeaderOrigin, + ]; - /// 成份位於第幾個欄位,0-index - static const orderIngredientIndex = 6; + /// Order's ingredients detail at which index, 0-index + static const orderDetailsIngredientIndex = 6; - static const orderIngredientHeaders = [ - '時間戳記', - '成份', - '份量', - '數量', - ]; + static List get orderDetailsIngredientHeaders => [ + S.transitGSOrderIngredientHeaderTs, + S.transitGSOrderIngredientHeaderName, + S.transitGSOrderIngredientHeaderQuantity, + S.transitGSOrderIngredientHeaderAmount, + ]; } diff --git a/lib/ui/transit/google_sheet/order_setting_page.dart b/lib/ui/transit/google_sheet/order_setting_page.dart index 45dae09b..5af1a0c2 100644 --- a/lib/ui/transit/google_sheet/order_setting_page.dart +++ b/lib/ui/transit/google_sheet/order_setting_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:possystem/components/mixin/item_modal.dart'; +import 'package:possystem/components/scaffold/item_modal.dart'; import 'package:possystem/components/style/card_info_text.dart'; import 'package:possystem/components/style/text_divider.dart'; import 'package:possystem/helpers/exporter/google_sheet_exporter.dart'; @@ -34,7 +34,7 @@ class _OrderSettingPageState extends State with ItemModal '訂單匯出設定'; + String get title => S.transitGSOrderSettingTitle; @override List buildFormFields() { @@ -42,8 +42,8 @@ class _OrderSettingPageState extends State with ItemModal with ItemModal with ItemModal with ItemModal sheets; - // 是否覆蓋表單的資料,預設是 true + /// Whether to overwrite the data in the form, default is true final bool isOverwrite; - // 表單名稱是否前綴日期,預設是 true + /// Whether the form name is prefixed with the date, default is true final bool withPrefix; const OrderSpreadsheetProperties({ @@ -141,7 +141,7 @@ class OrderSpreadsheetProperties { final isRequired = Cache.instance.get('$key.required') ?? true; sheets.add(OrderSheetProperties( type, - name ?? S.transitType(type.name), + name ?? S.transitModelName(type.name), isRequired, )); } @@ -179,7 +179,7 @@ class OrderSheetProperties { enum OrderSheetType { order, - orderSetAttr, - orderProduct, - orderIngredient, + orderDetailsAttr, + orderDetailsProduct, + orderDetailsIngredient, } diff --git a/lib/ui/transit/google_sheet/order_table.dart b/lib/ui/transit/google_sheet/order_table.dart index 8bf985fb..b8e21cd8 100644 --- a/lib/ui/transit/google_sheet/order_table.dart +++ b/lib/ui/transit/google_sheet/order_table.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/style/hint_text.dart'; import 'package:possystem/components/style/text_divider.dart'; import 'package:possystem/models/objects/order_object.dart'; +import 'package:possystem/translator.dart'; import 'order_formatter.dart'; @@ -26,25 +27,25 @@ class _OrderTableState extends State { headers: OrderFormatter.orderHeaders, data: OrderFormatter.formatOrder(widget.order), expandableIndexes: const [ - OrderFormatter.orderSetAttrIndex, - OrderFormatter.orderProductIndex, + OrderFormatter.orderDetailsAttrIndex, + OrderFormatter.orderDetailsProductIndex, ], ), - const TextDivider(label: '訂單顧客設定'), + TextDivider(label: S.transitGSOrderAttributeTitle), SimpleTable( - headers: OrderFormatter.orderSetAttrHeaders, - data: OrderFormatter.formatOrderSetAttr(widget.order), + headers: OrderFormatter.orderDetailsAttrHeaders, + data: OrderFormatter.formatOrderDetailsAttr(widget.order), ), - const TextDivider(label: '訂單產品細項'), + TextDivider(label: S.transitGSOrderProductTitle), SimpleTable( - headers: OrderFormatter.orderProductHeaders, - data: OrderFormatter.formatOrderProduct(widget.order), - expandableIndexes: const [OrderFormatter.orderIngredientIndex], + headers: OrderFormatter.orderDetailsProductHeaders, + data: OrderFormatter.formatOrderDetailsProduct(widget.order), + expandableIndexes: const [OrderFormatter.orderDetailsIngredientIndex], ), - const TextDivider(label: '訂單成份細項'), + TextDivider(label: S.transitGSOrderIngredientTitle), SimpleTable( - headers: OrderFormatter.orderIngredientHeaders, - data: OrderFormatter.formatOrderIngredient(widget.order), + headers: OrderFormatter.orderDetailsIngredientHeaders, + data: OrderFormatter.formatOrderDetailsIngredient(widget.order), ), ]), ); @@ -100,7 +101,7 @@ class SimpleTable extends StatelessWidget { yield Padding( padding: const EdgeInsets.all(4.0), child: idxOf != -1 - ? const HintText('詳見下欄') + ? HintText(S.transitGSOrderExpandableHint) : Text( cell.toString(), textAlign: cell is String ? TextAlign.end : TextAlign.start, diff --git a/lib/ui/transit/google_sheet/sheet_namer.dart b/lib/ui/transit/google_sheet/sheet_namer.dart index be3edf33..c392a493 100644 --- a/lib/ui/transit/google_sheet/sheet_namer.dart +++ b/lib/ui/transit/google_sheet/sheet_namer.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/dialog/single_text_dialog.dart'; -import 'package:possystem/components/style/more_button.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/helpers/validator.dart'; import 'package:possystem/translator.dart'; @@ -35,7 +35,7 @@ class SheetNamerState extends State { final secondary = widget.action == null ? IconButton( icon: const Icon(KIcons.edit), - tooltip: '修改標題', + tooltip: S.transitGSSheetNameUpdate, onPressed: editSheetName, ) : EntryMoreButton( @@ -67,10 +67,10 @@ class SheetNamerState extends State { void showActions() async { final result = await showCircularBottomSheet(context, actions: [ - const BottomSheetAction( - key: Key('btn.edit'), - title: Text('修改標題'), - leading: Icon(KIcons.edit), + BottomSheetAction( + key: const Key('btn.edit'), + title: Text(S.transitGSSheetNameUpdate), + leading: const Icon(KIcons.edit), returnValue: 0, ), BottomSheetAction( @@ -115,13 +115,13 @@ class SheetNamerState extends State { class SheetNamerProperties { final SheetType type; - // 表單標題 + /// The name of the sheet String name; - // 初始是否啟用 + /// Whether the sheet is enabled bool checked; - // 用作 autocomplete + /// Use as autocomplete Iterable? hints; SheetNamerProperties( @@ -132,6 +132,6 @@ class SheetNamerProperties { }); String get labelText { - return S.transitGSSheetLabel(S.transitType(type.name)); + return S.transitGSSheetNameLabel(S.transitModelName(type.name)); } } diff --git a/lib/ui/transit/google_sheet/sheet_selector.dart b/lib/ui/transit/google_sheet/sheet_selector.dart index faccfb58..4db765c2 100644 --- a/lib/ui/transit/google_sheet/sheet_selector.dart +++ b/lib/ui/transit/google_sheet/sheet_selector.dart @@ -29,8 +29,8 @@ class SheetSelectorState extends State { value: selected, style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( - label: Text(S.transitGSSheetLabel( - S.transitType(widget.label), + label: Text(S.transitGSSheetNameLabel( + S.transitModelName(widget.label), )), floatingLabelBehavior: FloatingLabelBehavior.always, ), @@ -39,7 +39,7 @@ class SheetSelectorState extends State { DropdownMenuItem( value: null, child: Text( - '請先確認試算表', + S.stockReplenishmentNameHint, style: TextStyle(color: Theme.of(context).hintColor), ), ), diff --git a/lib/ui/transit/google_sheet/spreadsheet_selector.dart b/lib/ui/transit/google_sheet/spreadsheet_selector.dart index 40b3c392..ab077750 100644 --- a/lib/ui/transit/google_sheet/spreadsheet_selector.dart +++ b/lib/ui/transit/google_sheet/spreadsheet_selector.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/bottom_sheet_actions.dart'; import 'package:possystem/components/dialog/confirm_dialog.dart'; import 'package:possystem/components/dialog/single_text_dialog.dart'; -import 'package:possystem/components/meta_block.dart'; -import 'package:possystem/components/style/more_button.dart'; +import 'package:possystem/components/style/buttons.dart'; import 'package:possystem/components/style/snackbar.dart'; import 'package:possystem/helpers/exporter/google_sheet_exporter.dart'; import 'package:possystem/helpers/logger.dart'; @@ -21,41 +20,45 @@ class SpreadsheetSelector extends StatefulWidget { final ValueNotifier? notifier; - /// 快取的鍵 + /// The key to cache the selected spreadsheet final String cacheKey; - // 允許設定一個預設的表單 + /// The key to cache the selected spreadsheet when the [cacheKey] is not found + /// + /// For example import and export use different cache key but they want to + /// share the same spreadsheet when the user only select one. final String fallbackCacheKey; - // 預設的試算表名稱 + /// The default name of the spreadsheet final String defaultName; - /// 試算表存在的話,按鈕的文字 + /// The text of the button when the spreadsheet is exist final String existLabel; - /// 試算表不存在的話,按鈕的文字 + /// The text of the button when the spreadsheet is not set final String emptyLabel; - /// 試算表存在的話,按鈕下方的文字。 - /// %name 可以替代為現在的 spreadsheet 名稱 - final String existHint; + /// The hint text when the spreadsheet is exist + final String Function(String) existHint; - /// 試算表不存在的話,按鈕下方的文字 + /// The hint text when the spreadsheet is not set final String emptyHint; - /// 試算表被更新了 + /// Spreadsheet has been updated(clear or changed), should update the UI final Future Function(GoogleSpreadsheet? spreadsheet)? onUpdated; - /// 根據選擇好的試算表去執行某些行為,例如匯入 + /// Spreadsheet has been changed, should update the UI final Future Function(GoogleSpreadsheet spreadsheet)? onChosen; - /// 根據選擇好的試算表並且準備好 [sheetsToCreate] 的表單後,去執行某些行為,例如匯出 + /// Spreadsheet has been prepared(sheet has been created), should start exporting final Future Function( GoogleSpreadsheet spreadsheet, Map sheets, )? onPrepared; - /// 若設定此值,代表允許建立試算表,並同時準備好該試算表的部分表單 + /// The title of the sheets when the spreadsheet is created. + /// + /// This means the user can create a spreadsheet and prepare the sheets at the same time. final Map Function()? requiredSheetTitles; const SpreadsheetSelector({ @@ -95,7 +98,7 @@ class SpreadsheetSelectorState extends State { String get label => isExist ? widget.existLabel : widget.emptyLabel; - String get hint => (isExist ? widget.existHint.replaceFirst('%name', spreadsheet!.name) : widget.emptyHint); + String get hint => isExist ? widget.existHint(spreadsheet!.name) : widget.emptyHint; @override Widget build(BuildContext context) { @@ -122,15 +125,15 @@ class SpreadsheetSelectorState extends State { final selected = await showCircularBottomSheet<_ActionTypes>( context, actions: >[ - const BottomSheetAction( - title: Text('選擇試算表'), - leading: Icon(Icons.file_open_outlined), + BottomSheetAction( + title: Text(S.transitGSSpreadsheetActionSelect), + leading: const Icon(Icons.file_open_outlined), returnValue: _ActionTypes.choose, ), if (widget.requiredSheetTitles != null) - const BottomSheetAction( - title: Text('清除所選'), - leading: Icon(Icons.cleaning_services_outlined), + BottomSheetAction( + title: Text(S.transitGSSpreadsheetActionClear), + leading: const Icon(Icons.cleaning_services_outlined), returnValue: _ActionTypes.clear, ), ], @@ -148,7 +151,7 @@ class SpreadsheetSelectorState extends State { } } - // 執行要求的函式 + /// Execute the action when the button is clicked void execute() async { await showSnackbarWhenFailed( _execute(), @@ -159,7 +162,7 @@ class SpreadsheetSelectorState extends State { _notify('_finish'); } - // 選擇試算表 + /// Choose a spreadsheet Future choose() async { _notify('_start'); @@ -172,7 +175,7 @@ class SpreadsheetSelectorState extends State { _notify('_finish'); } - // 清除所選的試算表 + /// Clear the selected spreadsheet Future clear() async { await _update(null); @@ -189,14 +192,14 @@ class SpreadsheetSelectorState extends State { Future _execute() async { final requiredSheetTitles = widget.requiredSheetTitles; - // 如果不能建立,就再去跟使用者要一次 + // If the required sheets are not set, notify the user to choose a spreadsheet if (requiredSheetTitles == null) { _notify('_start'); if (!isExist) { await _choose(); } - // 剛剛有成功要到或者本來就有 + // It is successful from previous action or already have the spreadsheet if (isExist) { await widget.onChosen?.call(spreadsheet!); } @@ -206,12 +209,12 @@ class SpreadsheetSelectorState extends State { final confirmed = await ConfirmDialog.show( context, title: '$label?', - content: '此動作將會$hint', + content: S.transitGSSpreadsheetConfirm(hint), ); if (confirmed) { _notify('_start'); - // 建立並且回應 + // create sheets and execute callback final sheets = await _prepare(spreadsheet, requiredSheetTitles()); if (sheets != null) { await widget.onPrepared?.call(spreadsheet!, sheets); @@ -232,7 +235,7 @@ class SpreadsheetSelectorState extends State { initialValue: spreadsheet?.id, decoration: InputDecoration( labelText: S.transitGSSpreadsheetLabel, - helperText: spreadsheet?.name == null ? '輸入試算表網址或試算表 ID' : '本試算表為「${spreadsheet?.name}」', + helperText: S.transitGSSpreadsheetSelectionHint(spreadsheet?.name.toString() ?? '_'), floatingLabelBehavior: FloatingLabelBehavior.always, errorMaxLines: 5, ), @@ -255,24 +258,17 @@ class SpreadsheetSelectorState extends State { } else if (mounted) { showMoreInfoSnackBar( context, - '找不到表單', - MetaBlock.withString( - context, - [ - '別擔心,通常都可以簡單解決!\n可能的原因有:\n', - '網路狀況不穩;\n', - '該表單被限制存取了,請打開權限;\n', - '打錯了,請嘗試複製整個網址後貼上;\n', - '該表單被刪除了。', - ], - textOverflow: TextOverflow.visible)!, + S.transitGSErrorImportNotFoundSpreadsheet, + Text(S.transitGSErrorImportNotFoundHelper), ); } } Future _update(GoogleSpreadsheet? other) async { Log.ger('change start', 'gs_export', other.toString()); - setState(() => spreadsheet = other); + if (mounted) { + setState(() => spreadsheet = other); + } await widget.onUpdated?.call(other); if (other != null) { @@ -280,31 +276,31 @@ class SpreadsheetSelectorState extends State { } } - /// 準備好試算表裡的表單 + /// Prepare the sheets in the spreadsheet. /// - /// 若沒有試算表則建立,並建立所有可能的表單。 - /// 若有則把需要的表單準備好。 + /// If the spreadsheet is not exist, create it and create all the sheets. + /// If the spreadsheet is exist, prepare the sheets. Future?> _prepare( GoogleSpreadsheet? ss, Map sheets, ) async { if (sheets.isEmpty) { - showSnackBar(context, S.transitGSErrors('sheetEmpty')); + showSnackBar(context, S.transitGSErrorSheetEmpty); return null; } if (sheets.values.toSet().length != sheets.length) { - showSnackBar(context, S.transitGSErrors('sheetRepeat')); + showSnackBar(context, S.transitGSErrorSheetRepeat); return null; } - _notify('驗證身份中'); + _notify(S.transitGSProgressStatusVerifyUser); await widget.exporter.auth(); final names = sheets.values.toList(); if (ss == null) { - _notify(S.transitGSProgressStatus('addSpreadsheet')); + _notify(S.transitGSProgressStatusAddSpreadsheet); final other = await widget.exporter.addSpreadsheet( widget.defaultName, @@ -314,15 +310,8 @@ class SpreadsheetSelectorState extends State { if (mounted) { showMoreInfoSnackBar( context, - S.transitGSErrors('spreadsheet'), - MetaBlock.withString( - context, - [ - '別擔心,通常都可以簡單解決!\n可能的原因有:\n', - '網路狀況不穩;\n', - '尚未授權 POS 系統進行表單的編輯。', - ], - textOverflow: TextOverflow.visible)!, + S.transitGSErrorCreateSpreadsheet, + Text(S.transitGSErrorCreateSpreadsheetHelper), ); } return null; @@ -339,17 +328,8 @@ class SpreadsheetSelectorState extends State { if (mounted) { showMoreInfoSnackBar( context, - S.transitGSErrors('sheet'), - MetaBlock.withString( - context, - [ - '別擔心,通常都可以簡單解決!\n可能的原因有:\n', - '網路狀況不穩;\n', - '尚未進行授權;\n', - '表單 ID 打錯了,請嘗試複製整個網址後貼上;\n', - '該試算表被刪除了。', - ], - textOverflow: TextOverflow.visible)!, + S.transitGSErrorCreateSheet, + Text(S.transitGSErrorCreateSheetHelper), ); } return null; @@ -366,7 +346,7 @@ class SpreadsheetSelectorState extends State { }; } - /// 補足該試算表的表單 + /// Fulfill the sheets in the spreadsheet Future _fulfillSheets(GoogleSpreadsheet ss, List names) async { final exist = ss.sheets.map((e) => e.title).toSet(); final missing = names.toSet().difference(exist); @@ -374,7 +354,7 @@ class SpreadsheetSelectorState extends State { return true; } - _notify(S.transitGSProgressStatus('addSheets')); + _notify(S.transitGSProgressStatusAddSheets); final added = await widget.exporter.addSheets(ss, missing.toList()); if (added != null) { ss.sheets.addAll(added); @@ -386,12 +366,10 @@ class SpreadsheetSelectorState extends State { } String? _spreadsheetValidator(String? text) { - if (text == null || text.isEmpty) return '不能為空'; + if (text == null || text.isEmpty) return S.transitGSErrorSpreadsheetIdEmpty; if (!_sheetUrlRegex.hasMatch(text) && !_sheetIdRegex.hasMatch(text)) { - return '不合法的文字,必須包含:\n' - '/spreadsheets/d//\n' - '或者直接給 ID(英文+數字+底線+減號的組合)'; + return S.transitGSErrorSpreadsheetIdInvalid; } return null; @@ -417,7 +395,7 @@ enum SheetType { replenisher, orderAttr, order, - orderSetAttr, - orderProduct, - orderIngredient, + orderDetailsAttr, + orderDetailsProduct, + orderDetailsIngredient, } diff --git a/lib/ui/transit/plain_text_widgets/export_basic_view.dart b/lib/ui/transit/plain_text/export_basic_view.dart similarity index 93% rename from lib/ui/transit/plain_text_widgets/export_basic_view.dart rename to lib/ui/transit/plain_text/export_basic_view.dart index 0a35f8a3..26327f6a 100644 --- a/lib/ui/transit/plain_text_widgets/export_basic_view.dart +++ b/lib/ui/transit/plain_text/export_basic_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/style/snackbar.dart'; -import 'package:possystem/helpers/formatter/formatter.dart'; import 'package:possystem/helpers/exporter/plain_text_exporter.dart'; +import 'package:possystem/helpers/formatter/formatter.dart'; import 'package:possystem/translator.dart'; class ExportBasicView extends StatefulWidget { @@ -29,7 +29,7 @@ class _ExportBasicViewState extends State with SingleTickerProv for (final able in Formattable.values) Tab( key: Key('tab.${able.name}'), - text: S.transitType(able.name), + text: S.transitModelName(able.name), ) ], ), @@ -66,16 +66,13 @@ class _ExportBasicViewState extends State with SingleTickerProv margin: const EdgeInsets.symmetric(horizontal: 16.0), child: ListTile( key: Key('export_btn.${able.name}'), - title: const Text('複製文字'), + title: Text(S.transitPTCopyBtn), subtitle: MetaBlock.withString( context, widget.exporter.formatter.getHeader(able), ), onTap: () => _copy(able), - trailing: const Icon( - Icons.copy_outlined, - semanticLabel: '複製文字', - ), + trailing: Icon(Icons.copy_outlined, semanticLabel: S.transitPTCopyBtn), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8.0)), ), @@ -137,6 +134,6 @@ class _ExportBasicViewState extends State with SingleTickerProv widget.exporter.export(able), context, 'pt_export_failed', - ).then((value) => showSnackBar(context, '複製成功')); + ).then((value) => showSnackBar(context, S.transitPTCopySuccess)); } } diff --git a/lib/ui/transit/plain_text_widgets/export_order_view.dart b/lib/ui/transit/plain_text/export_order_view.dart similarity index 60% rename from lib/ui/transit/plain_text_widgets/export_order_view.dart rename to lib/ui/transit/plain_text/export_order_view.dart index 2e0eafcc..c99dfeb6 100644 --- a/lib/ui/transit/plain_text_widgets/export_order_view.dart +++ b/lib/ui/transit/plain_text/export_order_view.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:possystem/components/style/snackbar.dart'; import 'package:possystem/helpers/exporter/plain_text_exporter.dart'; import 'package:possystem/models/objects/order_object.dart'; import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/translator.dart'; -import 'package:possystem/ui/transit/transit_order_range.dart'; import 'package:possystem/ui/transit/transit_order_list.dart'; +import 'package:possystem/ui/transit/transit_order_range.dart'; class ExportOrderView extends StatelessWidget { final ValueNotifier notifier; @@ -26,8 +25,8 @@ class ExportOrderView extends StatelessWidget { key: const Key('export_btn'), margin: const EdgeInsets.symmetric(horizontal: 16.0), child: ListTile( - title: const Text('複製文字'), - subtitle: const Text('複製過大的文字可能會造成系統的崩潰'), + title: Text(S.transitPTCopyBtn), + subtitle: Text(S.transitPTCopyWarning), trailing: const Icon(Icons.copy_outlined), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8.0)), @@ -37,7 +36,7 @@ class ExportOrderView extends StatelessWidget { export(), context, 'pt_export_failed', - ).then((value) => showSnackBar(context, '複製成功')); + ).then((value) => showSnackBar(context, S.transitPTCopySuccess)); }, ), ), @@ -62,54 +61,56 @@ class ExportOrderView extends StatelessWidget { const exporter = PlainTextExporter(); await exporter.exportToClipboard(orders .map((o) => [ - DateFormat('M月d日 HH:mm:ss').format(o.createdAt), + S.transitOrderItemTitle(o.createdAt), formatOrder(o), ].join('\n')) .join('\n\n')); } - /// 實際輸出結果: + /// Actual result depends on language, here is English version: /// - /// 共 110 元,其中的 90 元是產品價錢。 - /// 付額 150 元、成分 30 元。 - /// 顧客的 用餐位置 為 內用、年紀 為 三十歲。 - /// 餐點有 3 份(2 種)包括: - /// 起士漢堡(漢堡)1 份共 200 元,成份包括 - /// 起士(多量,使用 3 個)。 + /// Total $110, $90 of them are product price. + /// Paid $150, cost $30. + /// Customer's dining location is Dine-in, age is 30. + /// There are 3 (2 kinds) products including: + /// Cheese Burger (Burger) 1, total $200, ingredients are Cheese (Large, use 3). static int memoryPredictor(OrderMetrics m) { return (m.count * 60 + m.attrCount! * 18 + m.productCount! * 25 + m.ingredientCount! * 10).toInt(); } static String formatOrder(OrderObject order) { final attributes = order.attributes.map((a) { - return '${a.name} 為 ${a.optionName}'; + return S.transitPTFormatOrderOrderAttributeItem(a.name, a.optionName); }).join('、'); final products = order.products.map((p) { final ing = p.ingredients.map((i) { - final amount = i.amount == 0 ? '' : ',使用 ${i.amount} 個'; - return S.orderProductIngredientName( + return S.transitPTFormatOrderIngredient( + i.amount, i.ingredientName, - (i.quantityName ?? '預設份量') + amount, + i.quantityName ?? S.transitPTFormatOrderNoQuantity, ); }).join('、'); - return [ - '${p.productName}(${p.catalogName})', - '${p.count} 份共 ${p.totalPrice.toCurrency()} 元,', - ing == '' ? '沒有設定成分' : '成份包括 $ing', - ].join(''); + return S.transitPTFormatOrderProduct( + p.ingredients.length, + p.productName, + p.catalogName, + p.count, + p.totalPrice.toCurrency(), + ing, + ); }).join(';\n'); - final pl = order.products.length; - final tc = order.productsCount; + final setCount = order.products.length; + final totalCount = order.productsCount; return [ - '共 ${order.price.toCurrency()} 元', - order.productsPrice == order.price ? '。\n' : ',其中的 ${order.productsPrice.toCurrency()} 元是產品價錢。\n', - '付額 ${order.paid.toCurrency()} 元、', - '成分 ${order.cost.toCurrency()} 元。\n', - if (attributes != '') '顧客的 $attributes。\n', - '餐點有 $tc 份', - if (pl != tc) '($pl 種)', - '包括:\n$products。', - ].join(''); + S.transitPTFormatOrderPrice( + order.productsPrice == order.price ? 0 : 1, + order.price.toCurrency(), + order.productsPrice.toCurrency(), + ), + S.transitPTFormatOrderMoney(order.paid.toCurrency(), order.cost.toCurrency()), + if (attributes != '') S.transitPTFormatOrderOrderAttribute(attributes), + S.transitPTFormatOrderProductCount(totalCount, setCount, products) + ].join('\n'); } } diff --git a/lib/ui/transit/plain_text_widgets/import_basic_view.dart b/lib/ui/transit/plain_text/import_basic_view.dart similarity index 85% rename from lib/ui/transit/plain_text_widgets/import_basic_view.dart rename to lib/ui/transit/plain_text/import_basic_view.dart index da765e37..6ee40515 100644 --- a/lib/ui/transit/plain_text_widgets/import_basic_view.dart +++ b/lib/ui/transit/plain_text/import_basic_view.dart @@ -31,7 +31,7 @@ class _ImportBasicViewState extends State with AutomaticKeepAli key: const Key('import_btn'), margin: const EdgeInsets.symmetric(horizontal: 16.0), child: ListTile( - title: const Text('預覽結果'), + title: Text(S.transitImportPreviewTitle), trailing: const Icon(KIcons.preview), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8.0)), @@ -48,12 +48,12 @@ class _ImportBasicViewState extends State with AutomaticKeepAli keyboardType: TextInputType.multiline, minLines: 3, maxLines: 6, - decoration: const InputDecoration( - border: OutlineInputBorder( + decoration: InputDecoration( + border: const OutlineInputBorder( borderSide: BorderSide(width: 5.0), ), - hintText: '請貼上複製而來的文字', - helperText: '貼上文字後,會分析文字並決定匯入的是什麼種類的資訊。', + hintText: S.transitPTImportHint, + helperText: S.transitPTImportHelper, helperMaxLines: 2, ), ), @@ -71,7 +71,7 @@ class _ImportBasicViewState extends State with AutomaticKeepAli final able = widget.exporter.formatter.whichFormattable(first); if (able == null) { - showSnackBar(context, '這段文字無法匹配相應的服務,請參考匯出時的文字內容'); + showSnackBar(context, S.transitPTImportErrorNotFound); return; } diff --git a/lib/ui/transit/plain_text_widgets/views.dart b/lib/ui/transit/plain_text/views.dart similarity index 100% rename from lib/ui/transit/plain_text_widgets/views.dart rename to lib/ui/transit/plain_text/views.dart diff --git a/lib/ui/transit/previews/ingredient_preview_page.dart b/lib/ui/transit/previews/ingredient_preview_page.dart index 584d199e..e55b9aeb 100644 --- a/lib/ui/transit/previews/ingredient_preview_page.dart +++ b/lib/ui/transit/previews/ingredient_preview_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/models/stock/ingredient.dart'; +import 'package:possystem/translator.dart'; import 'preview_page.dart'; @@ -18,14 +19,14 @@ class IngredientPreviewPage extends PreviewPage { status: item.statusName, ), subtitle: MetaBlock.withString(context, [ - '庫存:${item.currentAmount}', - '最大值:${item.totalAmount ?? '未設定'}', + S.transitImportPreviewIngredientMetaAmount(item.currentAmount), + S.transitImportPreviewIngredientMetaMaxAmount(item.totalAmount == null ? 0 : 1, item.totalAmount ?? 0), ]), ); } @override Widget getHeader(BuildContext context) { - return const Text('匯入後,為了避免影響「菜單」的狀況,並不會把舊的成分移除。'); + return Text(S.transitImportPreviewIngredientHeader); } } diff --git a/lib/ui/transit/previews/order_attribute_preview_page.dart b/lib/ui/transit/previews/order_attribute_preview_page.dart index a56832a2..b14708e8 100644 --- a/lib/ui/transit/previews/order_attribute_preview_page.dart +++ b/lib/ui/transit/previews/order_attribute_preview_page.dart @@ -15,7 +15,7 @@ class OrderAttributePreviewPage extends PreviewPage { @override Widget getItem(BuildContext context, OrderAttribute item) { - final mode = S.orderAttributeModeNames(item.mode.name); + final mode = S.orderAttributeModeName(item.mode.name); final defaultName = item.defaultOption?.name ?? S.orderAttributeMetaNoDefault; return ExpansionTile( title: ImporterColumnStatus( @@ -31,8 +31,8 @@ class OrderAttributePreviewPage extends PreviewPage { for (final option in item.items) ListTile( title: Text(option.name), - subtitle: OrderAttributeValueWidget(option.mode, option.modeValue), - trailing: option.isDefault ? OutlinedText(S.orderAttributeOptionIsDefault) : null, + subtitle: OrderAttributeValueWidget.build(option.mode, option.modeValue), + trailing: option.isDefault ? OutlinedText(S.orderAttributeOptionMetaDefault) : null, ), ], ); diff --git a/lib/ui/transit/previews/preview_page.dart b/lib/ui/transit/previews/preview_page.dart index 1fb78771..b5209c45 100644 --- a/lib/ui/transit/previews/preview_page.dart +++ b/lib/ui/transit/previews/preview_page.dart @@ -4,8 +4,8 @@ import 'package:possystem/helpers/formatter/formatter.dart'; import 'package:possystem/models/model.dart'; import 'package:possystem/translator.dart'; -import 'order_attribute_preview_page.dart'; import 'ingredient_preview_page.dart'; +import 'order_attribute_preview_page.dart'; import 'product_preview_page.dart'; import 'quantity_preview_page.dart'; import 'replenishment_preview_page.dart'; @@ -48,7 +48,7 @@ abstract class PreviewPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(S.transitPreviewImportTitle), + title: Text(S.transitImportPreviewTitle), leading: const CloseButton(), actions: [ TextButton( @@ -88,7 +88,7 @@ abstract class PreviewPage extends StatelessWidget { Widget getItem(BuildContext context, T item); Widget getHeader(BuildContext context) { - return const Text('注意:匯入後將會把下面沒列到的資料移除,請確認是否執行!'); + return Text(S.transitImportPreviewHeader); } } diff --git a/lib/ui/transit/previews/product_preview_page.dart b/lib/ui/transit/previews/product_preview_page.dart index 324aa26e..8dd40deb 100644 --- a/lib/ui/transit/previews/product_preview_page.dart +++ b/lib/ui/transit/previews/product_preview_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:possystem/components/meta_block.dart'; import 'package:possystem/helpers/formatter/formatter.dart'; import 'package:possystem/models/menu/product.dart'; +import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/translator.dart'; import 'preview_page.dart'; @@ -56,7 +57,7 @@ class ProductPreviewPage extends PreviewPage { subtitle: MetaBlock.withString( context, item.items.map((e) => e.name), - emptyText: S.menuProductListEmptyIngredient, + emptyText: S.menuProductEmptyIngredients, textStyle: textStyle, ), childrenPadding: const EdgeInsets.symmetric(horizontal: 8.0), @@ -87,8 +88,8 @@ class ProductPreviewPage extends PreviewPage { context, [ S.menuQuantityMetaAmount(quantity.amount), - S.menuQuantityMetaPrice(quantity.additionalPrice), - S.menuQuantityMetaCost(quantity.additionalCost), + S.menuQuantityMetaAdditionalPrice(quantity.additionalPrice.toCurrency()), + S.menuQuantityMetaAdditionalCost(quantity.additionalCost.toCurrency()), ], textStyle: textStyle, ), diff --git a/lib/ui/transit/previews/quantity_preview_page.dart b/lib/ui/transit/previews/quantity_preview_page.dart index ee33c4f9..21687ec3 100644 --- a/lib/ui/transit/previews/quantity_preview_page.dart +++ b/lib/ui/transit/previews/quantity_preview_page.dart @@ -19,13 +19,13 @@ class QuantityPreviewPage extends PreviewPage { status: item.statusName, ), subtitle: MetaBlock.withString(context, [ - S.quantityMetaProportion(item.defaultProportion), + S.stockQuantityMetaProportion(item.defaultProportion), ]), ); } @override Widget getHeader(BuildContext context) { - return const Text('匯入後,為了避免影響「菜單」的狀況,並不會把舊的份量移除。'); + return Text(S.transitImportPreviewQuantityHeader); } } diff --git a/lib/ui/transit/previews/replenishment_preview_page.dart b/lib/ui/transit/previews/replenishment_preview_page.dart index ba3de8de..b17d56c5 100644 --- a/lib/ui/transit/previews/replenishment_preview_page.dart +++ b/lib/ui/transit/previews/replenishment_preview_page.dart @@ -22,7 +22,7 @@ class ReplenishmentPreviewPage extends PreviewPage { name: item.name, status: item.statusName, ), - subtitle: Text(S.stockReplenishmentSubtitle(item.data.length)), + subtitle: Text(S.stockReplenishmentMetaAffect(item.data.length)), childrenPadding: const EdgeInsets.symmetric(horizontal: 8.0), children: _getData(context, item).toList(), ); diff --git a/lib/ui/transit/transit_order_list.dart b/lib/ui/transit/transit_order_list.dart index e4b1ebe7..49dd78f9 100644 --- a/lib/ui/transit/transit_order_list.dart +++ b/lib/ui/transit/transit_order_list.dart @@ -1,4 +1,5 @@ import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:possystem/components/linkify.dart'; @@ -36,7 +37,8 @@ class TransitOrderList extends StatelessWidget { ); } - /// 因為匯出時過大的資訊量會導致服務崩潰,所以先盡可能的計算大小。 + /// Since exporting too much data will cause the service to crash, + /// calculate the size as much as possible first. Widget _buildMemoryInfo(BuildContext context, OrderMetrics metrics) { final size = memoryPredictor(metrics); final level = size < 500000 // 500KB @@ -55,7 +57,7 @@ class TransitOrderList extends StatelessWidget { return IconButton( icon: const Icon(Icons.check_outlined), iconSize: 16.0, - tooltip: '容量剛好', + tooltip: S.transitOrderCapacityOk, style: FilledButton.styleFrom( backgroundColor: Colors.green[800], foregroundColor: Colors.white, @@ -68,7 +70,7 @@ class TransitOrderList extends StatelessWidget { return IconButton( icon: const Icon(Icons.warning_amber_outlined), iconSize: 16.0, - tooltip: '容量警告', + tooltip: S.transitOrderCapacityWarn, style: FilledButton.styleFrom( backgroundColor: Colors.yellow, foregroundColor: Colors.black, @@ -80,7 +82,7 @@ class TransitOrderList extends StatelessWidget { return IconButton( icon: const Icon(Icons.dangerous_outlined), iconSize: 16.0, - tooltip: '容量危險', + tooltip: S.transitOrderCapacityDanger, style: FilledButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, @@ -95,11 +97,11 @@ class TransitOrderList extends StatelessWidget { padding: const EdgeInsets.only(top: 4.0), child: Text(DateFormat.Hm(S.localeName).format(order.createdAt)), ), - title: Text(DateFormat('M月d日 HH:mm:ss').format(order.createdAt)), - subtitle: Text([ - '${order.productsCount} 份餐點', - '共 ${order.price.toCurrency()} 元', - ].join(MetaBlock.string)), + title: Text(S.transitOrderItemTitle(order.createdAt)), + subtitle: MetaBlock.withString(context, [ + S.transitOrderItemMetaProductCount(order.productsCount), + S.transitOrderItemMetaPrice(order.price.toCurrency()), + ]), trailing: const Icon(Icons.expand_outlined), onTap: () async { final detailedOrder = await Seller.instance.getOrder(order.id!); @@ -107,7 +109,7 @@ class TransitOrderList extends StatelessWidget { await showDialog( context: context, builder: (context) { - return SimpleDialog(title: const Text('訂單細節'), children: [ + return SimpleDialog(title: Text(S.transitOrderItemDialogTitle), children: [ Padding( padding: const EdgeInsets.all(8.0), child: formatOrder(detailedOrder), @@ -124,7 +126,7 @@ class TransitOrderList extends StatelessWidget { const style = TextStyle(fontWeight: FontWeight.bold); return SimpleDialog(children: [ Column(children: [ - Text('預估容量為:${getMemoryWithUnit(size)}'), + Text(S.transitOrderCapacityTitle(getMemoryWithUnit(size))), const SizedBox(height: 8.0), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -155,7 +157,7 @@ class TransitOrderList extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Linkify.fromString([ - '過高的容量可能會讓執行錯誤,建議分次執行,不要一次匯出太多筆。', + S.transitOrderCapacityContent, if (warning != null) '\n$warning', ].join()), ) diff --git a/lib/ui/transit/transit_order_range.dart b/lib/ui/transit/transit_order_range.dart index afedbd41..252e0a3d 100644 --- a/lib/ui/transit/transit_order_range.dart +++ b/lib/ui/transit/transit_order_range.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; +import 'package:possystem/components/style/date_range_picker.dart'; import 'package:possystem/helpers/util.dart'; -import 'package:possystem/settings/language_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; class TransitOrderRange extends StatefulWidget { @@ -22,8 +20,8 @@ class _TransitOrderRangeState extends State { Widget build(BuildContext context) { return ListTile( key: const Key('btn.edit_range'), - title: Text('${range.format(DateFormat.MMMd(S.localeName))} 的訂單'), - subtitle: Text('${range.duration.inDays} 天的資料'), + title: Text(S.transitOrderMetaRange(range.format(S.localeName))), + subtitle: Text(S.transitOrderMetaRangeDays(range.duration.inDays)), onTap: pickRange, trailing: const Icon(Icons.date_range_sharp), ); @@ -31,36 +29,11 @@ class _TransitOrderRangeState extends State { DateTimeRange get range => widget.notifier.value; - /// 對人類來說 5/1~5/2 代表兩天 - /// 對機器來說 5/1~5/2 代表一天(5/1 0:0 ~ 5/2 0:0) - /// 需要注意對機器和對人之間的轉換 void pickRange() async { - final result = await showDateRangePicker( - context: context, - initialDateRange: DateTimeRange( - start: range.start, - end: range.end.subtract(const Duration(days: 1)), - ), - initialEntryMode: DatePickerEntryMode.calendarOnly, - firstDate: DateTime(2021, 1), - lastDate: DateTime.now(), - locale: SettingsProvider.of().value, - // TODO: 應該會修掉這個 bug,需要注意 - // 另外包裝設計,因為選擇日期時,背景會使用有點半透明的 primary color - // 這個會讓本來預期的對比降低,將會看不清楚,所以調整 onPrimary 的顏色。 - builder: (context, dialog) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme.copyWith( - onPrimary: theme.textTheme.bodyMedium?.color, - ); - return Theme( - data: theme.copyWith(colorScheme: colorScheme), - child: dialog ?? const SizedBox.shrink(), - ); - }); + final result = await showMyDateRangePicker(context, range); if (result != null) { - _updateRange(result.start, result.end.add(const Duration(days: 1))); + _updateRange(result.start, result.end); } } diff --git a/lib/ui/transit/transit_page.dart b/lib/ui/transit/transit_page.dart index 8bf0e32e..f1e76d21 100644 --- a/lib/ui/transit/transit_page.dart +++ b/lib/ui/transit/transit_page.dart @@ -17,22 +17,19 @@ class TransitPage extends StatefulWidget { } class _TransitPageState extends State { - final selector = GlobalKey>(); + final selector = GlobalKey>(); @override Widget build(BuildContext context) { final body = ListView(children: [ - ChoiceChipWithHelp( + ChoiceChipWithHelp( key: selector, - values: TransitType.values, - selected: TransitType.order, - labels: const ['訂單記錄', '商家資訊'], - helpTexts: const [ - '訂單資訊可以讓你匯出到第三方位置後做更細緻的統計分析。', - '商家資訊通常是用來把菜單、庫存等資訊同步到第三方位置或用來匯入到另一台手機。', - ], + values: TransitCatalog.values, + selected: TransitCatalog.order, + labels: TransitCatalog.values.map((e) => S.transitCatalogName(e.name)).toList(), + helpTexts: TransitCatalog.values.map((e) => S.transitCatalogHelper(e.name)).toList(), ), - TextDivider(label: S.transitDescription), + TextDivider(label: S.transitMethodTitle), ListTile( key: const Key('transit.google_sheet'), leading: CircleAvatar( @@ -42,7 +39,7 @@ class _TransitPageState extends State { width: 24, ), ), - title: Text(S.transitMethod(TransitMethod.googleSheet.name)), + title: Text(S.transitMethodName(TransitMethod.googleSheet.name)), subtitle: Text(S.transitGSDescription), onTap: () => _goToStation(context, TransitMethod.googleSheet), ), @@ -52,8 +49,8 @@ class _TransitPageState extends State { radius: 24, child: Text('Text'), ), - title: Text(S.transitMethod(TransitMethod.plainText.name)), - subtitle: const Text('快速檢查、快速分享。'), + title: Text(S.transitMethodName(TransitMethod.plainText.name)), + subtitle: Text(S.transitPTDescription), onTap: () => _goToStation(context, TransitMethod.plainText), ), ]); diff --git a/lib/ui/transit/transit_station.dart b/lib/ui/transit/transit_station.dart index f35b2c68..05283712 100644 --- a/lib/ui/transit/transit_station.dart +++ b/lib/ui/transit/transit_station.dart @@ -7,11 +7,11 @@ import 'package:possystem/helpers/util.dart'; import 'package:possystem/translator.dart'; import 'google_sheet/views.dart' as gs; -import 'plain_text_widgets/views.dart' as pt; +import 'plain_text/views.dart' as pt; -enum TransitType { +enum TransitCatalog { order, - basic, + model, } enum TransitMethod { @@ -22,7 +22,7 @@ enum TransitMethod { class TransitStation extends StatefulWidget { final TransitMethod method; - final TransitType type; + final TransitCatalog catalog; final DateTimeRange? range; @@ -34,7 +34,7 @@ class TransitStation extends StatefulWidget { const TransitStation({ super.key, - required this.type, + required this.catalog, required this.method, this.exporter, this.notifier, @@ -48,7 +48,7 @@ class TransitStation extends StatefulWidget { class _TransitStationState extends State with TickerProviderStateMixin { final loading = GlobalKey(); - /// 這個是用來顯示「正在執行中」的資訊,避免匯出時被中斷。 + /// This is used to display the "in progress" information to avoid interruption during export. late final ValueNotifier stateNotifier; late final TabController tabController; @@ -59,7 +59,7 @@ class _TransitStationState extends State with TickerProviderStat key: loading, child: Scaffold( appBar: AppBar( - title: Text(S.transitMethod(widget.method.name)), + title: Text(S.transitMethodName(widget.method.name)), leading: const PopButton(), bottom: _buildAppBarBottom(), ), @@ -102,13 +102,13 @@ class _TransitStationState extends State with TickerProviderStat } PreferredSizeWidget? _buildAppBarBottom() { - switch (widget.type) { - case TransitType.basic: + switch (widget.catalog) { + case TransitCatalog.model: return TabBar( controller: tabController, tabs: [ - Tab(text: S.btnExport), - Tab(text: S.btnImport), + Tab(text: S.transitExportBtn), + Tab(text: S.transitImportBtn), ], ); default: @@ -117,8 +117,8 @@ class _TransitStationState extends State with TickerProviderStat } Widget _buildBody() { - switch (widget.type) { - case TransitType.basic: + switch (widget.catalog) { + case TransitCatalog.model: return TabBarView( key: const Key('transit.basic_tab'), controller: tabController, @@ -127,7 +127,7 @@ class _TransitStationState extends State with TickerProviderStat _buildScreen(_Combination.importBasic), ], ); - case TransitType.order: + case TransitCatalog.order: return _buildScreen(_Combination.exportOrder); } } diff --git a/pubspec.lock b/pubspec.lock index 9a773938..76c6b775 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + arb_glue: + dependency: "direct dev" + description: + name: arb_glue + sha256: b5eb0ab046bf6028a69a34c2c8536cf09cbba469a31f2b9748949dba9c70e35c + url: "https://pub.dev" + source: hosted + version: "0.6.0" archive: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index acd48d5e..c4d4b46a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,6 +78,8 @@ dev_dependencies: sqflite_common: ^2.5.4 # logging sqflite_common_ffi: ^2.3.3 + arb_glue: ^0.6.0 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -99,3 +101,11 @@ flutter_native_splash: color: "#5c98ff" fullscreen: true image: assets/logo.png + +arb_glue: + source: assets/l10n + destination: lib/l10n + author: Lu Shueh Chou + fileTemplate: 'app_{lang}.arb' + base: en + verbose: true diff --git a/test/components/snackbar_test.dart b/test/components/snackbar_test.dart index 01a50371..7a7aa922 100644 --- a/test/components/snackbar_test.dart +++ b/test/components/snackbar_test.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:possystem/components/style/snackbar.dart'; +import 'package:possystem/translator.dart'; + +import '../test_helpers/translator.dart'; void main() { group('Widget Snackbar', () { @@ -14,7 +17,6 @@ void main() { context, 'message', const Text('info'), - label: 'label', ); }, child: const Text('btn')); @@ -28,10 +30,14 @@ void main() { expect(find.text('message'), findsOneWidget); - await tester.tap(find.text('label')); + await tester.tap(find.text(S.actMoreInfo)); await tester.pumpAndSettle(); expect(find.text('info'), findsOneWidget); }); }); + + setUpAll(() { + initializeTranslator(); + }); } diff --git a/test/debug/random_gen_order_test.dart b/test/debug/random_gen_order_test.dart index 54c18cc0..5d37a322 100644 --- a/test/debug/random_gen_order_test.dart +++ b/test/debug/random_gen_order_test.dart @@ -16,7 +16,6 @@ import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/models/stock/ingredient.dart'; import 'package:possystem/models/stock/quantity.dart'; -import 'package:possystem/settings/currency_setting.dart'; import 'package:provider/provider.dart'; import '../mocks/mock_database.dart'; @@ -26,7 +25,7 @@ void main() { group('Random Generate Order', () { test('no gen if same date', () { final now = DateTime.now(); - final result = generateOrder( + final result = generateOrders( orderCount: 10, startFrom: now, endTo: now, @@ -37,7 +36,7 @@ void main() { test('default setting', () { final end = DateTime.now(); - final result = generateOrder( + final result = generateOrders( orderCount: 10, startFrom: end.subtract(const Duration(days: 1)), endTo: end, @@ -61,7 +60,13 @@ void main() { const btn = Key('test'); await tester.pumpWidget(ChangeNotifierProvider.value( value: Seller.instance, - child: const MaterialApp(home: RandomGenerateOrderButton(key: btn)), + child: MaterialApp(home: Builder(builder: (context) { + return TextButton( + key: btn, + onPressed: goGenerateRandomOrders(context), + child: const Text('test'), + ); + })), )); await tester.tap(find.byKey(btn)); @@ -81,8 +86,6 @@ void main() { }); setUpAll(() { - CurrencySetting().isInt = true; - final stock = Stock() ..replaceItems({ 'i-1': Ingredient(id: 'i-1', name: 'i-1'), diff --git a/test/debug/rerun_migration_test.dart b/test/debug/rerun_migration_test.dart index e89ce661..6bcec168 100644 --- a/test/debug/rerun_migration_test.dart +++ b/test/debug/rerun_migration_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:possystem/debug/rerun_migration.dart'; import 'package:possystem/services/database.dart'; @@ -8,17 +7,11 @@ import '../services/database_test.mocks.dart'; void main() { group('DEBUG', () { - testWidgets('Rerun the migration test', (tester) async { + test('Rerun the migration test', () async { dbMigrationActions.remove(Database.latestVersion); Database.instance.db = MockDatabase(); - await tester.pumpWidget(const MaterialApp( - home: Scaffold(body: RerunMigration()), - )); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.clear_all_sharp)); - await tester.pumpAndSettle(); + rerunMigration(); }); }); } diff --git a/test/helpers/formatter/google_sheet_formatter_test.dart b/test/helpers/formatter/google_sheet_formatter_test.dart index 1417ee8b..b349aa37 100644 --- a/test/helpers/formatter/google_sheet_formatter_test.dart +++ b/test/helpers/formatter/google_sheet_formatter_test.dart @@ -15,6 +15,7 @@ import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/models/stock/ingredient.dart'; import 'package:possystem/models/stock/quantity.dart'; import 'package:possystem/models/stock/replenishment.dart'; +import 'package:possystem/translator.dart'; import '../../test_helpers/translator.dart'; @@ -318,12 +319,12 @@ void main() { const c1Data = '- co1,true\n- co2,,5'; final items = formatter.format(Formattable.orderAttr, [ - ['c1', '折扣', c1Data], + ['c1', S.orderAttributeModeName('changeDiscount'), c1Data], ['c1', '', '- co1,20'], ['c2'], - ['c2', '折扣', '- a,b,10000'], + ['c2', S.orderAttributeModeName('changeDiscount'), '- a,b,10000'], ['c2', ''], - ['c3', '變價', '+'], + ['c3', S.orderAttributeModeName('changePrice'), '+'], ]); // should not changed diff --git a/test/helpers/formatter/plain_text_formatter_test.dart b/test/helpers/formatter/plain_text_formatter_test.dart index 89fff1d3..82cb4403 100644 --- a/test/helpers/formatter/plain_text_formatter_test.dart +++ b/test/helpers/formatter/plain_text_formatter_test.dart @@ -17,7 +17,6 @@ import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/models/stock/ingredient.dart'; import 'package:possystem/models/stock/quantity.dart'; import 'package:possystem/models/stock/replenishment.dart'; -import 'package:possystem/settings/currency_setting.dart'; import '../../test_helpers/translator.dart'; @@ -105,23 +104,23 @@ void main() { final items = format( Formattable.menu, - '本菜單共有 3 個產品種類、4 個產品。\n' + 'This menu has 3 categories, 4 products.\n' '\n' - '第1個種類叫做 A,共有 3 個產品。\n' - '第1個產品叫做 pA,其售價為 2 元,成本為 2 元,它沒有設定任何成份。\n' - '第2個產品叫做 pA2,其售價為 0 元,成本為 0 元,它的成份有 3 種:i1、i2、i5。' - '每份產品預設需要使用 2 個 i1,它還有 2 個不同份量:' - 'q1(每份產品改成使用 2 個並調整產品售價 2 元和成本 2 元)、' - 'q2(每份產品改成使用 5 個並調整產品售價 -5 元和成本 -5 元);' - '每份產品預設需要使用 0 個 i2,無法做份量調整;' - '每份產品預設需要使用 0 個 i5,它還有 1 個不同份量:' - 'q1(每份產品改成使用 1 個並調整產品售價 1 元和成本 1 元)。\n' - '第3個產品叫做 pA3,其售價為 0 元,成本為 0 元,它沒有設定任何成份。\n' + 'Category 1 is called A and it has 3 products.\n' + 'Product 1 is called pA, with price at \$2, cost \$2 and it has no ingredient.\n' + 'Product 2 is called pA2, with price at \$0, cost \$0 and it has 3 ingredients: i1、i2、i5.\n' + 'Each product requires 2 of i1 and it also has 2 different quantities :' + 'q1(quantity 2 with additional price \$2 and cost \$2)、' + 'q2(quantity 5 with additional price \$-5 and cost \$-5);' + '0 of i2 and it is unable to adjust quantity;' + '0 of i5 and it also has one different quantity :' + 'q1(quantity 1 with additional price \$1 and cost \$1).\n' + 'Product 3 is called pA3, with price at \$0, cost \$0 and it has no ingredient.\n' '\n' - '第2個種類叫做 B,沒有設定產品。\n' + 'Category 2 is called B and it has no product.\n' '\n' - '第3個種類叫做 C,共有 1 個產品。\n' - '第1個產品叫做 pA4,其售價為 0 元,成本為 0 元,它沒有設定任何成份。', + 'Category 3 is called C and it has one product.\n' + 'Product 1 is called pA4, with price at \$0, cost \$0 and it has no ingredient.', ); expect( @@ -153,11 +152,11 @@ void main() { final items = format( Formattable.stock, - '本庫存共有 3 種成份\n' + 'The inventory has 3 ingredients in total.\n' '\n' - '第1個成份叫做 i1,庫存現有 0.0 個\n' - '第2個成份叫做 i2,庫存現有 1.0 個\n' - '第3個成份叫做 i3,庫存現有 1.0 個,最大量有 2.0 個。', + 'Ingredient at 1 is called i1, with 0 amount.\n' + 'Ingredient at 2 is called i2, with 1 amount.\n' + 'Ingredient at 3 is called i3, with 1 amount, with a maximum of 2 pieces.', ); expect( @@ -177,12 +176,12 @@ void main() { final items = format( Formattable.quantities, - '共設定 4 種份量\n' + '4 quantities have been set.\n' '\n' - '第1種份量叫做 q1,預設會讓成分的份量乘以 1 倍。\n' - '第2種份量叫做 q2,預設會讓成分的份量乘以 2 倍。\n' - '第3種份量叫做 q3,預設會讓成分的份量乘以 0 倍。\n' - '第4種份量叫做 q4,預設會讓成分的份量乘以 0.5 倍。', + 'Quantity at 1 is called q1, which defaults to multiplying ingredient quantity by 1.\n' + 'Quantity at 2 is called q2, which defaults to multiplying ingredient quantity by 2.\n' + 'Quantity at 3 is called q3, which defaults to multiplying ingredient quantity by 0.\n' + 'Quantity at 4 is called q4, which defaults to multiplying ingredient quantity by 0.5.', ); expect( @@ -213,11 +212,10 @@ void main() { final items = format( Formattable.replenisher, - '共設定 2 種補貨方式\n' + '2 replenishment methods have been set.\n' '\n' - '第1種方式叫做 r1,它並不會調整庫存。\n' - '\n' - '第2種方式叫做 r2,它會調整3種成份的庫存:i1(20 個)、i2(-30 個)、i3(0.5 個)。', + 'Replenishment method at 1 is called r1, it will not adjust inventory.\n' + 'Replenishment method at 2 is called r2, it will adjust the inventory of 3 ingredients:i1(20)、i2(-30)、i3(0.5).', ); expect( @@ -229,7 +227,6 @@ void main() { }); test('order attributes', () { - CurrencySetting().isInt = true; final attrs = OrderAttributes(); attrs.replaceItems({ 'c1': OrderAttribute( @@ -282,15 +279,11 @@ void main() { final items = format( Formattable.orderAttr, - '共設定 3 種顧客屬性\n' - '\n' - '第1種屬性叫做 c1,屬於 變價 類型。它有 3 個選項:' - 'o1、o2(預設)、o3(選項的值為 20)\n' - '\n' - '第2種屬性叫做 c2,屬於 一般 類型。它並沒有設定選項。\n' + '3 customer attributes have been set.\n' '\n' - '第3種屬性叫做 c3,屬於 折扣 類型。它有 2 個選項:' - 'o1(預設,選項的值為 20)、o2(選項的值為 0)', + 'Attribute at 1 is called c1, belongs to Price Change type, it has 3 options:o1、o2(default)、o3(option value is 20).\n' + 'Attribute at 2 is called c2, belongs to Normal type, it has no options.\n' + 'Attribute at 3 is called c3, belongs to Discount type, it has 2 options:o1(default,option value is 20)、o2(option value is 0).', ); expect( diff --git a/test/helpers/util_test.dart b/test/helpers/util_test.dart index 34223b2e..39b3066f 100644 --- a/test/helpers/util_test.dart +++ b/test/helpers/util_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:intl/date_symbol_data_local.dart'; import 'package:possystem/helpers/util.dart'; void main() { @@ -32,6 +33,13 @@ void main() { expect(date.second, equals(33)); }); + test('#formatCompact', () { + initializeDateFormatting('en', null); + DateTimeRange range = Util.getDateRange(now: DateTime.utc(2021, 6, 14), days: 3); + + expect(range.formatCompact('en'), equals('20210614 - 20210616')); + }); + testWidgets('#handleSnapshot error', (WidgetTester tester) async { final f = Util.handleSnapshot((context, data) => const SizedBox.shrink()); const err = AsyncSnapshot.withError(ConnectionState.done, 'test'); diff --git a/test/image_gallery_page_test.dart b/test/image_gallery_page_test.dart index 1701e43a..1df8d305 100644 --- a/test/image_gallery_page_test.dart +++ b/test/image_gallery_page_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:possystem/models/xfile.dart'; import 'package:possystem/routes.dart'; +import 'package:possystem/translator.dart'; import 'package:possystem/ui/image_gallery_page.dart'; import 'test_helpers/file_mocker.dart'; @@ -78,12 +79,12 @@ void main() { await tester.longPress(find.byKey(const Key('image_gallery.0'))); await tester.pumpAndSettle(); - expect(find.text('刪除所選'), findsOneWidget); + expect(find.text(S.imageGallerySelectionTitle), findsOneWidget); // disable selecting await tester.tap(find.byKey(const Key('image_gallery.cancel'))); await tester.pumpAndSettle(); - expect(find.text('刪除所選'), findsNothing); + expect(find.text(S.imageGallerySelectionTitle), findsNothing); expect(find.text('go'), findsNothing); // leave diff --git a/test/models/initialize_test.dart b/test/models/initialize_test.dart index 01d7471e..e0564efa 100644 --- a/test/models/initialize_test.dart +++ b/test/models/initialize_test.dart @@ -338,7 +338,7 @@ void main() { expect(c1.type.name, equals('cartesian')); expect( c1.metrics, - equals([OrderMetricType.cost, OrderMetricType.revenue]), + equals([OrderMetricType.cost, OrderMetricType.profit]), ); expect(c2.name, equals('c-2')); expect(c2.type.name, equals('circular')); diff --git a/test/models/repository_test.dart b/test/models/repository_test.dart index 91e02184..c05fd373 100644 --- a/test/models/repository_test.dart +++ b/test/models/repository_test.dart @@ -22,15 +22,15 @@ void main() { test('Cashier', () async { when(cache.get(any)).thenReturn(null); - final currency = CurrencySetting()..initialize(); + CurrencySetting.instance.initialize(); final cashier = Cashier(); // ignore: invalid_use_of_protected_member - expect(currency.hasListeners, isTrue); + expect(CurrencySetting.instance.hasListeners, isTrue); cashier.dispose(); // ignore: invalid_use_of_protected_member - expect(currency.hasListeners, isFalse); + expect(CurrencySetting.instance.hasListeners, isFalse); }); }); @@ -43,7 +43,7 @@ void main() { final dirtyData = [ {'count': '', 'unit': 2} ]; - CurrencySetting().unitList = [1, 2, 3]; + CurrencySetting.instance.unitList = [1, 2, 3]; final cashier = Cashier(); await cashier.deleteFavorite(0); @@ -63,7 +63,6 @@ void main() { group('Cashier', () { test('should handler error parsing', () async { final cashier = Cashier(); - CurrencySetting(); CurrencySetting.instance.unitList = [1]; await cashier.setCurrent([ @@ -74,7 +73,6 @@ void main() { }); test('#findPossibleChange', () async { - CurrencySetting(); final cashier = Cashier(); await cashier.setCurrent([ {'unit': 10}, @@ -103,7 +101,6 @@ void main() { }); test('#update', () async { - CurrencySetting(); final cashier = Cashier(); await cashier.setCurrent([ {'unit': 5, 'count': 3}, @@ -115,7 +112,6 @@ void main() { }); test('#paid', () async { - CurrencySetting(); final cashier = Cashier(); await cashier.setCurrent([ {'unit': 5}, diff --git a/test/my_app_test.dart b/test/my_app_test.dart index edc4e6bf..b5e06895 100644 --- a/test/my_app_test.dart +++ b/test/my_app_test.dart @@ -4,9 +4,7 @@ import 'package:mockito/mockito.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/my_app.dart'; -import 'package:possystem/settings/language_setting.dart'; import 'package:possystem/settings/settings_provider.dart'; -import 'package:possystem/settings/theme_setting.dart'; import 'package:provider/provider.dart'; import 'mocks/mock_cache.dart'; @@ -20,19 +18,13 @@ void main() { when(cache.get('tutorial.home.order_attr')).thenReturn(true); await Firebase.initializeApp(); - final settings = SettingsProvider([ - ThemeSetting(), - LanguageSetting(), - ]); final app = MultiProvider( providers: [ - ChangeNotifierProvider.value(value: settings), + ChangeNotifierProvider.value(value: SettingsProvider.instance), ChangeNotifierProvider.value(value: Menu()), ChangeNotifierProvider.value(value: OrderAttributes()), ], - builder: (_, __) => MyApp( - settings: settings, - ), + builder: (_, __) => const MyApp(), ); await tester.pumpWidget(app); diff --git a/test/settings/currency_settting_test.dart b/test/settings/currency_settting_test.dart index 972b5992..3a35f97e 100644 --- a/test/settings/currency_settting_test.dart +++ b/test/settings/currency_settting_test.dart @@ -9,18 +9,17 @@ void main() { test('set', () { when(cache.set(any, any)).thenAnswer((_) => Future.value(true)); - CurrencySetting().updateRemotely(CurrencyTypes.usd); + CurrencySetting.instance.updateRemotely(CurrencyTypes.usd); verify(cache.set('currency', 1)); }); test('initialize', () { when(cache.get(any)).thenReturn(1); - final currency = CurrencySetting(); - currency.initialize(); + CurrencySetting.instance.initialize(); - expect(currency.isInt, false); + expect(CurrencySetting.instance.isInt, false); }); setUpAll(() { diff --git a/test/settings/language_setting_test.dart b/test/settings/language_setting_test.dart index aa2f6fd2..1019682d 100644 --- a/test/settings/language_setting_test.dart +++ b/test/settings/language_setting_test.dart @@ -4,14 +4,13 @@ import 'package:possystem/settings/language_setting.dart'; void main() { group('Language Setting', () { test('Parse language', () { - final languageSetting = LanguageSetting(); - - expect(languageSetting.parseLanguage(''), isNull); - expect(languageSetting.parseLanguage('something'), equals(LanguageSetting.defaultLanguage)); - expect(languageSetting.parseLanguage('zh'), equals(LanguageSetting.defaultLanguage)); - expect(languageSetting.parseLanguage('zh_TW'), equals(LanguageSetting.defaultLanguage)); - expect(languageSetting.parseLanguage('zh_Hant'), equals(LanguageSetting.defaultLanguage)); - expect(languageSetting.parseLanguage('zh_Hant_TW'), equals(LanguageSetting.defaultLanguage)); + final l = LanguageSetting.instance; + expect(l.parseLanguage(''), isNull); + expect(l.parseLanguage('something'), equals(Language.en)); + expect(l.parseLanguage('zh'), equals(Language.zhTW)); + expect(l.parseLanguage('zh_TW'), equals(Language.zhTW)); + expect(l.parseLanguage('zh_Hant'), equals(Language.zhTW)); + expect(l.parseLanguage('zh_Hant_TW'), equals(Language.zhTW)); }); }); } diff --git a/test/test_helpers/order_setter.dart b/test/test_helpers/order_setter.dart index eea68a5b..3438179c 100644 --- a/test/test_helpers/order_setter.dart +++ b/test/test_helpers/order_setter.dart @@ -150,9 +150,9 @@ class OrderSetter { )).thenAnswer((_) => Future.value([ { "count": orders.length, - "price": orders.fold(0, (pre, e) => pre + e.price), + "revenue": orders.fold(0, (pre, e) => pre + e.price), "cost": orders.fold(0, (pre, e) => pre + e.cost), - "revenue": orders.fold(0, (pre, e) => pre + e.revenue), + "profit": orders.fold(0, (pre, e) => pre + e.profit), } ])); diff --git a/test/test_helpers/translator.dart b/test/test_helpers/translator.dart index f32db361..8b43f7b2 100644 --- a/test/test_helpers/translator.dart +++ b/test/test_helpers/translator.dart @@ -1,14 +1,14 @@ +import 'package:flutter_gen/gen_l10n/app_localizations_en.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:possystem/translator.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations_zh.dart'; var _initialized = false; void initializeTranslator() { if (!_initialized) { _initialized = true; - S = AppLocalizationsZh(); + S = AppLocalizationsEn(); Intl.systemLocale = S.localeName; Intl.defaultLocale = S.localeName; initializeDateFormatting(S.localeName); diff --git a/test/ui/analysis/analysis_view_test.dart b/test/ui/analysis/analysis_view_test.dart index 1bdf96bd..a6f53009 100644 --- a/test/ui/analysis/analysis_view_test.dart +++ b/test/ui/analysis/analysis_view_test.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; -import 'package:intl/intl.dart'; import 'package:mockito/mockito.dart'; import 'package:possystem/constants/icons.dart'; import 'package:possystem/helpers/util.dart'; @@ -9,9 +8,6 @@ import 'package:possystem/models/analysis/analysis.dart'; import 'package:possystem/models/analysis/chart.dart'; import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/routes.dart'; -import 'package:possystem/settings/currency_setting.dart'; -import 'package:possystem/settings/language_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/ui/analysis/analysis_view.dart'; import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -29,16 +25,8 @@ void main() { when(cache.get( argThat(predicate((key) => key.startsWith('tutorial.'))), )).thenReturn(true); - final settings = SettingsProvider([ - LanguageSetting(), - CurrencySetting(), - ]); - - return MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: settings), - ChangeNotifierProvider.value(value: Seller.instance), - ], + return ChangeNotifierProvider.value( + value: Seller.instance, builder: (_, __) => MaterialApp.router( routerConfig: GoRouter( routes: [ @@ -58,7 +46,7 @@ void main() { void mockGetChart() { when(database.query( any, - columns: argThat(contains('SUM(price) price'), named: 'columns'), + columns: argThat(contains('SUM(price) revenue'), named: 'columns'), orderBy: anyNamed('orderBy'), escapeTable: anyNamed('escapeTable'), groupBy: anyNamed('groupBy'), @@ -110,7 +98,7 @@ void main() { type: AnalysisChartType.cartesian, ignoreEmpty: false, target: OrderMetricTarget.order, - metrics: const [OrderMetricType.price], + metrics: const [OrderMetricType.revenue], targetItems: [], ), }); @@ -142,7 +130,7 @@ void main() { expect(chart.type.name, equals('cartesian')); expect(chart.ignoreEmpty, equals(false)); expect(chart.target, OrderMetricTarget.order); - expect(chart.metrics, equals(const [OrderMetricType.price])); + expect(chart.metrics, equals(const [OrderMetricType.revenue])); expect(chart.targetItems, isEmpty); expect(chart.index, 0); @@ -159,8 +147,7 @@ void main() { now: DateTime.now().subtract(const Duration(days: 7)), days: 7, ); - final format = DateFormat.MMMd('zh_TW'); - expect(find.text(range.format(format)), findsOneWidget); + expect(find.text(range.format('en')), findsOneWidget); await tester.tap(find.byIcon(Icons.arrow_back_ios_new_sharp)); await tester.pump(const Duration(milliseconds: 50)); @@ -169,7 +156,7 @@ void main() { now: DateTime.now().subtract(const Duration(days: 14)), days: 7, ); - expect(find.text(range.format(format)), findsOneWidget); + expect(find.text(range.format('en')), findsOneWidget); await tester.tap(find.byIcon(Icons.arrow_forward_ios_sharp)); await tester.pump(const Duration(milliseconds: 50)); @@ -178,14 +165,14 @@ void main() { now: DateTime.now().subtract(const Duration(days: 7)), days: 7, ); - expect(find.text(range.format(format)), findsOneWidget); + expect(find.text(range.format('en')), findsOneWidget); // select date range await tester.tap(find.byKey(const Key('anal.chart_range'))); await tester.pumpAndSettle(); await tester.tap(find.text('OK')); await tester.pump(const Duration(milliseconds: 50)); - expect(find.text(range.format(format)), findsAtLeastNWidgets(1)); + expect(find.text(range.format('en')), findsAtLeastNWidgets(1)); }); setUpAll(() { diff --git a/test/ui/analysis/history_page_test.dart b/test/ui/analysis/history_page_test.dart index 482333a6..221d0f91 100644 --- a/test/ui/analysis/history_page_test.dart +++ b/test/ui/analysis/history_page_test.dart @@ -4,9 +4,6 @@ import 'package:go_router/go_router.dart'; import 'package:mockito/mockito.dart'; import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/routes.dart'; -import 'package:possystem/settings/currency_setting.dart'; -import 'package:possystem/settings/language_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/analysis/history_page.dart'; import 'package:provider/provider.dart'; @@ -25,14 +22,8 @@ void main() { when(cache.get( argThat(predicate((key) => key.startsWith('tutorial.'))), )).thenReturn(true); - final settings = SettingsProvider([ - LanguageSetting(), - CurrencySetting(), - ]); - return MultiProvider( providers: [ - ChangeNotifierProvider.value(value: settings), ChangeNotifierProvider.value(value: Seller.instance), ], builder: (_, __) => MaterialApp.router( @@ -138,7 +129,7 @@ void main() { await tester.pumpAndSettle(); // change format - await tester.tap(find.text(S.analysisCalendarMonth)); + await tester.tap(find.text(S.singleMonth)); await tester.pumpAndSettle(); expect(find.text('50'), findsOneWidget); @@ -163,10 +154,10 @@ void main() { await tester.tap(find.byKey(const Key('history.export'))); await tester.pumpAndSettle(); // dropdown have multiple child for items - await tester.tap(find.text(S.transitMethod('plainText')).last); + await tester.tap(find.text(S.transitMethodName('plainText')).last); await tester.pumpAndSettle(); - expect(find.text(S.transitMethod('plainText')), findsOneWidget); + expect(find.text(S.transitMethodName('plainText')), findsOneWidget); }); setUpAll(() { diff --git a/test/ui/analysis/widgets/chart_card_view_test.dart b/test/ui/analysis/widgets/chart_card_view_test.dart index 198906ce..e2304024 100644 --- a/test/ui/analysis/widgets/chart_card_view_test.dart +++ b/test/ui/analysis/widgets/chart_card_view_test.dart @@ -92,8 +92,8 @@ void main() { table: equals('(SELECT CAST((createdAt - $today) / 3600 AS INT) day, ' '* FROM order_records WHERE createdAt BETWEEN $today AND $tomorrow) t'), rows: [ - {'day': 1, 'price': 1.1, 'revenue': 2.2}, - {'day': 3, 'price': 1.2, 'revenue': 2.3}, + {'day': 1, 'revenue': 1.1, 'profit': 2.2}, + {'day': 3, 'revenue': 1.2, 'profit': 2.3}, ]); await tester.pumpWidget(buildApp(Chart( @@ -122,17 +122,17 @@ void main() { final chip = find.byKey(Key('chart.metrics.${type.name}')).evaluate(); return (chip.single.widget as ChoiceChip).selected; }); - expect(types.map((e) => e.name).join(','), equals('price,cost,count')); + expect(types.map((e) => e.name).join(','), equals('revenue,cost,count')); await tester.tap(find.byKey(const Key('chart.metrics.cost'))); await tester.pumpAndSettle(); - expect(types.map((e) => e.name).join(','), equals('price,count')); + expect(types.map((e) => e.name).join(','), equals('revenue,count')); await tester.dragFrom(const Offset(500, 500), const Offset(0, -500)); await tester.tap(find.byKey(const Key('chart.target.product'))); await tester.pumpAndSettle(); // reset - expect(types.map((e) => e.name).join(','), equals('price')); + expect(types.map((e) => e.name).join(','), equals('revenue')); await tester.tap(find.byKey(const Key('chart.metrics.cost'))); await tester.dragFrom(const Offset(500, 500), const Offset(0, -500)); @@ -256,15 +256,15 @@ void main() { ), ); mockGetMetricsInPeriod(rows: [ - {'day': 1, 'price': 1.1, 'count': 2}, - {'day': 2, 'price': 2.2, 'count': 3}, + {'day': 1, 'revenue': 1.1, 'count': 2}, + {'day': 2, 'revenue': 2.2, 'count': 3}, ]); await tester.pumpWidget(buildApp( Chart( type: AnalysisChartType.cartesian, id: 'test', - metrics: [OrderMetricType.price, OrderMetricType.count], + metrics: [OrderMetricType.revenue, OrderMetricType.count], ), range: range, )); @@ -299,8 +299,8 @@ void main() { testWidgets('all types and ignore drag', (tester) async { mockGetMetricsInPeriod(rows: [ - {'day': 1, 'price': 1.1, 'revenue': 1.1, 'cost': 1.1, 'count': 2}, - {'day': 2, 'price': 2.2, 'revenue': 2.2, 'cost': 2.2, 'count': 3}, + {'day': 1, 'revenue': 1.1, 'profit': 1.1, 'cost': 1.1, 'count': 2}, + {'day': 2, 'revenue': 2.2, 'profit': 2.2, 'cost': 2.2, 'count': 3}, ]); final chart = Chart( @@ -409,7 +409,7 @@ void main() { await tester.tap(find.byKey(const Key('chart.target.attribute'))); await tester.pumpAndSettle(); - expect(find.byKey(const Key('chart.metric.price')), findsNothing); + expect(find.byKey(const Key('chart.metric.revenue')), findsNothing); await tester.dragFrom(const Offset(500, 500), const Offset(0, -300)); await tester.dragFrom(const Offset(500, 500), const Offset(0, -300)); diff --git a/test/ui/analysis/widgets/chart_range_page_test.dart b/test/ui/analysis/widgets/chart_range_page_test.dart index 6aacbf6c..0f0dab5c 100644 --- a/test/ui/analysis/widgets/chart_range_page_test.dart +++ b/test/ui/analysis/widgets/chart_range_page_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:intl/intl.dart'; import 'package:possystem/helpers/util.dart'; +import 'package:possystem/translator.dart'; import 'package:possystem/ui/analysis/widgets/chart_range_page.dart'; import '../../../test_helpers/translator.dart'; @@ -16,7 +16,6 @@ void main() { start: today.subtract(const Duration(days: 7)), end: today, ); - final format = DateFormat.MMMd('zh_TW'); DateTimeRange? selected; await tester.pumpWidget( @@ -38,30 +37,30 @@ void main() { await tester.tap(find.text('go')); await tester.pumpAndSettle(); - expect(find.text('最近7日'), findsOneWidget); - expect(find.text(range.format(format)), findsAtLeastNWidgets(1)); - expect(find.text('本週'), findsOneWidget); - expect(find.text('上週'), findsOneWidget); + expect(find.text(S.analysisChartRangeLast7Days), findsOneWidget); + expect(find.text(range.format('en')), findsAtLeastNWidgets(1)); + expect(find.text(S.analysisChartRangeThisWeek), findsOneWidget); + expect(find.text(S.analysisChartRangeLastWeek), findsOneWidget); - await tester.tap(find.text('日期')); + await tester.tap(find.text(S.analysisChartRangeTabName('day'))); await tester.pumpAndSettle(); - expect(find.text('昨日'), findsOneWidget); - await tester.tap(find.text('今日')); + expect(find.text(S.analysisChartRangeYesterday), findsOneWidget); + await tester.tap(find.text(S.analysisChartRangeToday)); await tester.pumpAndSettle(); - await tester.tap(find.text('月')); + await tester.tap(find.text(S.analysisChartRangeTabName('month'))); await tester.pumpAndSettle(); - expect(find.text('最近30日'), findsOneWidget); - expect(find.text('本月'), findsOneWidget); - expect(find.text('上月'), findsOneWidget); + expect(find.text(S.analysisChartRangeLast30Days), findsOneWidget); + expect(find.text(S.analysisChartRangeThisMonth), findsOneWidget); + expect(find.text(S.analysisChartRangeLastMonth), findsOneWidget); - await tester.tap(find.text('自訂')); + await tester.tap(find.text(S.analysisChartRangeTabName('custom'))); await tester.pumpAndSettle(); await tester.tap( - find.text(DateTimeRange(start: today, end: tomorrow).format(format)), + find.text(DateTimeRange(start: today, end: tomorrow).format('en')), ); await tester.pumpAndSettle(); await tester.tap(find.text('OK'), warnIfMissed: false); diff --git a/test/ui/analysis/widgets/goals_card_view_test.dart b/test/ui/analysis/widgets/goals_card_view_test.dart index 192aaec2..933e8368 100644 --- a/test/ui/analysis/widgets/goals_card_view_test.dart +++ b/test/ui/analysis/widgets/goals_card_view_test.dart @@ -4,11 +4,13 @@ import 'package:mockito/mockito.dart'; import 'package:possystem/helpers/analysis/ema_calculator.dart'; import 'package:possystem/helpers/util.dart'; import 'package:possystem/models/repository/seller.dart'; +import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/ui/analysis/widgets/goals_card_view.dart'; import 'package:visibility_detector/visibility_detector.dart'; import '../../../mocks/mock_cache.dart'; import '../../../mocks/mock_database.dart'; +import '../../../test_helpers/translator.dart'; void main() { Future>> mockQuery(int begin, int cease) { @@ -33,8 +35,8 @@ void main() { { 'day': i, 'count': i, - 'price': i * 1.1, - 'revenue': i * 1.2, + 'revenue': i * 1.1, + 'profit': i * 1.2, 'cost': i * 1.3, } ]); @@ -51,12 +53,12 @@ void main() { const calculator = EMACalculator(20); final data = { 'count': calculator.calculate([for (var i = 0; i < 20; i++) i]), - 'price': calculator.calculate([for (var i = 0; i < 20; i++) i * 1.1]), - 'revenue': calculator.calculate([for (var i = 0; i < 20; i++) i * 1.2]), + 'revenue': calculator.calculate([for (var i = 0; i < 20; i++) i * 1.1]), + 'profit': calculator.calculate([for (var i = 0; i < 20; i++) i * 1.2]), }; findText('20/${data['count']!.toInt()}'); - findText('22/${data['price']!.prettyString()}'); - findText('24/${data['revenue']!.prettyString()}'); + findText('22/${data['revenue']!.toCurrency()}'); + findText('24/${data['profit']!.toCurrency()}'); verify(mockQuery(fortyDaysAgo, tomorrow)); // notify the seller to update the view @@ -84,6 +86,7 @@ void main() { setUpAll(() { initializeDatabase(); initializeCache(); + initializeTranslator(); VisibilityDetectorController.instance.updateInterval = Duration.zero; }); }); diff --git a/test/ui/analysis/widgets/history_order_list_test.dart b/test/ui/analysis/widgets/history_order_list_test.dart index 8494a3f4..d0bd9ec0 100644 --- a/test/ui/analysis/widgets/history_order_list_test.dart +++ b/test/ui/analysis/widgets/history_order_list_test.dart @@ -10,11 +10,9 @@ import 'package:possystem/models/objects/order_object.dart'; import 'package:possystem/models/repository/menu.dart'; import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/routes.dart'; -import 'package:possystem/settings/currency_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/analysis/widgets/history_order_list.dart'; -import 'package:provider/provider.dart'; +import 'package:possystem/ui/analysis/widgets/history_order_modal.dart'; import '../../../mocks/mock_cache.dart'; import '../../../mocks/mock_database.dart'; @@ -30,22 +28,19 @@ void main() { when(cache.get( argThat(predicate((String e) => e.startsWith('tutorial.'))), )).thenReturn(true); - return ChangeNotifierProvider.value( - value: SettingsProvider([CurrencySetting()]), - child: MaterialApp.router( - routerConfig: GoRouter( - routes: [ - GoRoute( - path: '/', - builder: (_, __) { - return Material( - child: HistoryOrderList(notifier: notifier), - ); - }, - routes: Routes.routes, - ), - ], - ), + return MaterialApp.router( + routerConfig: GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, __) { + return Material( + child: HistoryOrderList(notifier: notifier), + ); + }, + routes: Routes.routes, + ), + ], ), ); } @@ -86,7 +81,7 @@ void main() { // should not set progress if empty result expect(find.byType(CircularLoading), findsNothing); - expect(find.text('查無點餐紀錄'), findsOneWidget); + expect(find.text(S.orderLoaderEmpty), findsOneWidget); expect(loadCount, equals(1)); }); @@ -151,7 +146,7 @@ void main() { expect(find.text('oao-1'), findsOneWidget); expect(find.text('p-2'), findsOneWidget); - await tester.tap(find.text(S.orderObjectTotalPrice('40'))); + await tester.tap(find.text(S.orderObjectViewPriceTotal('40'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('pop'))); await tester.pumpAndSettle(); @@ -200,6 +195,15 @@ void main() { verify(txn.delete(any, where: anyNamed('where'))).called(4); }); + testWidgets('order not found', (tester) async { + when(database.query(any, where: argThat(equals('id = 666'), named: 'where'))).thenAnswer((_) => Future.value([])); + + await tester.pumpWidget(const MaterialApp(home: HistoryOrderModal(666))); + await tester.pumpAndSettle(); + + expect(find.text(S.analysisHistoryOrderNotFound), findsOneWidget); + }); + setUpAll(() { initializeCache(); initializeTranslator(); diff --git a/test/ui/cashier/cashier_view_test.dart b/test/ui/cashier/cashier_view_test.dart index d909c9f5..f42eb83e 100644 --- a/test/ui/cashier/cashier_view_test.dart +++ b/test/ui/cashier/cashier_view_test.dart @@ -4,8 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:mockito/mockito.dart'; import 'package:possystem/models/repository/cashier.dart'; import 'package:possystem/routes.dart'; -import 'package:possystem/settings/currency_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; +import 'package:possystem/translator.dart'; import 'package:possystem/ui/cashier/cashier_view.dart'; import 'package:provider/provider.dart'; @@ -24,20 +23,15 @@ void main() { )).thenReturn(true); when(storage.get(any, any)).thenAnswer((_) => Future.value({})); - final settings = SettingsProvider([CurrencySetting.instance]); - - return MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: Cashier.instance), - ChangeNotifierProvider.value(value: settings), - ], + return ChangeNotifierProvider.value( + value: Cashier.instance, builder: (_, __) => MaterialApp.router( routerConfig: GoRouter(routes: [ GoRoute( path: '/', builder: (_, __) => const Scaffold(body: CashierView()), routes: Routes.routes, - ) + ), ]), ), ); @@ -77,7 +71,7 @@ void main() { await tester.tap(find.byKey(const Key('cashier.surplus'))); await tester.pumpAndSettle(); - expect(find.text('尚未設定,請點選右上角「設為預設」'), findsOneWidget); + expect(find.text(S.cashierSurplusErrorEmptyDefault), findsOneWidget); }); testWidgets('should execute surplus', (tester) async { @@ -128,7 +122,7 @@ void main() { String number, [ String action = 'confirm', ]) async { - await tester.tap(find.text('幣值:$unit')); + await tester.tap(find.text(S.cashierUnitLabel(unit))); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextFormField), number); await tester.tap(find.byKey(Key('slider_dialog.$action'))); @@ -177,7 +171,6 @@ void main() { }); setUp(() { - CurrencySetting(); Cashier(); }); diff --git a/test/ui/cashier/changer_page_test.dart b/test/ui/cashier/changer_page_test.dart index ac8f640d..9ce9e5c5 100644 --- a/test/ui/cashier/changer_page_test.dart +++ b/test/ui/cashier/changer_page_test.dart @@ -6,6 +6,7 @@ import 'package:possystem/constants/icons.dart'; import 'package:possystem/models/repository/cashier.dart'; import 'package:possystem/routes.dart'; import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; import 'package:possystem/ui/cashier/changer_page.dart'; import 'package:provider/provider.dart'; @@ -82,7 +83,7 @@ void main() { await tester.tap(find.byKey(const Key('changer.apply'))); await tester.pumpAndSettle(); - expect(find.text('請選擇要套用的組合'), findsOneWidget); + expect(find.text(S.cashierChangerErrorNoSelection), findsOneWidget); await tester.tap(find.byKey(const Key('changer.favorite.0'))); await tester.tap(find.byKey(const Key('changer.apply'))); @@ -163,7 +164,10 @@ void main() { await tester.pumpAndSettle(); expect( - find.text('4 個 10 元沒辦法換\n- 8 個 5 元\n- 1 個 5 元\n- 5 個 1 元'), + find.text('${S.cashierChangerErrorInvalidHead(4, '10')}\n' + ' • ${S.cashierChangerErrorInvalidBody(8, '5')}\n' + ' • ${S.cashierChangerErrorInvalidBody(1, '5')}\n' + ' • ${S.cashierChangerErrorInvalidBody(5, '1')}'), findsOneWidget, ); @@ -187,7 +191,6 @@ void main() { }); setUp(() { - CurrencySetting(); Cashier(); }); diff --git a/test/ui/home/features_page_test.dart b/test/ui/home/features_page_test.dart index 67e72391..4ccf0ab2 100644 --- a/test/ui/home/features_page_test.dart +++ b/test/ui/home/features_page_test.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:mockito/mockito.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:possystem/routes.dart'; +import 'package:possystem/settings/language_setting.dart'; import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; -import 'package:possystem/ui/home/features_page.dart'; import 'package:provider/provider.dart'; import '../../mocks/mock_auth.dart'; @@ -17,11 +19,18 @@ import '../../test_helpers/translator.dart'; void main() { group('Features Page', () { Widget buildApp() { - final setting = SettingsProvider(SettingsProvider.allSettings); - return ChangeNotifierProvider.value( - value: setting, - builder: (_, __) => const MaterialApp(home: FeaturesPage()), + value: SettingsProvider.instance..initialize(), + builder: (_, __) => MaterialApp.router( + locale: LanguageSetting.defaultValue.locale, + routerConfig: GoRouter(initialLocation: Routes.features, routes: [ + GoRoute( + path: '/', + builder: (ctx, state) => const Text('Home'), + routes: Routes.routes, + ), + ]), + ), ); } @@ -54,7 +63,7 @@ void main() { controller.add(user); await tester.pumpAndSettle(); - expect(find.text('HI,TestUser'), findsOneWidget); + expect(find.text(S.settingWelcome('TestUser')), findsOneWidget); // sign out await tester.tap(find.byKey(const Key('feature.sign_out'))); @@ -68,60 +77,68 @@ void main() { testWidgets('select theme', (tester) async { await tester.pumpWidget(buildApp()); - expect(find.text(S.settingThemeTypes('system')), findsOneWidget); + expect(find.text(S.settingThemeName('system')), findsOneWidget); await tester.tap(find.byKey(const Key('feature.theme'))); await tester.pumpAndSettle(); - await tester.tap(find.text(S.settingThemeTypes('dark'))); + await tester.tap(find.text(S.settingThemeName('dark'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('pop'))); await tester.pumpAndSettle(); - expect(find.text(S.settingThemeTypes('dark')), findsOneWidget); - verify(cache.set(any, 2)); + expect(find.text(S.settingThemeName('dark')), findsOneWidget); + verify(cache.set(any, ThemeMode.dark.index)); }); testWidgets('select language', (tester) async { await tester.pumpWidget(buildApp()); - expect(find.text('繁體中文'), findsOneWidget); + expect(find.text('English'), findsOneWidget); await tester.tap(find.byKey(const Key('feature.language'))); await tester.pumpAndSettle(); - await tester.tap(find.text('English')); + await tester.tap(find.text('繁體中文')); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('pop'))); await tester.pumpAndSettle(); - expect(find.text('English'), findsOneWidget); - verify(cache.set(any, 'en')); + expect(find.text('繁體中文'), findsOneWidget); + verify(cache.set(any, 'zh_TW')); }); - testWidgets('select outlook_order', (tester) async { + testWidgets('select order_outlook', (tester) async { await tester.pumpWidget(buildApp()); - expect(find.text(S.settingOrderOutlookTypes('slidingPanel')), findsOneWidget); + expect(find.text(S.settingOrderOutlookName('slidingPanel')), findsOneWidget); - await tester.tap(find.byKey(const Key('feature.outlook_order'))); + await tester.tap(find.byKey(const Key('feature.order_outlook'))); await tester.pumpAndSettle(); - await tester.tap(find.text(S.settingOrderOutlookTypes('singleView'))); + await tester.tap(find.text(S.settingOrderOutlookName('singleView'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('pop'))); await tester.pumpAndSettle(); - expect(find.text(S.settingOrderOutlookTypes('singleView')), findsOneWidget); + expect(find.text(S.settingOrderOutlookName('singleView')), findsOneWidget); verify(cache.set(any, 1)); }); testWidgets('select checkout_warning', (tester) async { await tester.pumpWidget(buildApp()); - expect(find.text(S.settingCheckoutWarningTypes('showAll')), findsOneWidget); + expect(find.text(S.settingCheckoutWarningName('showAll')), findsOneWidget); await tester.tap(find.byKey(const Key('feature.checkout_warning'))); await tester.pumpAndSettle(); - await tester.tap(find.text(S.settingCheckoutWarningTypes('onlyNotEnough'))); + await tester.tap(find.text(S.settingCheckoutWarningName('onlyNotEnough'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('pop'))); await tester.pumpAndSettle(); - expect(find.text(S.settingCheckoutWarningTypes('onlyNotEnough')), findsOneWidget); + expect(find.text(S.settingCheckoutWarningName('onlyNotEnough')), findsOneWidget); verify(cache.set(any, 1)); }); @@ -129,6 +146,8 @@ void main() { await tester.pumpWidget(buildApp()); final finder = find.byKey(const Key('feature.order_product_count')); + await tester.scrollUntilVisible(finder, 200); + await tester.drag(finder, const Offset(-500, 0)); await tester.pumpAndSettle(); @@ -141,28 +160,31 @@ void main() { }); testWidgets('switch awake_ordering', (tester) async { - tester.view.physicalSize = const Size(1000, 3000); - addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget(buildApp()); - await tester.tap(find.byKey(const Key('feature.awake_ordering'))); + final finder = find.byKey(const Key('feature.order_awakening')); + await tester.scrollUntilVisible(find.byKey(const Key('feature.collect_events')), 200); + + await tester.tap(finder); await tester.pumpAndSettle(); verify(cache.set(any, false)); }); testWidgets('switch collect_events', (tester) async { - tester.view.physicalSize = const Size(1000, 3000); - addTearDown(tester.view.resetPhysicalSize); await tester.pumpWidget(buildApp()); - await tester.tap(find.byKey(const Key('feature.collect_events'))); + final finder = find.byKey(const Key('feature.collect_events')); + await tester.scrollUntilVisible(finder, 200); + + await tester.tap(finder); await tester.pumpAndSettle(); verify(cache.set(any, false)); }); setUp(() { + reset(cache); when(cache.get(any)).thenReturn(null); when(cache.set(any, any)).thenAnswer((_) => Future.value(true)); when(auth.authStateChanges()).thenAnswer((_) => Stream.value(null)); diff --git a/test/ui/home/home_page_test.dart b/test/ui/home/home_page_test.dart index a7cf219e..82039445 100644 --- a/test/ui/home/home_page_test.dart +++ b/test/ui/home/home_page_test.dart @@ -44,12 +44,11 @@ void main() { escapeTable: anyNamed('escapeTable'), limit: anyNamed('limit'), )).thenAnswer((_) => Future.value([])); - final settings = SettingsProvider(SettingsProvider.allSettings); final stock = Stock()..replaceItems({'i1': Ingredient(id: 'i1')}); await tester.pumpWidget(MultiProvider( providers: [ - ChangeNotifierProvider.value(value: settings), + ChangeNotifierProvider.value(value: SettingsProvider.instance), ChangeNotifierProvider.value(value: Seller.instance), ChangeNotifierProvider.value(value: Menu()), ChangeNotifierProvider.value(value: stock), @@ -98,6 +97,7 @@ void main() { await navAndPop('setting_header.order_attrs', 'order_attributes.reorder'); // rest + await navAndPop('setting.debug', 'debug.list'); await navAndPop('setting.menu', 'menu.search'); await navAndPop('setting.exporter', 'transit.google_sheet'); await navAndPop('setting.quantity', 'quantity.add'); @@ -115,7 +115,7 @@ void main() { setUp(() { // setup currency when(cache.get('currency')).thenReturn(null); - CurrencySetting().initialize(); + CurrencySetting.instance.initialize(); // setup seller when(database.query( diff --git a/test/ui/menu/menu_page_test.dart b/test/ui/menu/menu_page_test.dart index b99696ff..bc68c1dc 100644 --- a/test/ui/menu/menu_page_test.dart +++ b/test/ui/menu/menu_page_test.dart @@ -13,9 +13,11 @@ import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/models/stock/ingredient.dart'; import 'package:possystem/models/stock/quantity.dart'; import 'package:possystem/routes.dart'; +import 'package:possystem/translator.dart'; import 'package:possystem/ui/menu/menu_page.dart'; import 'package:provider/provider.dart'; +import '../../mocks/mock_cache.dart'; import '../../mocks/mock_storage.dart'; import '../../test_helpers/file_mocker.dart'; import '../../test_helpers/translator.dart'; @@ -269,7 +271,7 @@ void main() { await tester.enterText(find.byType(TextField).last, 'empty'); await tester.pumpAndSettle(); - expect(find.text('搜尋不到相關資訊,打錯字了嗎?'), findsOneWidget); + expect(find.text(S.menuSearchNotFound), findsOneWidget); // enter match products (including ingredient) await tester.enterText(find.byType(TextField).last, '2'); @@ -334,8 +336,14 @@ void main() { expect(find.byKey(const Key('catalog.c-1')), findsOneWidget); }); + setUp(() async { + await cache.reset(); + when(cache.get(any)).thenReturn(true); + }); + setUpAll(() { initializeStorage(); + initializeCache(); initializeTranslator(); initializeFileSystem(); }); diff --git a/test/ui/menu/product_page_test.dart b/test/ui/menu/product_page_test.dart index bd965eea..0234c0a9 100644 --- a/test/ui/menu/product_page_test.dart +++ b/test/ui/menu/product_page_test.dart @@ -147,7 +147,7 @@ void main() { await tester.pumpAndSettle(); // error message - expect(find.text(S.menuIngredientSearchEmptyError), findsOneWidget); + expect(find.text(S.menuIngredientSearchErrorEmpty), findsOneWidget); // add new ingredient await tester.tap(find.byKey(const Key('product_ingredient.search'))); @@ -246,7 +246,7 @@ void main() { await tester.pumpAndSettle(); // error message - expect(find.text(S.menuIngredientRepeatError), findsOneWidget); + expect(find.text(S.menuIngredientSearchErrorRepeat), findsOneWidget); // search for ingredient3 await tester.tap(find.byKey(const Key('product_ingredient.search'))); @@ -345,7 +345,7 @@ void main() { await tester.pumpAndSettle(); // error message - expect(find.text(S.menuQuantitySearchEmptyError), findsOneWidget); + expect(find.text(S.menuQuantitySearchErrorEmpty), findsOneWidget); // add new quantity await tester.tap(find.byKey(const Key('product_quantity.search'))); @@ -426,7 +426,7 @@ void main() { await tester.pumpAndSettle(); // error message - expect(find.text(S.menuQuantityRepeatError), findsOneWidget); + expect(find.text(S.menuQuantitySearchErrorRepeat), findsOneWidget); // search for quantity3 await tester.tap(find.byKey(const Key('product_quantity.search'))); diff --git a/test/ui/order/order_actions_test.dart b/test/ui/order/order_actions_test.dart index 1d23ed26..e6f1ba22 100644 --- a/test/ui/order/order_actions_test.dart +++ b/test/ui/order/order_actions_test.dart @@ -2,29 +2,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mockito/mockito.dart'; -import 'package:possystem/models/objects/order_object.dart'; -import 'package:possystem/models/order/order_attribute.dart'; -import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/models/menu/product.dart'; import 'package:possystem/models/menu/product_ingredient.dart'; import 'package:possystem/models/menu/product_quantity.dart'; +import 'package:possystem/models/objects/order_object.dart'; import 'package:possystem/models/order/cart_product.dart'; +import 'package:possystem/models/order/order_attribute.dart'; +import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:possystem/models/repository/cashier.dart'; -import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/stashed_orders.dart'; import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/models/stock/ingredient.dart'; import 'package:possystem/models/stock/quantity.dart'; import 'package:possystem/routes.dart'; -import 'package:possystem/settings/currency_setting.dart'; -import 'package:possystem/settings/order_awakening_setting.dart'; -import 'package:possystem/settings/order_outlook_setting.dart'; -import 'package:possystem/settings/order_product_axis_count_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/ui/order/order_page.dart'; import 'package:provider/provider.dart'; @@ -36,13 +31,6 @@ import '../../test_helpers/translator.dart'; void main() { group('Order Actions', () { void prepareData() { - SettingsProvider([ - CurrencySetting(), - OrderOutlookSetting(), - OrderAwakeningSetting(), - OrderProductAxisCountSetting(), - ]); - Stock().replaceItems({ 'i-1': Ingredient(id: 'i-1', name: 'i-1'), 'i-2': Ingredient(id: 'i-2', name: 'i-2'), @@ -210,7 +198,7 @@ void main() { await tester.tap(find.byKey(const Key('order.more'))); await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('order.action.changer'))); + await tester.tap(find.byKey(const Key('order.action.exchange'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('changer.favorite.0'))); diff --git a/test/ui/order/order_details_page_test.dart b/test/ui/order/order_checkout_page_test.dart similarity index 98% rename from test/ui/order/order_details_page_test.dart rename to test/ui/order/order_checkout_page_test.dart index 71e8866d..0dc5497f 100644 --- a/test/ui/order/order_details_page_test.dart +++ b/test/ui/order/order_checkout_page_test.dart @@ -2,19 +2,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mockito/mockito.dart'; -import 'package:possystem/models/objects/order_object.dart'; -import 'package:possystem/models/order/order_attribute.dart'; -import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/models/menu/product.dart'; import 'package:possystem/models/menu/product_ingredient.dart'; import 'package:possystem/models/menu/product_quantity.dart'; import 'package:possystem/models/objects/order_attribute_object.dart'; +import 'package:possystem/models/objects/order_object.dart'; import 'package:possystem/models/order/cart_product.dart'; +import 'package:possystem/models/order/order_attribute.dart'; +import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:possystem/models/repository/cashier.dart'; -import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/stashed_orders.dart'; import 'package:possystem/models/repository/stock.dart'; @@ -23,7 +23,6 @@ import 'package:possystem/models/stock/quantity.dart'; import 'package:possystem/routes.dart'; import 'package:possystem/services/storage.dart'; import 'package:possystem/settings/currency_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/order/order_page.dart'; import 'package:provider/provider.dart'; @@ -37,8 +36,6 @@ import '../../test_helpers/translator.dart'; void main() { group('Order Details', () { void prepareData() { - SettingsProvider(SettingsProvider.allSettings); - Stock().replaceItems({ 'i-1': Ingredient(id: 'i-1', name: 'i-1', currentAmount: 100), 'i-2': Ingredient(id: 'i-2', name: 'i-2', currentAmount: 100), @@ -225,7 +222,7 @@ void main() { await tester.tap(find.byKey(const Key('cashier.snapshot.30'))); await tester.pumpAndSettle(); - expect(find.text(S.orderCashierSnapshotChangeField(2)), findsOneWidget); + expect(find.text(S.orderCheckoutCashierSnapshotLabelChange('2')), findsOneWidget); await tester.drag( find.byKey(const Key('order.details.ds')), const Offset(0, -408), @@ -250,7 +247,7 @@ void main() { await tester.tap(fCKey('submit')); await tester.pumpAndSettle(); - expect(find.text(S.orderCashierCalculatorChangeNotEnough), findsWidgets); + expect(find.text(S.orderCheckoutSnackbarPaidFailed), findsWidgets); scaffoldMessenger.currentState?.removeCurrentSnackBar(); await tester.pumpAndSettle(); @@ -272,7 +269,7 @@ void main() { await tester.pumpAndSettle(); expect(find.byKey(const Key('cashier.snapshot.90')), findsOneWidget); - expect(find.text(S.orderCashierSnapshotChangeField(62)), findsOneWidget); + expect(find.text(S.orderCheckoutCashierSnapshotLabelChange('62')), findsOneWidget); await Cashier.instance.setCurrentByUnit(1, 5); diff --git a/test/ui/order/order_page_test.dart b/test/ui/order/order_page_test.dart index 2756511b..00af62e4 100644 --- a/test/ui/order/order_page_test.dart +++ b/test/ui/order/order_page_test.dart @@ -6,14 +6,14 @@ import 'package:possystem/components/meta_block.dart'; import 'package:possystem/components/style/hint_text.dart'; import 'package:possystem/components/style/outlined_text.dart'; import 'package:possystem/models/menu/catalog.dart'; -import 'package:possystem/models/menu/product_ingredient.dart'; import 'package:possystem/models/menu/product.dart'; +import 'package:possystem/models/menu/product_ingredient.dart'; import 'package:possystem/models/menu/product_quantity.dart'; import 'package:possystem/models/order/cart_product.dart'; import 'package:possystem/models/repository/cart.dart'; import 'package:possystem/models/repository/cashier.dart'; -import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/seller.dart'; import 'package:possystem/models/repository/stock.dart'; @@ -21,7 +21,10 @@ import 'package:possystem/models/stock/ingredient.dart'; import 'package:possystem/models/stock/quantity.dart'; import 'package:possystem/routes.dart'; import 'package:possystem/settings/checkout_warning.dart'; -import 'package:possystem/settings/settings_provider.dart'; +import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/settings/order_awakening_setting.dart'; +import 'package:possystem/settings/order_outlook_setting.dart'; +import 'package:possystem/settings/order_product_axis_count_setting.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/order/order_page.dart'; import 'package:provider/provider.dart'; @@ -33,8 +36,6 @@ import '../../test_helpers/translator.dart'; void main() { group('Order Page', () { void prepareData() { - SettingsProvider(SettingsProvider.allSettings); - Stock().replaceItems({ 'i-1': Ingredient(id: 'i-1', name: 'i-1'), 'i-2': Ingredient(id: 'i-2', name: 'i-2'), @@ -208,14 +209,15 @@ void main() { expect(tester.widget(find.byKey(Key('$key.count'))).data, equals(count.toString())); } if (price != null) { - expect( - tester.widget(find.byKey(Key('$key.price'))).data, equals(S.orderCartItemPrice(price.toInt()))); + expect(tester.widget(find.byKey(Key('$key.price'))).data, + equals(S.orderCartProductPrice(price.toCurrency()))); } } verifyMetadata(int count, num price) { final w = tester.widget(find.byKey(const Key('cart.metadata'))); - final t = '${S.orderMetaTotalCount(count)}${MetaBlock.string}${S.orderMetaTotalPrice(price)}'; + final t = + '${S.orderCartMetaTotalCount(count)}${MetaBlock.string}${S.orderCartMetaTotalPrice(price.toCurrency())}'; expect((w.child as RichText).text.toPlainText(), equals(t)); } @@ -254,7 +256,7 @@ void main() { // select quantity await tester.tap(find.byKey(const Key('order.quantity.pq-1'))); await tester.pumpAndSettle(); - verifyProductList(0, subtitle: S.orderProductIngredientName('i-1', 'q-1'), price: 27); + verifyProductList(0, subtitle: S.orderCartProductIngredient('i-1', 'q-1'), price: 27); await tester.tap(find.byKey(const Key('order.quantity.default'))); await tester.pumpAndSettle(); @@ -262,7 +264,7 @@ void main() { await tester.tap(find.byKey(const Key('order.quantity.pq-2'))); await tester.pumpAndSettle(); - verifyProductList(0, subtitle: S.orderProductIngredientName('i-1', 'q-2'), price: 7); + verifyProductList(0, subtitle: S.orderCartProductIngredient('i-1', 'q-2'), price: 7); // add count await tester.tap(find.byKey(const Key('cart.product.0.add'))); @@ -314,43 +316,54 @@ void main() { ['p-1', '2'], ['p-2', '2'], ], 36); + + // open by tapping snapshot product + await tester.tap(find.byKey(const Key('cart_snapshot.0'))); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('order.ingredient.pi-2')), findsOneWidget); }); }); group('All in one page', () { testWidgets('scroll to bottom', (tester) async { - when(cache.get('feat.orderAwakening')).thenReturn(false); - when(cache.get('feat.orderOutlook')).thenReturn(1); - // text only - when(cache.get('feat.orderProductAxisCount')).thenReturn(0); + OrderAwakeningSetting.instance.value = false; + OrderOutlookSetting.instance.value = OrderOutlookTypes.singleView; + OrderProductAxisCountSetting.instance.value = 0; - prepareData(); + try { + prepareData(); - await tester.pumpWidget(buildApp()); + await tester.pumpWidget(buildApp()); - await tester.tap(find.byKey(const Key('order.product.p-1'))); - await tester.tap(find.byKey(const Key('order.catalog.c-2'))); - await tester.pumpAndSettle(); - await tester.tap(find.byKey(const Key('order.product.p-2'))); - await tester.tap(find.byKey(const Key('order.product.p-2'))); - await tester.tap(find.byKey(const Key('order.product.p-2'))); - await tester.tap(find.byKey(const Key('order.product.p-2'))); - await tester.tap(find.byKey(const Key('order.product.p-2'))); - await tester.tap(find.byKey(const Key('order.product.p-2'))); - await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('order.product.p-1'))); + await tester.tap(find.byKey(const Key('order.catalog.c-2'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('order.product.p-2'))); + await tester.tap(find.byKey(const Key('order.product.p-2'))); + await tester.tap(find.byKey(const Key('order.product.p-2'))); + await tester.tap(find.byKey(const Key('order.product.p-2'))); + await tester.tap(find.byKey(const Key('order.product.p-2'))); + await tester.tap(find.byKey(const Key('order.product.p-2'))); + await tester.pumpAndSettle(); - expect(find.byKey(const Key('cart_snapshot.price')), findsNothing); - final scrollController = tester.widget(find.byKey(const Key('cart.product_list'))).controller!; - // scroll to bottom - expect(scrollController.position.maxScrollExtent, isNonZero); - expect(find.byKey(const Key('order.orientation.landscape')), findsOneWidget); + expect(find.byKey(const Key('cart_snapshot.price')), findsNothing); + final scrollController = tester.widget(find.byKey(const Key('cart.product_list'))).controller!; + // scroll to bottom + expect(scrollController.position.maxScrollExtent, isNonZero); + expect(find.byKey(const Key('order.orientation.landscape')), findsOneWidget); - // setup portrait env - tester.view.physicalSize = const Size(1000, 2000); - addTearDown(tester.view.resetPhysicalSize); + // setup portrait env + tester.view.physicalSize = const Size(1000, 2000); + addTearDown(tester.view.resetPhysicalSize); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('order.orientation.portrait')), findsOneWidget); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('order.orientation.portrait')), findsOneWidget); + } finally { + OrderAwakeningSetting.instance.value = OrderAwakeningSetting.defaultValue; + OrderOutlookSetting.instance.value = OrderOutlookSetting.defaultValue; + OrderProductAxisCountSetting.instance.value = OrderProductAxisCountSetting.defaultValue; + } }); }); @@ -406,13 +419,15 @@ void main() { expect(tester.widget(find.byKey(Key('$key.count'))).data, equals(count.toString())); } if (price != null) { - expect(tester.widget(find.byKey(Key('$key.price'))).data, equals(S.orderCartItemPrice(price.toInt()))); + expect(tester.widget(find.byKey(Key('$key.price'))).data, + equals(S.orderCartProductPrice(price.toCurrency()))); } } verifyMetadata(int count, num price) { final w = tester.widget(find.byKey(const Key('cart.metadata'))); - final t = '${S.orderMetaTotalCount(count)}${MetaBlock.string}${S.orderMetaTotalPrice(price)}'; + final t = + '${S.orderCartMetaTotalCount(count)}${MetaBlock.string}${S.orderCartMetaTotalPrice(price.toCurrency())}'; expect((w.child as RichText).text.toPlainText(), equals(t)); } @@ -491,7 +506,7 @@ void main() { } // hide all - SettingsProvider.of().value = CheckoutWarningTypes.hideAll; + CheckoutWarningSetting.instance.value = CheckoutWarningTypes.hideAll; await tapWithCheck(CheckoutStatus.ok, S.actSuccess); await tapWithCheck(CheckoutStatus.restore, S.actSuccess); await tapWithCheck(CheckoutStatus.stash, S.actSuccess); @@ -506,16 +521,17 @@ void main() { await tapWithCheck(CheckoutStatus.nothingHappened); // only not enough - SettingsProvider.of().value = CheckoutWarningTypes.onlyNotEnough; - await tapWithCheck(CheckoutStatus.cashierNotEnough, S.orderCashierPaidNotEnough); + CheckoutWarningSetting.instance.value = CheckoutWarningTypes.onlyNotEnough; + await tapWithCheck(CheckoutStatus.cashierNotEnough, S.orderSnackbarCashierNotEnough); await tapWithCheck(CheckoutStatus.cashierUsingSmall, S.actSuccess); // show all - SettingsProvider.of().value = CheckoutWarningTypes.showAll; - await tapWithCheck(CheckoutStatus.cashierUsingSmall, S.orderCashierPaidUsingSmallMoney); + CheckoutWarningSetting.instance.value = CheckoutWarningTypes.showAll; + await tapWithCheck(CheckoutStatus.cashierUsingSmall, S.orderSnackbarCashierUsingSmallMoney); }); setUp(() { + cache.reset(); // disable any features when(cache.get(any)).thenReturn(null); // disable tutorial diff --git a/test/ui/order/stashed_order_test.dart b/test/ui/order/stashed_order_test.dart index 196f510e..fb0ff613 100644 --- a/test/ui/order/stashed_order_test.dart +++ b/test/ui/order/stashed_order_test.dart @@ -13,10 +13,8 @@ import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/stashed_orders.dart'; import 'package:possystem/models/repository/stock.dart'; -import 'package:possystem/settings/currency_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; -import 'package:possystem/ui/order/cashier/stashed_order_list_view.dart'; +import 'package:possystem/ui/order/checkout/stashed_order_list_view.dart'; import '../../mocks/mock_cache.dart'; import '../../mocks/mock_database.dart'; @@ -229,7 +227,7 @@ void main() { await tester.tap(find.byKey(const Key('cashier.calculator.submit'))); await tester.pumpAndSettle(); - expect(find.text(S.orderCashierPaidFailed), findsOneWidget); + expect(find.text(S.orderCheckoutSnackbarPaidFailed), findsOneWidget); }); testWidgets('checkout will delete order after success', (tester) async { @@ -254,7 +252,6 @@ void main() { setUp(() { when(cache.get(any)).thenReturn(null); - SettingsProvider(SettingsProvider.allSettings); }); setUpAll(() { @@ -262,7 +259,6 @@ void main() { initializeDatabase(); initializeTranslator(); OrderAttributes(); - CurrencySetting().isInt = true; }); }); } diff --git a/test/ui/order_attr/order_attribute_page_test.dart b/test/ui/order_attr/order_attribute_page_test.dart index c55cd26b..cea970c5 100644 --- a/test/ui/order_attr/order_attribute_page_test.dart +++ b/test/ui/order_attr/order_attribute_page_test.dart @@ -3,14 +3,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mockito/mockito.dart'; import 'package:possystem/constants/icons.dart'; +import 'package:possystem/models/objects/order_attribute_object.dart'; import 'package:possystem/models/order/order_attribute.dart'; import 'package:possystem/models/order/order_attribute_option.dart'; -import 'package:possystem/models/objects/order_attribute_object.dart'; import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/routes.dart'; import 'package:possystem/services/storage.dart'; -import 'package:possystem/settings/currency_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/order_attr/order_attribute_page.dart'; import 'package:provider/provider.dart'; @@ -90,12 +88,10 @@ void main() { }); when(cache.get(any)).thenReturn(null); - final currency = CurrencySetting(); await tester.pumpWidget(MultiProvider( providers: [ ChangeNotifierProvider.value(value: attrs), - ChangeNotifierProvider.value(value: SettingsProvider([currency])), ], child: MaterialApp.router( routerConfig: GoRouter(routes: [ @@ -334,7 +330,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('order_attributes.1.more'))); await tester.pumpAndSettle(); - await tester.tap(find.text(S.orderAttributeOptionReorder)); + await tester.tap(find.text(S.orderAttributeOptionTitleReorder)); await tester.pumpAndSettle(); await tester.drag(find.byIcon(Icons.reorder_sharp).first, const Offset(0, 200)); diff --git a/test/ui/stock/replenishment_page_test.dart b/test/ui/stock/replenishment_page_test.dart index 4f2aab67..e79f58dd 100644 --- a/test/ui/stock/replenishment_page_test.dart +++ b/test/ui/stock/replenishment_page_test.dart @@ -16,6 +16,25 @@ import '../../test_helpers/translator.dart'; void main() { group('Replenishment Page', () { + Widget buildApp(Stock stock, Replenisher replenisher) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: stock), + ChangeNotifierProvider.value(value: replenisher), + ], + builder: (_, __) => MaterialApp.router( + routerConfig: GoRouter(routes: [ + GoRoute( + path: '/', + routes: Routes.routes, + builder: (_, __) => const ReplenishmentPage(), + ) + ]), + ), + ); + } + + // TODO: find which causing overflows testWidgets('Edit replenishment', (tester) async { final replenishment = Replenishment(id: 'r-1', name: 'r-1', data: { 'i-1': 1, @@ -31,21 +50,7 @@ void main() { }); when(storage.set(any, any)).thenAnswer((_) => Future.value()); - await tester.pumpWidget(MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: stock), - ChangeNotifierProvider.value(value: replenisher), - ], - builder: (_, __) => MaterialApp.router( - routerConfig: GoRouter(routes: [ - GoRoute( - path: '/', - routes: Routes.routes, - builder: (_, __) => const ReplenishmentPage(), - ) - ]), - ), - )); + await tester.pumpWidget(buildApp(stock, replenisher)); await tester.longPress(find.byKey(const Key('replenisher.r-1'))); await tester.pumpAndSettle(); @@ -79,21 +84,7 @@ void main() { final replenisher = Replenisher()..replaceItems({}); when(storage.set(any, any)).thenAnswer((_) => Future.value()); - await tester.pumpWidget(MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: stock), - ChangeNotifierProvider.value(value: replenisher), - ], - builder: (_, __) => MaterialApp.router( - routerConfig: GoRouter(routes: [ - GoRoute( - path: '/', - routes: Routes.routes, - builder: (_, __) => const ReplenishmentPage(), - ) - ]), - ), - )); + await tester.pumpWidget(buildApp(stock, replenisher)); await tester.tap(find.byKey(const Key('replenisher.add'))); await tester.pumpAndSettle(); @@ -120,18 +111,7 @@ void main() { final replenisher = Replenisher()..replaceItems({'r-1': replenishment}); when(storage.set(any, any)).thenAnswer((_) => Future.value()); - await tester.pumpWidget(ChangeNotifierProvider.value( - value: replenisher, - builder: (_, __) => MaterialApp.router( - routerConfig: GoRouter(routes: [ - GoRoute( - path: '/', - routes: Routes.routes, - builder: (_, __) => const ReplenishmentPage(), - ) - ]), - ), - )); + await tester.pumpWidget(buildApp(Stock(), replenisher)); await tester.longPress(find.byKey(const Key('replenisher.r-1'))); await tester.pumpAndSettle(); diff --git a/test/ui/stock/stock_view_test.dart b/test/ui/stock/stock_view_test.dart index ac38973e..bddb4d8f 100644 --- a/test/ui/stock/stock_view_test.dart +++ b/test/ui/stock/stock_view_test.dart @@ -13,6 +13,7 @@ import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/models/stock/ingredient.dart'; import 'package:possystem/models/stock/replenishment.dart'; import 'package:possystem/routes.dart'; +import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/stock/stock_view.dart'; import 'package:provider/provider.dart'; @@ -86,7 +87,7 @@ void main() { void verifyLastUpdated(DateTime dt) { final s = S.stockUpdatedAt(dt); - expect(find.text('$s${MetaBlock.string}總共 2 項'), findsOneWidget); + expect(find.text('$s${MetaBlock.string}${S.totalCount(2)}'), findsOneWidget); } verifyLastUpdated(DateTime(2020, 10, 11)); @@ -147,7 +148,7 @@ void main() { ], child: buildApp())); // correctly transform string - expect(find.text('0/5.43萬'), findsOneWidget); + expect(find.text('0/${54321.toCurrency()}'), findsOneWidget); expect(find.text('0/901'), findsOneWidget); final ingredient = Stock.instance.items.first; diff --git a/test/ui/transit/google_sheet/export_basic_test.dart b/test/ui/transit/google_sheet/export_basic_test.dart index 23d48ed6..efcd5289 100644 --- a/test/ui/transit/google_sheet/export_basic_test.dart +++ b/test/ui/transit/google_sheet/export_basic_test.dart @@ -6,14 +6,14 @@ import 'package:googleapis/sheets/v4.dart' as gs; import 'package:mockito/mockito.dart'; import 'package:possystem/helpers/exporter/google_sheet_exporter.dart'; import 'package:possystem/helpers/launcher.dart'; -import 'package:possystem/models/order/order_attribute.dart'; -import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/menu/catalog.dart'; import 'package:possystem/models/menu/product.dart'; import 'package:possystem/models/menu/product_ingredient.dart'; import 'package:possystem/models/menu/product_quantity.dart'; -import 'package:possystem/models/repository/order_attributes.dart'; +import 'package:possystem/models/order/order_attribute.dart'; +import 'package:possystem/models/order/order_attribute_option.dart'; import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/replenisher.dart'; import 'package:possystem/models/repository/stock.dart'; @@ -38,7 +38,7 @@ void main() { Widget buildApp([CustomMockSheetsApi? sheetsApi]) { return MaterialApp( home: TransitStation( - type: TransitType.basic, + catalog: TransitCatalog.model, method: TransitMethod.googleSheet, exporter: GoogleSheetExporter( sheetsApi: sheetsApi, @@ -123,7 +123,9 @@ void main() { bool selected = true, }) async { await tester.pumpAndSettle(); - await tester.tap(find.text(selected ? '指定匯出' : '建立匯出')); + await tester.tap(find.text( + selected ? S.transitGSSpreadsheetExportExistLabel : S.transitGSSpreadsheetExportEmptyLabel, + )); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('confirm_dialog.confirm'))); await tester.pumpAndSettle(); @@ -135,7 +137,7 @@ void main() { await tester.pumpWidget( MaterialApp( home: TransitStation( - type: TransitType.basic, + catalog: TransitCatalog.model, method: TransitMethod.googleSheet, notifier: notifier, exporter: GoogleSheetExporter(), @@ -152,7 +154,7 @@ void main() { when(cache.get(eCacheKey + '.menu')).thenReturn('title'); await tester.pumpWidget(buildApp()); await tapBtn(tester); - expect(find.text(S.transitGSErrors('sheetRepeat')), findsOneWidget); + expect(find.text(S.transitGSErrorSheetRepeat), findsOneWidget); }); testWidgets('spreadsheet create failed', (tester) async { @@ -168,7 +170,7 @@ void main() { await tester.pumpWidget(buildApp(sheetsApi)); await tapBtn(tester, selected: false); - expect(find.text(S.transitGSErrors('spreadsheet')), findsOneWidget); + expect(find.text(S.transitGSErrorCreateSpreadsheet), findsOneWidget); }); testWidgets('spreadsheet create success', (tester) async { @@ -185,7 +187,7 @@ void main() { await tester.pumpWidget(buildApp(sheetsApi)); await tapBtn(tester, selected: false); - final title = S.transitBasicTitle; + final title = S.transitGSSpreadsheetModelDefaultName; verify(cache.set(eCacheKey, 'abc:true:' + title)); verify(cache.set(iCacheKey, 'abc:true:' + title)); }); @@ -207,7 +209,7 @@ void main() { await tester.pumpWidget(buildApp(sheetsApi)); await tapBtn(tester); - expect(find.text(S.transitGSErrors('sheet')), findsOneWidget); + expect(find.text(S.transitGSErrorCreateSheet), findsOneWidget); }); testWidgets('export without new sheets', (tester) async { @@ -254,7 +256,7 @@ void main() { )); // which also verify button exist! - await tester.tap(find.text('開啟表單')); + await tester.tap(find.text(S.transitGSSpreadsheetSnackbarAction)); await tester.pumpAndSettle(); expect(Launcher.lastUrl, equals('https://docs.google.com/spreadsheets/d/id/edit')); diff --git a/test/ui/transit/google_sheet/export_order_test.dart b/test/ui/transit/google_sheet/export_order_test.dart index 9dd82aaf..78581257 100644 --- a/test/ui/transit/google_sheet/export_order_test.dart +++ b/test/ui/transit/google_sheet/export_order_test.dart @@ -6,14 +6,11 @@ import 'package:intl/intl.dart'; import 'package:mockito/mockito.dart'; import 'package:possystem/helpers/exporter/google_sheet_exporter.dart'; import 'package:possystem/helpers/util.dart'; -import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/settings/language_setting.dart'; -import 'package:possystem/settings/settings_provider.dart'; import 'package:possystem/translator.dart'; import 'package:possystem/ui/transit/google_sheet/order_formatter.dart'; import 'package:possystem/ui/transit/google_sheet/order_setting_page.dart'; import 'package:possystem/ui/transit/transit_station.dart'; -import 'package:provider/provider.dart'; import '../../../mocks/mock_auth.dart'; import '../../../mocks/mock_cache.dart'; @@ -31,7 +28,7 @@ void main() { Widget buildApp([CustomMockSheetsApi? sheetsApi]) { return MaterialApp( home: TransitStation( - type: TransitType.order, + catalog: TransitCatalog.order, method: TransitMethod.googleSheet, exporter: GoogleSheetExporter( sheetsApi: sheetsApi, @@ -60,33 +57,25 @@ void main() { OrderSetter.setMetrics([], countingAll: true); OrderSetter.setOrders([]); - final lang = LanguageSetting(); - final settings = SettingsProvider([lang]); - lang.value = const Locale('en', 'US'); final init = DateTimeRange( start: DateTime(2023, DateTime.june, 10), end: DateTime(2023, DateTime.june, 11), ); - await tester.pumpWidget(MultiProvider( - providers: [ - ChangeNotifierProvider.value(value: settings), + await tester.pumpWidget(MaterialApp( + locale: LanguageSetting.defaultValue.locale, + localizationsDelegates: const >[ + DefaultWidgetsLocalizations.delegate, + DefaultMaterialLocalizations.delegate, + DefaultCupertinoLocalizations.delegate, ], - child: MaterialApp( - locale: lang.value, - localizationsDelegates: const >[ - DefaultWidgetsLocalizations.delegate, - DefaultMaterialLocalizations.delegate, - DefaultCupertinoLocalizations.delegate, - ], - supportedLocales: [lang.value], - home: TransitStation( - type: TransitType.order, - method: TransitMethod.googleSheet, - range: init, - exporter: GoogleSheetExporter( - scopes: gsExporterScopes, - ), + supportedLocales: [LanguageSetting.defaultValue.locale], + home: TransitStation( + catalog: TransitCatalog.order, + method: TransitMethod.googleSheet, + range: init, + exporter: GoogleSheetExporter( + scopes: gsExporterScopes, ), ), )); @@ -107,7 +96,7 @@ void main() { ); expect( - find.text('${expected.format(DateFormat.MMMd('zh'))} 的訂單'), + find.text(S.transitOrderMetaRange(expected.format('en'))), findsOneWidget, ); }); @@ -131,7 +120,7 @@ void main() { return gs.Sheet( properties: gs.SheetProperties( sheetId: e.index, - title: '$today ${S.transitType(e.name)}', + title: '$today ${S.transitModelName(e.name)}', ), ); }).toList(), @@ -144,12 +133,14 @@ void main() { await tester.pumpWidget(buildApp(sheetsApi)); await tester.pumpAndSettle(); - await tester.tap(find.text('建立匯出')); + await tester.tap(find.text(S.transitGSSpreadsheetExportEmptyLabel)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('confirm_dialog.confirm'))); await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - final title = S.transitOrderTitle; + final title = S.transitGSSpreadsheetOrderDefaultName; verify(cache.set(cacheKey, 'abc:true:$title')); final expected = [ @@ -158,16 +149,16 @@ void main() { ...OrderFormatter.formatOrder(order), ], [ - OrderFormatter.orderSetAttrHeaders, - ...OrderFormatter.formatOrderSetAttr(order), + OrderFormatter.orderDetailsAttrHeaders, + ...OrderFormatter.formatOrderDetailsAttr(order), ], [ - OrderFormatter.orderProductHeaders, - ...OrderFormatter.formatOrderProduct(order), + OrderFormatter.orderDetailsProductHeaders, + ...OrderFormatter.formatOrderDetailsProduct(order), ], [ - OrderFormatter.orderIngredientHeaders, - ...OrderFormatter.formatOrderIngredient(order), + OrderFormatter.orderDetailsIngredientHeaders, + ...OrderFormatter.formatOrderDetailsIngredient(order), ], ]; verify(sheetsApi.spreadsheets.values.batchUpdate( @@ -203,9 +194,9 @@ void main() { // exist spreadsheet when(cache.get(cacheKey)).thenReturn('id:true:name'); when(cache.get('$cacheKey.order')).thenReturn('o title'); - when(cache.get('$cacheKey.orderSetAttr')).thenReturn('os title'); - when(cache.get('$cacheKey.orderProduct')).thenReturn('op title'); - when(cache.get('$cacheKey.orderIngredient')).thenReturn('oi title'); + when(cache.get('$cacheKey.orderDetailsAttr')).thenReturn('os title'); + when(cache.get('$cacheKey.orderDetailsProduct')).thenReturn('op title'); + when(cache.get('$cacheKey.orderDetailsIngredient')).thenReturn('oi title'); when(cache.get('$cacheKey.order.required')).thenReturn(false); when(sheetsApi.spreadsheets.get( any, @@ -247,15 +238,15 @@ void main() { verify(cache.set('$cacheKey.order.required', false)); // export - await tester.tap(find.text('指定匯出')); + await tester.tap(find.text(S.transitGSSpreadsheetExportExistLabel)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('confirm_dialog.confirm'))); await tester.pumpAndSettle(); final expected = { - 'os title': OrderFormatter.formatOrderSetAttr(order), - 'op title': OrderFormatter.formatOrderProduct(order), - 'oi title': OrderFormatter.formatOrderIngredient(order), + 'os title': OrderFormatter.formatOrderDetailsAttr(order), + 'op title': OrderFormatter.formatOrderDetailsProduct(order), + 'oi title': OrderFormatter.formatOrderDetailsIngredient(order), }; for (final e in expected.entries) { verify(sheetsApi.spreadsheets.values.append( @@ -283,7 +274,8 @@ void main() { }); }); - setUp(() { + setUp(() async { + await cache.reset(); when(cache.get(any)).thenReturn(null); when(auth.authStateChanges()).thenAnswer((_) => Stream.value(MockUser())); }); @@ -293,8 +285,6 @@ void main() { initializeTranslator(); initializeDatabase(); initializeAuth(); - // init dependencies - CurrencySetting().isInt = true; }); }); } diff --git a/test/ui/transit/google_sheet/import_basic_test.dart b/test/ui/transit/google_sheet/import_basic_test.dart index d3cfc803..a9cd26ed 100644 --- a/test/ui/transit/google_sheet/import_basic_test.dart +++ b/test/ui/transit/google_sheet/import_basic_test.dart @@ -5,8 +5,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:googleapis/sheets/v4.dart' as gs; import 'package:mockito/mockito.dart'; import 'package:possystem/helpers/exporter/google_sheet_exporter.dart'; -import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/menu.dart'; +import 'package:possystem/models/repository/order_attributes.dart'; import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/replenisher.dart'; import 'package:possystem/models/repository/stock.dart'; @@ -31,7 +31,7 @@ void main() { Widget buildApp([CustomMockSheetsApi? sheetsApi]) { return MaterialApp( home: TransitStation( - type: TransitType.basic, + catalog: TransitCatalog.model, method: TransitMethod.googleSheet, exporter: GoogleSheetExporter( sheetsApi: sheetsApi, @@ -42,13 +42,15 @@ void main() { } Future go2Importer(WidgetTester tester) async { - await tester.tap(find.widgetWithText(Tab, S.btnImport)); + await tester.tap(find.widgetWithText(Tab, S.transitImportBtn)); await tester.pumpAndSettle(); } group('#refresh -', () { Future tapBtn(WidgetTester tester, {bool selected = true}) async { - await tester.tap(find.text(selected ? '確認表單名稱' : '選擇試算表')); + await tester.tap(find.text( + selected ? S.transitGSSpreadsheetImportExistLabel : S.transitGSSpreadsheetImportEmptyLabel, + )); await tester.pump(); } @@ -152,7 +154,7 @@ void main() { await tester.pumpWidget(buildApp()); await tapBtn(tester); - expect(find.text(S.transitGSImportError('emptySpreadsheet')), findsOneWidget); + expect(find.text(S.transitGSErrorImportEmptySpreadsheet), findsOneWidget); }); testWidgets('sheet not selected', (tester) async { @@ -160,7 +162,7 @@ void main() { await tester.pumpWidget(buildApp()); await tapBtn(tester); - expect(find.text(S.transitGSImportError('emptySheet')), findsOneWidget); + expect(find.text(S.transitGSErrorImportEmptySheet), findsOneWidget); }); testWidgets('empty data', (tester) async { @@ -170,7 +172,7 @@ void main() { await tester.pumpWidget(buildApp(sheetsApi)); await tapBtn(tester); - expect(find.text('找不到表單「title」的資料'), findsOneWidget); + expect(find.text(S.transitGSErrorImportNotFoundSheets('title')), findsOneWidget); }); testWidgets('pop preview source', (tester) async { @@ -184,7 +186,7 @@ void main() { await tester.pumpWidget(MaterialApp( home: TransitStation( - type: TransitType.basic, + catalog: TransitCatalog.model, notifier: notifier, exporter: GoogleSheetExporter( sheetsApi: sheetsApi, @@ -196,7 +198,7 @@ void main() { await tapBtn(tester); expect(find.text(ing), findsOneWidget); - expect(notifier.value, equals('驗證身份中')); + expect(notifier.value, equals(S.transitGSProgressStatusVerifyUser)); await tester.tap(find.byKey(const Key('pop'))); await tester.pumpAndSettle(); @@ -220,7 +222,7 @@ void main() { )).thenAnswer((_) => Future.value(gs.Spreadsheet(sheets: [ gs.Sheet(properties: sheet), ]))); - await tester.tap(find.text('確認表單名稱')); + await tester.tap(find.text(S.transitGSSpreadsheetImportExistLabel)); await tester.pumpAndSettle(); final menu = find.byKey(const Key('gs_export.menu.sheet_selector')); await tester.tap(menu); @@ -242,13 +244,13 @@ void main() { verify(cache.set(iCacheKey + '.menu', 'new-sheet 2')); - await tester.tap(find.text(S.transitPreviewImportTitle)); + await tester.tap(find.text(S.transitImportPreviewBtn)); await tester.pumpAndSettle(); for (var e in ['p1', 'p2', 'p3', 'c1', 'c2']) { findText(e, 'staged'); } - expect(find.text('將忽略本行,相同的項目已於前面出現'), findsOneWidget); + expect(find.text(S.transitImportErrorDuplicate), findsOneWidget); await tester.tap(find.byType(ExpansionTile).first); await tester.pumpAndSettle(); @@ -284,7 +286,7 @@ void main() { await tester.pumpWidget(buildApp(sheetsApi)); await tapBtn(tester, index); - await tester.tap(find.text(S.transitPreviewImportTitle)); + await tester.tap(find.text(S.transitImportPreviewBtn)); await tester.pumpAndSettle(); if (names == null) { @@ -370,7 +372,7 @@ void main() { testWidgets('orderAttribute', (tester) async { await prepareImport(tester, 'orderAttr', 4, true, [ - ['c1', '折扣', '- co1,true\n- co2,,5'], + ['c1', S.orderAttributeModeName('changeDiscount'), '- co1,true\n- co2,,5'], ['c2'], ]); diff --git a/test/ui/transit/google_sheet/select_spreadsheet_test.dart b/test/ui/transit/google_sheet/select_spreadsheet_test.dart index fbe848fb..7111b532 100644 --- a/test/ui/transit/google_sheet/select_spreadsheet_test.dart +++ b/test/ui/transit/google_sheet/select_spreadsheet_test.dart @@ -12,8 +12,8 @@ import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/replenisher.dart'; import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/translator.dart'; -import 'package:possystem/ui/transit/transit_station.dart'; import 'package:possystem/ui/transit/google_sheet/sheet_namer.dart'; +import 'package:possystem/ui/transit/transit_station.dart'; import '../../../mocks/mock_auth.dart'; import '../../../mocks/mock_cache.dart'; @@ -31,7 +31,7 @@ void main() { Widget buildApp([CustomMockSheetsApi? sheetsApi]) { return MaterialApp( home: TransitStation( - type: TransitType.basic, + catalog: TransitCatalog.model, method: TransitMethod.googleSheet, exporter: GoogleSheetExporter( sheetsApi: sheetsApi, @@ -73,7 +73,7 @@ void main() { ))); } - testWidgets('exporter pick invalid and exist', (tester) async { + testWidgets('exporter pick invalid and exit', (tester) async { when(cache.get(eCacheKey)).thenReturn('old-id:true:old-name'); await tester.pumpWidget(buildApp()); @@ -84,10 +84,13 @@ void main() { expect(editorW.controller?.text, equals('old-id')); await tester.enterText(editor, 'QQ'); + await tester.tap(find.byKey(const Key('text_dialog.confirm'))); + await tester.pump(); + // not in dialog + expect(find.text(S.transitGSErrorSpreadsheetIdInvalid), findsOneWidget); + await tester.tap(find.byKey(const Key('text_dialog.cancel'))); await tester.pumpAndSettle(); - // not in dialog - expect(find.byKey(const Key('text_dialog.cancel')), findsNothing); }); testWidgets('exporter pick not exist', (tester) async { @@ -108,7 +111,7 @@ void main() { await tester.tap(find.byKey(const Key('text_dialog.confirm'))); await tester.pumpAndSettle(); - expect(find.text('找不到表單'), findsOneWidget); + expect(find.text(S.transitGSErrorImportNotFoundSpreadsheet), findsOneWidget); }); testWidgets('exporter pick success', (tester) async { @@ -166,7 +169,7 @@ void main() { final sheetsApi = getMockSheetsApi(); await tester.pumpWidget(buildApp(sheetsApi)); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(Tab, S.btnImport)); + await tester.tap(find.widgetWithText(Tab, S.transitImportBtn)); await tester.pumpAndSettle(); expect(getSelector('menu').initialValue?.title, equals('menu title')); @@ -217,4 +220,18 @@ void main() { initializeAuth(); }); }); + + test('GoogleSheetProperties hash code', () { + final a = GoogleSheetProperties(1, 'title'); + final b = GoogleSheetProperties(1, 'title'); + final c = GoogleSheetProperties(1, 'title2'); + final d = GoogleSheetProperties(2, 'title'); + + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + expect(a, isNot(equals(c))); + expect(a.hashCode, isNot(equals(c.hashCode))); + expect(a, isNot(equals(d))); + expect(a.hashCode, isNot(equals(d.hashCode))); + }); } diff --git a/test/ui/transit/plain_text/export_basic_test.dart b/test/ui/transit/plain_text/export_basic_test.dart index 244c9eab..465d555a 100644 --- a/test/ui/transit/plain_text/export_basic_test.dart +++ b/test/ui/transit/plain_text/export_basic_test.dart @@ -8,8 +8,9 @@ import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/replenisher.dart'; import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/models/stock/quantity.dart'; +import 'package:possystem/translator.dart'; +import 'package:possystem/ui/transit/plain_text/views.dart' as pt; import 'package:possystem/ui/transit/transit_station.dart'; -import 'package:possystem/ui/transit/plain_text_widgets/views.dart' as pt; import '../../../mocks/mock_storage.dart'; import '../../../test_helpers/translator.dart'; @@ -25,14 +26,12 @@ void main() { return const MaterialApp( home: TransitStation( exporter: PlainTextExporter(), - type: TransitType.basic, + catalog: TransitCatalog.model, method: TransitMethod.plainText, ), ); } - const message = '共設定 1 種份量\n\n第1種份量叫做 q1,預設會讓成分的份量乘以 1 倍。'; - test('test key attribute exist', () { var i = 1; pt.ExportBasicView(key: Key('test.${i++}')); @@ -51,7 +50,12 @@ void main() { await tester.pumpAndSettle(); final copied = await Clipboard.getData('text/plain'); - expect(copied?.text, equals(message)); + expect( + copied?.text, + equals([ + S.transitPTFormatModelQuantitiesHeader(1), + S.transitPTFormatModelQuantitiesQuantity('1', 'q1', '1'), + ].join('\n\n'))); }); setUpAll(() { diff --git a/test/ui/transit/plain_text/export_order_test.dart b/test/ui/transit/plain_text/export_order_test.dart index e5ed178d..2c642b3f 100644 --- a/test/ui/transit/plain_text/export_order_test.dart +++ b/test/ui/transit/plain_text/export_order_test.dart @@ -3,9 +3,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:possystem/helpers/exporter/plain_text_exporter.dart'; import 'package:possystem/models/objects/order_object.dart'; -import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; +import 'package:possystem/ui/transit/plain_text/views.dart'; import 'package:possystem/ui/transit/transit_station.dart'; -import 'package:possystem/ui/transit/plain_text_widgets/views.dart'; import '../../../mocks/mock_database.dart'; import '../../../mocks/mock_storage.dart'; @@ -24,7 +24,7 @@ void main() { return const MaterialApp( home: TransitStation( exporter: PlainTextExporter(), - type: TransitType.order, + catalog: TransitCatalog.order, method: TransitMethod.plainText, ), ); @@ -35,12 +35,38 @@ void main() { OrderSetter.setMetrics([order], countingAll: true); OrderSetter.setOrders([order]); - const message = '共 40 元,其中的 20 元是產品價錢。\n' - '付額 0 元、成分 30 元。\n' - '顧客的 oa-1 為 oao-1、oa-2 為 oao-2。\n' - '餐點有 10 份(2 種)包括:\n' - 'p-1(c-1)5 份共 35 元,成份包括 i-1(q-1,使用 3 個)、i-2(預設份量)、i-3(預設份量,使用 -5 個);\n' - 'p-2(c-2)15 份共 300 元,沒有設定成分。'; +// const message = '''Total price \$40, 20 of them are product price. +// Paid \$0, cost \$30. +// Customer's oa-1 is oao-1、oa-2 is oao-2. +// There are 10 products (2 types of set) including: +// 5 of p-1 (c-1), total price is \$35, ingredients are i-1 (q-1), used 3、i-2 (default quantity)、i-3 (default quantity), used -5; +// 15 of p-2 (c-2), total price is \$300, no ingredient settings. +// '''; + final message = [ + S.transitPTFormatOrderPrice(1, '40', '20'), + S.transitPTFormatOrderMoney('0', '30'), + S.transitPTFormatOrderOrderAttribute([ + S.transitPTFormatOrderOrderAttributeItem('oa-1', 'oao-1'), + S.transitPTFormatOrderOrderAttributeItem('oa-2', 'oao-2'), + ].join('、')), + S.transitPTFormatOrderProductCount( + 10, + 2, + [ + S.transitPTFormatOrderProduct( + 1, + 'p-1', + 'c-1', + 5, + '35', + [ + S.transitPTFormatOrderIngredient(3, 'i-1', 'q-1'), + S.transitPTFormatOrderIngredient(0, 'i-2', S.transitPTFormatOrderNoQuantity), + S.transitPTFormatOrderIngredient(-5, 'i-3', S.transitPTFormatOrderNoQuantity), + ].join('、')), + S.transitPTFormatOrderProduct(0, 'p-2', 'c-2', 15, '300', ''), + ].join(';\n')), + ].join('\n'); await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); @@ -58,16 +84,22 @@ void main() { await tester.pumpAndSettle(); final copied = await Clipboard.getData('text/plain'); + expect( copied?.text, - equals('3月4日 05:06:07\n$message'), + equals([S.transitOrderItemTitle(order.createdAt), message].join('\n')), ); }); test('format', () { - const expected = '共 0 元。\n' - '付額 0 元、成分 0 元。\n' - '餐點有 0 份包括:\n。'; +// const expected = '''Total price \$0. +// Paid \$0, cost \$0. +// There is no product.'''; + final expected = [ + S.transitPTFormatOrderPrice(0, '0', '0'), + S.transitPTFormatOrderMoney('0', '0'), + S.transitPTFormatOrderProductCount(0, 0, ''), + ].join('\n'); final actual = ExportOrderView.formatOrder( OrderObject(createdAt: DateTime.now()), @@ -80,8 +112,6 @@ void main() { initializeTranslator(); initializeDatabase(); initializeStorage(); - // init dependencies - CurrencySetting().isInt = true; }); }); } diff --git a/test/ui/transit/plain_text/import_basic_test.dart b/test/ui/transit/plain_text/import_basic_test.dart index a92dff66..8a3e1185 100644 --- a/test/ui/transit/plain_text/import_basic_test.dart +++ b/test/ui/transit/plain_text/import_basic_test.dart @@ -19,21 +19,17 @@ void main() { return const MaterialApp( home: TransitStation( exporter: PlainTextExporter(), - type: TransitType.basic, + catalog: TransitCatalog.model, method: TransitMethod.plainText, ), ); } - const message = '共設定 1 種份量\n\n第1種份量叫做 q1,預設會讓成分的份量乘以 1 倍。'; - testWidgets('wrong text', (tester) async { - const warnMsg = '這段文字無法匹配相應的服務,請參考匯出時的文字內容'; - await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(Tab, S.btnImport)); + await tester.tap(find.widgetWithText(Tab, S.transitImportBtn)); await tester.pumpAndSettle(); await tester.enterText( @@ -43,17 +39,20 @@ void main() { await tester.tap(find.byKey(const Key('import_btn'))); await tester.pumpAndSettle(); - expect(find.text(warnMsg), findsOneWidget); + expect(find.text(S.transitPTImportErrorNotFound), findsOneWidget); }); testWidgets('successfully', (tester) async { await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); - await tester.tap(find.widgetWithText(Tab, S.btnImport)); + await tester.tap(find.widgetWithText(Tab, S.transitImportBtn)); await tester.pumpAndSettle(); - await tester.enterText(find.byKey(const Key('import_text')), message); + await tester.enterText( + find.byKey(const Key('import_text')), + '${S.transitPTFormatModelQuantitiesHeader(1)}\n\n' + '${S.transitPTFormatModelQuantitiesQuantity('1', 'q1', '1')}'); await tester.tap(find.byKey(const Key('import_btn'))); await tester.pumpAndSettle(); diff --git a/test/ui/transit/transit_order_list_test.dart b/test/ui/transit/transit_order_list_test.dart index c41cd131..73114651 100644 --- a/test/ui/transit/transit_order_list_test.dart +++ b/test/ui/transit/transit_order_list_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:possystem/settings/currency_setting.dart'; +import 'package:possystem/translator.dart'; import 'package:possystem/ui/transit/transit_order_list.dart'; import '../../mocks/mock_database.dart'; @@ -15,7 +15,7 @@ void main() { final widget = TransitOrderList( notifier: ValueNotifier(range), formatOrder: (o) => const Text('hi'), - memoryPredictor: (m) => m.price.toInt(), + memoryPredictor: (m) => m.revenue.toInt(), warning: 'hi there', ); @@ -33,7 +33,7 @@ void main() { await showDialog(tester, Icons.check_outlined); - expect(find.text('預估容量為:<1KB'), findsOneWidget); + expect(find.text(S.transitOrderCapacityTitle('<1KB')), findsOneWidget); }); testWidgets('memory usage show warning', (tester) async { @@ -43,7 +43,7 @@ void main() { await showDialog(tester, Icons.warning_amber_outlined); - expect(find.text('預估容量為:700KB'), findsOneWidget); + expect(find.text(S.transitOrderCapacityTitle('700KB')), findsOneWidget); }); testWidgets('memory usage show danger', (tester) async { @@ -53,14 +53,12 @@ void main() { await showDialog(tester, Icons.dangerous_outlined); - expect(find.text('預估容量為:1.5MB'), findsOneWidget); + expect(find.text(S.transitOrderCapacityTitle('1.5MB')), findsOneWidget); }); setUpAll(() { initializeDatabase(); initializeTranslator(); - - CurrencySetting().isInt = true; }); }); } diff --git a/test/ui/transit/transit_page_test.dart b/test/ui/transit/transit_page_test.dart index df0d7aef..f3dc9c33 100644 --- a/test/ui/transit/transit_page_test.dart +++ b/test/ui/transit/transit_page_test.dart @@ -8,7 +8,6 @@ import 'package:possystem/models/repository/quantities.dart'; import 'package:possystem/models/repository/replenisher.dart'; import 'package:possystem/models/repository/stock.dart'; import 'package:possystem/routes.dart'; -import 'package:possystem/settings/currency_setting.dart'; import 'package:possystem/ui/transit/transit_page.dart'; import '../../mocks/mock_auth.dart'; @@ -64,7 +63,6 @@ void main() { Quantities(); Replenisher(); OrderAttributes(); - CurrencySetting().isInt = true; when(auth.authStateChanges()).thenAnswer((_) => Stream.value(null)); });