From e5976eeb1b440b9f1fc24f5a587b26917796a5b8 Mon Sep 17 00:00:00 2001
From: Miguel Caldas <23634895+MiguelCaldasMS@users.noreply.github.com>
Date: Thu, 22 Oct 2026 12:09:59 +0100
Subject: [PATCH 01/10] feat(i18n): add English and Chinese localization
Use Flutter ARB generation with persistent language selection, localized command help and dump analysis, bilingual UI coverage, and translation documentation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/workflows/build.yml | 5 +
.gitignore | 2 +
CHANGELOG.md | 6 +-
CONTRIBUTING.md | 8 +
README.en.md | 87 +
README.md | 14 +-
docs/localization.md | 93 +
l10n.yaml | 7 +
.../desktop_cli/desktop_cli_backend.dart | 9 +-
lib/backend/desktop_cli/pm3_process.dart | 146 +-
lib/core/pm3/pm3_controller.dart | 14 +-
lib/l10n/app_en.arb | 3342 +++++++++++++++++
lib/l10n/app_locale_controller.dart | 90 +
lib/l10n/app_zh.arb | 702 ++++
lib/l10n/l10n.dart | 8 +
lib/l10n/localized_messages.dart | 15 +
lib/main.dart | 60 +-
lib/models/command_card.dart | 28 +-
lib/models/dump_analysis.dart | 150 +-
lib/parsers/dump_parser.dart | 12 +-
lib/parsers/key_parser.dart | 12 +-
lib/services/dump_converter.dart | 85 +-
lib/services/locale_settings_store.dart | 54 +
lib/services/pm3_command_catalog.dart | 23 +-
lib/state/app_state.dart | 28 +-
lib/state/connection_state.dart | 17 +-
lib/state/file_state.dart | 5 +-
lib/state/hardware_state.dart | 10 +-
lib/ui/components/connection_guard.dart | 5 +-
lib/ui/components/hex_input_field.dart | 8 +-
lib/ui/components/result_display.dart | 36 +-
lib/ui/home_page.dart | 99 +-
lib/ui/pages/connection_page.dart | 234 +-
lib/ui/pages/data_page.dart | 60 +-
lib/ui/pages/dump_compare_page.dart | 43 +-
lib/ui/pages/dump_viewer_page.dart | 683 ++--
lib/ui/pages/hf_14b_page.dart | 49 +-
lib/ui/pages/hf_15_page.dart | 64 +-
lib/ui/pages/hf_emv_page.dart | 15 +-
lib/ui/pages/hf_felica_page.dart | 49 +-
lib/ui/pages/hf_fido_page.dart | 17 +-
lib/ui/pages/hf_iclass_page.dart | 63 +-
lib/ui/pages/hf_legic_page.dart | 48 +-
lib/ui/pages/hf_mfdes_page.dart | 89 +-
lib/ui/pages/hf_mfu_page.dart | 89 +-
lib/ui/pages/hf_seos_page.dart | 11 +-
lib/ui/pages/hf_sniff_page.dart | 22 +-
lib/ui/pages/lf_awid_page.dart | 21 +-
lib/ui/pages/lf_fdxb_page.dart | 11 +-
lib/ui/pages/lf_hid_page.dart | 39 +-
lib/ui/pages/lf_hitag_page.dart | 29 +-
lib/ui/pages/lf_indala_page.dart | 13 +-
lib/ui/pages/lf_io_page.dart | 13 +-
lib/ui/pages/lf_keri_page.dart | 11 +-
lib/ui/pages/lf_page.dart | 64 +-
lib/ui/pages/lf_pyramid_page.dart | 19 +-
lib/ui/pages/mifare_page.dart | 161 +-
lib/ui/pages/nfc_page.dart | 8 +-
lib/ui/pages/script_page.dart | 15 +-
lib/ui/pages/settings_page.dart | 130 +-
lib/ui/pages/terminal_page.dart | 82 +-
lib/ui/pages/trace_page.dart | 25 +-
lib/ui/widgets/command_card_editor.dart | 93 +-
pubspec.lock | 31 +-
pubspec.yaml | 6 +-
test/l10n/app_locale_controller_test.dart | 101 +
test/l10n/localization_test.dart | 213 ++
test/support/localized_test_app.dart | 37 +
test/ui/command_card_editor_test.dart | 4 +-
test/ui/page_output_test.dart | 32 +-
test/ui/settings_page_test.dart | 3 +-
tools/check_localizations.dart | 50 +
72 files changed, 6674 insertions(+), 1253 deletions(-)
create mode 100644 README.en.md
create mode 100644 docs/localization.md
create mode 100644 l10n.yaml
create mode 100644 lib/l10n/app_en.arb
create mode 100644 lib/l10n/app_locale_controller.dart
create mode 100644 lib/l10n/app_zh.arb
create mode 100644 lib/l10n/l10n.dart
create mode 100644 lib/l10n/localized_messages.dart
create mode 100644 lib/services/locale_settings_store.dart
create mode 100644 test/l10n/app_locale_controller_test.dart
create mode 100644 test/l10n/localization_test.dart
create mode 100644 test/support/localized_test_app.dart
create mode 100644 tools/check_localizations.dart
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c7d39d6..b1d6608 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -54,6 +54,11 @@ jobs:
- name: Get dependencies
run: flutter pub get
+ - name: Generate and check localizations
+ run: |
+ flutter gen-l10n
+ dart run tools/check_localizations.dart
+
- name: Analyze
if: runner.os == 'Linux'
run: flutter analyze --no-fatal-infos
diff --git a/.gitignore b/.gitignore
index f748180..3d63512 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,6 +60,8 @@ coverage/
*.freezed.dart
*.mocks.dart
lib/generated_plugin_registrant.dart
+lib/l10n/app_localizations.dart
+lib/l10n/app_localizations_*.dart
# OS
Thumbs.db
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1fa58b0..b13182d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,11 @@
### Added
+- Added English and Simplified Chinese ARB localizations, localized framework
+ delegates, immediate language switching with a persistent system-default
+ option, locale-aware command help and dump analysis, translation checks,
+ bilingual UI coverage and an English README.
+
- 从当前客户端 96 个帮助分组同步 1033 个终端命令条目,并提供 `tools/sync_command_catalog.dart` 更新工具;已有中文说明保留。
## [0.0.3] - 2026-09-04
@@ -34,4 +39,3 @@
[Unreleased]: https://github.com/AKCX2002/Promark3-GUI/compare/v0.0.3...HEAD
[0.0.3]: https://github.com/AKCX2002/Promark3-GUI/compare/v0.0.3-alpha...v0.0.3
-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e396c5b..c1127e3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,6 +27,14 @@ flutter test --no-pub
涉及桌面打包或平台代码时,再执行对应平台的 `flutter build`。涉及真实硬件行为时,请说明设备型号、只读/写入范围和实际验证结果;不要把模拟测试写成实机通过。
+## Localization
+
+English and Simplified Chinese resources are maintained in `lib/l10n/*.arb`.
+Follow [the translation workflow](docs/localization.md), update both catalogs,
+and run `flutter gen-l10n` and `dart run tools/check_localizations.dart` before
+the existing analysis/tests. Never edit generated localization Dart files or
+translate PM3 command syntax, protocol identifiers or raw hardware output.
+
## Pull Request
- 保持改动聚焦,并说明问题、解决方式和验证结果。
diff --git a/README.en.md b/README.en.md
new file mode 100644
index 0000000..de090ae
--- /dev/null
+++ b/README.en.md
@@ -0,0 +1,87 @@
+# PM3 GUI
+
+[简体中文](README.md) | **English**
+
+A GPL-3.0 Flutter desktop interface for the official Proxmark3 client.
+The interface is available in Simplified Chinese and English.
+Windows x64 and Linux x64 are supported desktop targets; the Proxmark3
+client and firmware are installed separately.
+
+Only operate on devices, cards and systems you own or are explicitly
+authorized to use.
+
+## Languages
+
+Choose **Settings → Language**:
+
+- **System default** follows the operating system's preferred languages.
+ Unsupported languages fall back to English.
+- **简体中文** selects Simplified Chinese.
+- **English** selects English.
+
+Changes apply immediately without restarting or disconnecting the device.
+The selection is saved in `language.json` in the operating system's application
+support directory, separately from the existing `pm3_settings.json`.
+Read/write failures are shown in Settings.
+
+The application uses Flutter's standard ARB / `gen-l10n` pipeline, typed
+localization accessors, ICU placeholders/plurals and localized Material,
+Cupertino and widget delegates. Both languages cover navigation, operation
+pages, warnings, editing, dump analysis and application-generated messages.
+Terminal command help switches languages as well.
+
+Proxmark3 commands, protocol names, hexadecimal data, file-format identifiers,
+exported machine-readable content and raw client output are intentionally
+unchanged. Native operating system file dialogs follow the operating system's
+language; their appearance is not controlled by Flutter.
+
+## Development
+
+Use the stable Flutter SDK with its bundled Dart SDK and the Windows or Linux
+desktop toolchain. This change was developed with Flutter 3.47.7 / Dart 3.13.5;
+`flutter pub get` resolves the framework-pinned dependencies for your SDK.
+
+```powershell
+git clone https://github.com/Babel36acl/Promark3-GUI.git
+Set-Location .\Promark3-GUI
+flutter pub get
+flutter run -d windows
+```
+
+For Linux, run `flutter run -d linux`. If Flutter is not in the current process's
+PATH, invoke your installed SDK's `bin\flutter.bat` directly on Windows.
+
+On Windows, select `pm3.bat` in the **root of the official Proxmark3 distribution**
+in the connection page or Settings. The script initializes its own environment
+and detects devices. Saved GUI port/argument values are not passed to that
+script. Native clients, including Linux `proxmark3`, still use the selected
+port and arguments. Each launch argument is entered on a separate line;
+spaces, quotes and backslashes are preserved.
+
+```powershell
+flutter gen-l10n
+dart run tools\check_localizations.dart
+flutter analyze --no-pub --no-fatal-infos
+flutter test --no-pub
+flutter build windows --release --no-pub
+```
+
+`flutter pub get` and builds also generate localizations automatically.
+Generated localization Dart files are ignored by Git; edit the ARB files,
+not generated accessors.
+
+For translator guidance and adding another language, see
+[Localization](docs/localization.md). The original [Chinese README](README.md)
+describes file formats, session logging and device integration in more detail.
+
+## Privacy and license
+
+The application does not upload device data or session logs. Local logs and
+dump files can contain card identifiers, keys and other sensitive information;
+review and redact them before sharing.
+
+See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md) and
+[LICENSE](LICENSE).
+The Proxmark3 client and firmware belong to
+[RfidResearchGroup/proxmark3](https://github.com/RfidResearchGroup/proxmark3)
+and their respective contributors; they are not redistributed by this project.
diff --git a/README.md b/README.md
index ec2f119..8882d78 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# PM3 GUI
+**简体中文** | [English](README.en.md)
+

@@ -13,6 +15,12 @@ PM3 GUI 是面向 Proxmark3 的 GPL-3.0 开源桌面图形客户端,提供常
请仅对自有或已明确授权的设备、卡片和系统执行操作,并遵守所在地适用法律。
+## 界面语言
+
+在“设置 → 语言”中选择“跟随系统”、“简体中文”或“English”。更改立即生效,无需重启或断开设备;不受支持的系统语言使用英语。语言设置独立保存,不影响现有 PM3 配置、草稿和设备数据。命令帮助说明也会切换语言;原始客户端输出、命令语法和导出格式保持不变。
+
+翻译维护和新增语言请参阅 [Localization](docs/localization.md)。
+
## 平台与范围
- Windows x64 是 1.0 主平台。
@@ -90,14 +98,14 @@ PM3EASY512K 等已由官方 [RfidResearchGroup/proxmark3](https://github.com/Rfi
### 环境要求
-- Flutter 3.27+
-- Dart 3.6+
+- Flutter stable(本次验证使用 3.47.7)
+- 随 Flutter 提供的 Dart SDK(本次验证使用 3.13.5)
- 官方 Proxmark3 client:Windows 发行包根目录 `pm3.bat`,或 Linux `proxmark3`
### 运行
```bash
-git clone https://github.com/AKCX2002/Promark3-GUI.git
+git clone https://github.com/Babel36acl/Promark3-GUI.git
cd Promark3-GUI
flutter pub get
flutter run -d windows
diff --git a/docs/localization.md b/docs/localization.md
new file mode 100644
index 0000000..c4aaebd
--- /dev/null
+++ b/docs/localization.md
@@ -0,0 +1,93 @@
+# Localization
+
+## Design
+
+This project follows Flutter's official
+[internationalization guide](https://docs.flutter.dev/ui/internationalization):
+
+- `flutter_localizations` supplies framework translations and text direction.
+- `lib/l10n/app_en.arb` is the template; `app_zh.arb` preserves Simplified Chinese.
+- `l10n.yaml` configures `flutter gen-l10n`; generated Dart files live beside
+ the ARB files and are not committed.
+- `MaterialApp` uses generated delegates, supported locales and
+ `onGenerateTitle`. English is the fallback for unsupported system languages.
+- UI code reads `context.l10n`. Stateful pages remain mounted when language
+ changes; navigation, text controllers, draft data and connections are retained.
+- Domain objects receive localization explicitly through `updateLocalizations`
+ or optional `localizations` arguments. There is no mutable global locale or
+ run-time dictionary that replaces text in hardware output. The immutable
+ Chinese default preserves existing non-UI caller behavior.
+- Dump-analysis caches include the locale; terminal suggestions are reloaded
+ on locale changes with stale asynchronous results discarded.
+
+## Editing messages
+
+Give messages stable, semantic camelCase keys. Localize complete sentences:
+do not concatenate translated word fragments, use display text as a storage
+key, or decide application behavior by comparing translated strings.
+
+Each template entry requires `@messageKey` translator metadata. Interpolations
+use named ARB placeholders with explicit Dart types. Counts with grammatical
+variants use ICU plurals, for example:
+
+```json
+{
+ "messageCommands": "{value, plural, =0{No commands} =1{1 command} other{{value} commands}}",
+ "@messageCommands": {
+ "description": "Number of commands in terminal history.",
+ "placeholders": {
+ "value": {"type": "int"}
+ }
+ }
+}
+```
+
+Keep placeholder names identical across languages, but allow translators to
+reorder them. Hex values, commands, protocol names, serialized field names and
+exported data must remain unchanged. Raw client output and OS diagnostic
+details are not machine-translated.
+
+After editing:
+
+```powershell
+flutter gen-l10n
+dart run tools\check_localizations.dart
+flutter analyze --no-pub --no-fatal-infos
+flutter test --no-pub
+```
+
+The resource check rejects missing/extra/empty messages, missing template
+metadata and untranslated Chinese in the English catalog. `gen-l10n` validates
+ICU syntax and typed placeholders. Tests cover both languages on all 30 pages,
+live switching, data retention, persistence, failure reporting, locale fallback,
+framework strings, command-help language and locale-aware analysis.
+
+Use wrapping or flexible layouts for captions that expand in translation.
+Do not shorten safety warnings to make them fit.
+
+## Adding a language
+
+1. Copy the template to `lib/l10n/app_
.arb`, set `@@locale`, and
+ translate every message while preserving placeholders.
+2. Add the language's native name to the selector in
+ `lib/ui/pages/settings_page.dart`. Native language names are deliberately
+ constant so users can recover from choosing an unfamiliar language.
+3. Run the commands above and exercise the new locale, including dialogs,
+ errors, larger text and long captions.
+4. If translated command help is available, add its asset and update
+ `Pm3CommandCatalog` selection. Otherwise document that command help remains
+ in English. Do not translate command syntax.
+
+Generated `supportedLocales` and the preference controller recognize languages
+from the ARB files, so a separate hard-coded locale list is not required.
+
+## Preference storage
+
+`LocaleSettingsStore` writes `language.json` in the application support
+directory using an exclusive temporary file, a flushed write and same-directory
+replacement. `null` means system default. Writes are serialized and flushed
+during application exit. Missing files are normal; corrupt, unsupported or
+unwritable preferences produce an explicit Settings error.
+
+Language settings are separate from Proxmark3 launch settings and do not change
+session records, device data, dump formats or client configuration.
diff --git a/l10n.yaml b/l10n.yaml
new file mode 100644
index 0000000..213fd96
--- /dev/null
+++ b/l10n.yaml
@@ -0,0 +1,7 @@
+arb-dir: lib/l10n
+template-arb-file: app_en.arb
+output-localization-file: app_localizations.dart
+output-class: AppLocalizations
+required-resource-attributes: true
+untranslated-messages-file: build/l10n_untranslated.json
+format: true
diff --git a/lib/backend/desktop_cli/desktop_cli_backend.dart b/lib/backend/desktop_cli/desktop_cli_backend.dart
index d53a972..50cfe0c 100644
--- a/lib/backend/desktop_cli/desktop_cli_backend.dart
+++ b/lib/backend/desktop_cli/desktop_cli_backend.dart
@@ -8,9 +8,10 @@ import 'package:pm3gui/core/pm3/pm3_event.dart';
import 'package:pm3gui/core/pm3/pm3_result.dart';
import 'pm3_process.dart';
+import 'package:pm3gui/l10n/localized_messages.dart';
/// Desktop backend backed by the official Proxmark3 CLI process.
-final class DesktopCliBackend implements Pm3Backend {
+final class DesktopCliBackend with LocalizedMessages implements Pm3Backend {
DesktopCliBackend({Pm3Process? process})
: _process = process ?? Pm3Process() {
_outputSubscription = _process.outputStream.listen((line) {
@@ -29,6 +30,12 @@ final class DesktopCliBackend implements Pm3Backend {
Pm3ConnectionConfig? _config;
Future? _shutdownFuture;
+ @override
+ void updateLocalizations(AppLocalizations value) {
+ super.updateLocalizations(value);
+ _process.updateLocalizations(value);
+ }
+
/// Selects a sensible external-client default without leaking platform
/// checks into application state or features.
static String detectExecutable() {
diff --git a/lib/backend/desktop_cli/pm3_process.dart b/lib/backend/desktop_cli/pm3_process.dart
index 1cc9fa9..c204ca6 100644
--- a/lib/backend/desktop_cli/pm3_process.dart
+++ b/lib/backend/desktop_cli/pm3_process.dart
@@ -10,6 +10,7 @@ library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
+import 'package:pm3gui/l10n/localized_messages.dart';
/// PM3 客户端进程的连接状态
enum Pm3ProcessState {
@@ -67,7 +68,7 @@ final class _IoPm3ProcessHandle implements Pm3ProcessHandle {
throw ProcessException(
'taskkill',
arguments,
- '整树终止失败 (exit=${result.exitCode})${detail.isEmpty ? '' : ': $detail'}',
+ 'Failed to terminate process tree (exit=${result.exitCode})${detail.isEmpty ? '' : ': $detail'}',
result.exitCode,
);
}
@@ -92,7 +93,7 @@ Future _startPm3Process(
));
/// 包装 pm3 命令行进程以进行通信
-class Pm3Process {
+class Pm3Process with LocalizedMessages {
Pm3ProcessHandle? _process; // PM3 进程实例
StreamSubscription? _stdoutSubscription;
StreamSubscription? _stderrSubscription;
@@ -196,7 +197,7 @@ class Pm3Process {
String? workingDirectory,
}) {
if (_disposed || _shuttingDown) {
- _lastError = 'PM3 进程管理器已释放';
+ _lastError = l10n.messageThePm3ProcessManagerHasBeenDisposed;
return Future.value(false);
}
if (_connectingFuture != null) return _connectingFuture!;
@@ -227,8 +228,9 @@ class Pm3Process {
final elapsed = DateTime.now().difference(_lastConnectAttempt!);
if (elapsed < _connectCooldown) {
final wait = _connectCooldown - elapsed;
- _lastError = '请等待 ${wait.inSeconds + 1} 秒后再试';
- _emitOutput('[请等待冷却: ${wait.inSeconds + 1}s]');
+ _lastError = l10n.messagePleaseWaitSecondsBeforeRetrying(
+ (wait.inSeconds + 1).toString());
+ _emitOutput(l10n.messageCoolingDownS((wait.inSeconds + 1).toString()));
return false;
}
}
@@ -246,19 +248,18 @@ class Pm3Process {
// 解析实际可执行文件路径
final resolved = resolvePm3Path(pm3Path);
if (resolved == null) {
- _lastError = '找不到 PM3 程序: $pm3Path\n'
- 'Windows 请优先选择官方发行包根目录的 pm3.bat;'
- 'Linux 请选择 proxmark3';
- _emitOutput('[错误] $_lastError');
+ _lastError =
+ l10n.messagePm3ProgramNotFoundOnWindowsSelect((pm3Path).toString());
+ _emitOutput(l10n.messageError((_lastError).toString()));
_setState(Pm3ProcessState.disconnected);
return false;
}
final (execPath, resolvedWorkDir) = resolved;
final workDir = workingDirectory ?? resolvedWorkDir;
- _emitOutput('[使用 PM3: $execPath]');
+ _emitOutput(l10n.messageUsingPm3((execPath).toString()));
if (workDir != null) {
- _emitOutput('[工作目录: $workDir]');
+ _emitOutput(l10n.messageWorkingDirectory((workDir).toString()));
}
// 官方 Windows 发行包以根目录 pm3.bat 作为入口。脚本会自行完成
@@ -292,13 +293,17 @@ class Pm3Process {
_connectionCompleter = completer;
var connected = false;
var batchHandshakeSent = false;
- final readyMarker = 'PM3GUI_READY_${connectionGeneration}_${DateTime.now().microsecondsSinceEpoch}';
+ final readyMarker =
+ 'PM3GUI_READY_${connectionGeneration}_${DateTime.now().microsecondsSinceEpoch}';
var stdoutPending = '';
void handleStdoutRecord(String line) {
if (!identical(_process, process) || _disposed) return;
- if (batchHandshakeSent && line.contains('pm3 --> rem $readyMarker')) return;
- if (batchHandshakeSent && line.trimRight().endsWith('remark: $readyMarker')) {
+ if (batchHandshakeSent && line.contains('pm3 --> rem $readyMarker')) {
+ return;
+ }
+ if (batchHandshakeSent &&
+ line.trimRight().endsWith('remark: $readyMarker')) {
connected = true;
_setState(Pm3ProcessState.connected);
if (!completer.isCompleted) completer.complete(true);
@@ -320,7 +325,7 @@ class Pm3Process {
// 检测致命错误和连接提示
if (_detectFatalError(line)) {
- _failConnecting(completer, 'PM3 stdout 出现致命错误');
+ _failConnecting(completer, l10n.messageFatalErrorOnPm3Stdout);
unawaited(_closeCurrentProcess(
process,
connectionGeneration,
@@ -351,8 +356,9 @@ class Pm3Process {
await process.writeLine('rem $readyMarker');
} catch (error) {
if (!identical(_process, process) || _disposed) return;
- _lastError = 'PM3 BAT 只读握手写入失败: $error';
- _emitOutput('[错误] $_lastError');
+ _lastError =
+ l10n.messageFailedToSendTheReadOnlyPm3((error).toString());
+ _emitOutput(l10n.messageError((_lastError).toString()));
_failConnecting(completer, _lastError);
await _closeCurrentProcess(
process,
@@ -391,10 +397,13 @@ class Pm3Process {
// 监听 stdout
// 部分 Windows 客户端帮助文本含非 UTF-8 字节;保留后续输出与会话。
- _stdoutSubscription = process.stdout.transform(const Utf8Decoder(allowMalformed: true)).listen(
+ _stdoutSubscription = process.stdout
+ .transform(const Utf8Decoder(allowMalformed: true))
+ .listen(
handleStdoutChunk,
onError: (Object error, StackTrace stackTrace) {
- _failConnecting(completer, 'PM3 stdout 流错误: $error');
+ _failConnecting(
+ completer, l10n.messagePm3StdoutStreamError((error).toString()));
unawaited(_closeCurrentProcess(
process,
connectionGeneration,
@@ -422,7 +431,7 @@ class Pm3Process {
.listen((line) {
if (!identical(_process, process) || _disposed) return;
if (_detectFatalError(line)) {
- _failConnecting(completer, 'PM3 stderr 出现致命错误');
+ _failConnecting(completer, l10n.messageFatalErrorOnPm3Stderr);
unawaited(_closeCurrentProcess(
process,
connectionGeneration,
@@ -433,7 +442,8 @@ class Pm3Process {
if (_commandCompleter != null) _responseBuffer.writeln('[ERR] $line');
_emitOutput('[ERR] $line');
}, onError: (Object error, StackTrace stackTrace) {
- _failConnecting(completer, 'PM3 stderr 流错误: $error');
+ _failConnecting(
+ completer, l10n.messagePm3StderrStreamError((error).toString()));
unawaited(_closeCurrentProcess(
process,
connectionGeneration,
@@ -452,15 +462,17 @@ class Pm3Process {
unawaited(process.exitCode.then(
(code) async {
if (!identical(_process, process) || _disposed) return;
- _emitOutput('[PM3 进程退出, code=$code]');
+ _emitOutput(l10n.messagePm3ProcessExitedCode((code).toString()));
if (!connected) {
- _lastError = 'PM3 进程在连接提示前退出 (code=$code)';
+ _lastError = l10n.messagePm3ProcessExitedBeforeTheConnectionPrompt(
+ (code).toString());
if (!completer.isCompleted) completer.complete(false);
}
await _closeCurrentProcess(process, connectionGeneration);
},
onError: (Object error, StackTrace stackTrace) async {
- _failConnecting(completer, 'PM3 进程退出状态错误: $error');
+ _failConnecting(completer,
+ l10n.messagePm3ProcessExitStatusError((error).toString()));
await _closeCurrentProcess(
process,
connectionGeneration,
@@ -474,8 +486,9 @@ class Pm3Process {
return await completer.future.timeout(_connectTimeout);
} on TimeoutException {
if (!connected && identical(_process, process)) {
- _lastError = '连接超时 (${_connectTimeout.inSeconds}s),请检查设备是否已连接';
- _emitOutput('[连接超时]');
+ _lastError = l10n.messageConnectionTimedOutSCheckThatThe(
+ (_connectTimeout.inSeconds).toString());
+ _emitOutput(l10n.messageConnectionTimedOut);
await _closeCurrentProcess(
process,
connectionGeneration,
@@ -486,18 +499,17 @@ class Pm3Process {
}
} on ProcessException catch (e) {
_startingFuture = null;
- _lastError = '无法启动 PM3 程序: ${e.message}\n'
- '路径: $pm3Path\n'
- '请检查路径是否正确,程序是否已编译';
- _emitOutput('[错误] $_lastError');
+ _lastError = l10n.messageCouldNotStartPm3PathCheckThe(
+ (e.message).toString(), (pm3Path).toString());
+ _emitOutput(l10n.messageError((_lastError).toString()));
if (connectionGeneration == _processGeneration) {
_setState(Pm3ProcessState.disconnected);
}
return false;
} catch (e) {
_startingFuture = null;
- _lastError = '连接失败: $e';
- _emitOutput('[错误] $_lastError');
+ _lastError = l10n.messageConnectionFailed((e).toString());
+ _emitOutput(l10n.messageError((_lastError).toString()));
if (connectionGeneration == _processGeneration) {
_setState(Pm3ProcessState.disconnected);
}
@@ -508,7 +520,7 @@ class Pm3Process {
/// 向交互式会话发送命令
Future sendCommand(String command) async {
if (_process == null || _state != Pm3ProcessState.connected) {
- _emitOutput('[未连接]');
+ _emitOutput(l10n.messageNotConnected);
return;
}
_emitOutput('[pm3] $command');
@@ -517,24 +529,23 @@ class Pm3Process {
/// 等待客户端执行完成标记;静默、表格边框和命令回显都不是结束信号。
/// 默认持续等待。显式超时会断开会话,防止后续命令混入未完成的输出。
- Future sendCommandAndWait(String command,
- {Duration? timeout}) async {
+ Future sendCommandAndWait(String command, {Duration? timeout}) async {
if (_process == null || _state != Pm3ProcessState.connected) {
- return '[未连接]';
+ return l10n.messageNotConnected;
}
if (_commandCompleter != null) {
- throw StateError('PM3 命令仍在执行,请等待完成或断开连接');
+ throw StateError(l10n.pm3Busy);
}
final process = _process!;
final completer = Completer();
_commandCompleter = completer;
- _commandMarker = 'PM3GUI_DONE_${DateTime.now().microsecondsSinceEpoch}_${++_commandSequence}';
+ _commandMarker =
+ 'PM3GUI_DONE_${DateTime.now().microsecondsSinceEpoch}_${++_commandSequence}';
_responseBuffer.clear();
// 在写 stdin 前注册错误处理,断开或写入失败也不会产生未处理 Future。
- final result = timeout == null
- ? completer.future
- : completer.future.timeout(timeout);
+ final result =
+ timeout == null ? completer.future : completer.future.timeout(timeout);
unawaited(result.then((_) {}, onError: (Object _) {}));
try {
_emitOutput('[pm3] $command');
@@ -542,7 +553,7 @@ class Pm3Process {
await process.writeLine('rem $_commandMarker');
return await result;
} on TimeoutException {
- _emitOutput('[错误] 命令超时,会话已停止');
+ _emitOutput(l10n.messageErrorCommandTimedOutTheSessionWas);
await disconnect();
rethrow;
} catch (_) {
@@ -558,7 +569,8 @@ class Pm3Process {
void _failCommand() {
final completer = _commandCompleter;
if (completer != null && !completer.isCompleted) {
- completer.completeError(StateError('PM3 会话在命令完成前断开'));
+ completer.completeError(
+ StateError(l10n.messageThePm3SessionDisconnectedBeforeTheCommand));
}
}
@@ -603,7 +615,8 @@ class Pm3Process {
try {
await _terminateUnownedOnce(await starting);
} catch (error) {
- _recordLifecycleError('等待 PM3 启动失败: $error');
+ _recordLifecycleError(
+ l10n.messageFailedWhileWaitingForPm3ToStart((error).toString()));
}
}
} finally {
@@ -647,7 +660,8 @@ class Pm3Process {
final startedProcess = await starting;
await _terminateUnownedOnce(startedProcess);
} catch (error) {
- _recordLifecycleError('等待 PM3 启动失败: $error');
+ _recordLifecycleError(
+ l10n.messageFailedWhileWaitingForPm3ToStart((error).toString()));
}
}
} finally {
@@ -676,7 +690,8 @@ class Pm3Process {
try {
await process.writeLine('quit');
} catch (error) {
- _recordLifecycleError('发送 quit 失败: $error');
+ _recordLifecycleError(
+ l10n.messageFailedToSendQuit((error).toString()));
}
final exited = await _waitForExit(process, _gracefulExitTimeout);
if (!exited) forceKill = true;
@@ -725,18 +740,21 @@ class Pm3Process {
const Duration(milliseconds: 100),
);
if (!identical(_process, process) || _disposed) return;
- _emitOutput('[PM3 进程退出, code=$code]');
- _lastError = 'PM3 进程在连接提示前退出 (code=$code)';
+ _emitOutput(l10n.messagePm3ProcessExitedCode((code).toString()));
+ _lastError = l10n
+ .messagePm3ProcessExitedBeforeTheConnectionPrompt((code).toString());
_failConnecting(completer, _lastError);
await _closeCurrentProcess(process, generation);
return;
} on TimeoutException {
// 输出管道孤立关闭,以下路径强制收口该进程。
} catch (error) {
- _recordLifecycleError('PM3 $streamName 流关闭后读取退出状态失败: $error');
+ _recordLifecycleError(l10n.messageCouldNotReadTheExitStatusAfter(
+ (streamName).toString(), (error).toString()));
}
if (!identical(_process, process) || _disposed) return;
- _failConnecting(completer, 'PM3 $streamName 流已关闭');
+ _failConnecting(
+ completer, l10n.messagePm3StreamClosed((streamName).toString()));
await _closeCurrentProcess(process, generation, forceKill: true);
}
@@ -747,7 +765,8 @@ class Pm3Process {
} on TimeoutException {
return false;
} catch (error) {
- _recordLifecycleError('等待 PM3 退出失败: $error');
+ _recordLifecycleError(
+ l10n.messageFailedWhileWaitingForPm3ToExit((error).toString()));
return false;
}
}
@@ -755,18 +774,18 @@ class Pm3Process {
Future _killAndWait(Pm3ProcessHandle process) async {
final terminated = await _tryTerminate(process, force: false);
if (!terminated) {
- _recordLifecycleError('终止 PM3 进程失败: TERM/整树终止未成功');
+ _recordLifecycleError(l10n.messageFailedToTerminatePm3TermProcessTree);
}
var exited = await _waitForExit(process, _killExitTimeout);
if (!exited) {
final forceTerminated = await _tryTerminate(process, force: true);
if (!forceTerminated) {
- _recordLifecycleError('强制终止 PM3 进程失败: KILL/整树终止未成功');
+ _recordLifecycleError(l10n.messageFailedToForceTerminatePm3KillProcess);
}
exited = await _waitForExit(process, _killExitTimeout);
}
if (!exited) {
- _recordLifecycleError('终止 PM3 后等待退出超时');
+ _recordLifecycleError(l10n.messageTimedOutWaitingForPm3ToExit);
}
}
@@ -778,12 +797,14 @@ class Pm3Process {
final terminated = await process.terminate(force: force);
if (!terminated) {
_recordLifecycleError(
- '终止 PM3 进程失败: ${force ? 'KILL' : 'TERM'} 返回 false',
+ l10n.messageFailedToTerminatePm3ReturnedFalse(
+ (force ? 'KILL' : 'TERM').toString()),
);
}
return terminated;
} catch (error) {
- _recordLifecycleError('终止 PM3 进程失败: $error');
+ _recordLifecycleError(
+ l10n.messageFailedToTerminatePm3((error).toString()));
return false;
}
}
@@ -818,7 +839,7 @@ class Pm3Process {
} else if (!_lastError.contains(message)) {
_lastError = '$_lastError;$message';
}
- _emitOutput('[错误] $message');
+ _emitOutput(l10n.messageError((message).toString()));
}
/// 检测致命错误,这意味着我们应该停止尝试
@@ -827,23 +848,20 @@ class Pm3Process {
if (lower.contains('claimed by another process') ||
lower.contains('is claimed by')) {
- _lastError = '串口 已被其他进程占用\n'
- '请关闭其他使用该端口的程序(如另一个 pm3 终端)';
+ _lastError = l10n.messageTheSerialPortIsInUseBy;
return true;
}
if (lower.contains('no such file or directory') &&
lower.contains('serial')) {
- _lastError = '串口设备不存在,请检查 PM3 是否已通过 USB 连接';
+ _lastError = l10n.messageSerialDeviceNotFoundCheckThePm3;
return true;
}
if (lower.contains('permission denied')) {
- _lastError = '串口权限不足\n'
- '请尝试: sudo chmod 666 /dev/ttyACM0\n'
- '或将用户加入 dialout 组';
+ _lastError = l10n.messagePermissionDeniedForTheSerialPortTry;
return true;
}
if (lower.contains('error') && lower.contains('serial port')) {
- _lastError = '串口连接错误: $line';
+ _lastError = l10n.messageSerialConnectionError((line).toString());
return true;
}
return false;
diff --git a/lib/core/pm3/pm3_controller.dart b/lib/core/pm3/pm3_controller.dart
index 900a367..481a1f2 100644
--- a/lib/core/pm3/pm3_controller.dart
+++ b/lib/core/pm3/pm3_controller.dart
@@ -5,12 +5,22 @@ import 'pm3_command.dart';
import 'pm3_connection.dart';
import 'pm3_event.dart';
import 'pm3_result.dart';
+import 'package:pm3gui/l10n/localized_messages.dart';
/// The only PM3 entry point exposed to application and feature layers.
-final class Pm3Controller {
+final class Pm3Controller with LocalizedMessages {
Pm3Controller(this._backend);
final Pm3Backend _backend;
+
+ @override
+ void updateLocalizations(AppLocalizations value) {
+ super.updateLocalizations(value);
+ if (_backend case final LocalizedMessages backend) {
+ backend.updateLocalizations(value);
+ }
+ }
+
final StreamController _commands =
StreamController.broadcast(sync: true);
Future? _shutdownFuture;
@@ -40,7 +50,7 @@ final class Pm3Controller {
Future disconnect() => _backend.disconnect();
Future execute(Pm3Command command, {Duration? timeout}) async {
- if (_isExecuting) throw StateError('PM3 命令仍在执行,请等待完成或断开连接');
+ if (_isExecuting) throw StateError(l10n.pm3Busy);
_isExecuting = true;
try {
_commands.add(command);
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
new file mode 100644
index 0000000..90f958c
--- /dev/null
+++ b/lib/l10n/app_en.arb
@@ -0,0 +1,3342 @@
+{
+ "@@locale": "en",
+ "appTitle": "PM3 GUI",
+ "@appTitle": {
+ "description": "Application title"
+ },
+ "navConnection": "Connection",
+ "@navConnection": {
+ "description": "Connection navigation entry"
+ },
+ "navTerminal": "Terminal",
+ "@navTerminal": {
+ "description": "Terminal navigation entry"
+ },
+ "navDumpViewer": "Dump viewer",
+ "@navDumpViewer": {
+ "description": "Dump viewer navigation entry"
+ },
+ "navDumpCompare": "Compare dumps",
+ "@navDumpCompare": {
+ "description": "Dump comparison navigation entry"
+ },
+ "navHfSniff": "HF sniff / tune",
+ "@navHfSniff": {
+ "description": "High-frequency sniffing and tuning navigation"
+ },
+ "navLfGeneral": "LF general / EM / T55",
+ "@navLfGeneral": {
+ "description": "Low-frequency general operations navigation"
+ },
+ "navData": "Data processing",
+ "@navData": {
+ "description": "Data processing navigation entry"
+ },
+ "navScripts": "Scripts",
+ "@navScripts": {
+ "description": "Scripts navigation entry"
+ },
+ "navSettings": "Settings",
+ "@navSettings": {
+ "description": "Settings page title and navigation entry"
+ },
+ "navCollapse": "Collapse menu",
+ "@navCollapse": {
+ "description": "Collapse navigation sidebar"
+ },
+ "navHighFrequency": "High frequency (HF)",
+ "@navHighFrequency": {
+ "description": "High-frequency navigation group"
+ },
+ "navLowFrequency": "Low frequency (LF)",
+ "@navLowFrequency": {
+ "description": "Low-frequency navigation group"
+ },
+ "navTools": "Tools",
+ "@navTools": {
+ "description": "Tools navigation group"
+ },
+ "connected": "Connected",
+ "@connected": {
+ "description": "Device connection status"
+ },
+ "disconnected": "Disconnected",
+ "@disconnected": {
+ "description": "Device disconnected status"
+ },
+ "pm3NotConnected": "PM3 is not connected",
+ "@pm3NotConnected": {
+ "description": "Command rejected because no device is connected"
+ },
+ "pm3Busy": "A PM3 command is still running. Wait for it to finish or disconnect.",
+ "@pm3Busy": {
+ "description": "Command rejected because another command is running"
+ },
+ "hexDigits": "{count} hex digits",
+ "@hexDigits": {
+ "description": "Hex input length hint",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "maxCharacters": "Maximum {count} characters",
+ "@maxCharacters": {
+ "description": "Input character limit",
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "executingCommand": "Execute: {command}",
+ "@executingCommand": {
+ "description": "Executed command heading",
+ "placeholders": {
+ "command": {
+ "type": "String"
+ }
+ }
+ },
+ "executeToViewResults": "Run a command to view results",
+ "@executeToViewResults": {
+ "description": "Empty command result panel"
+ },
+ "copiedToClipboard": "Copied to clipboard",
+ "@copiedToClipboard": {
+ "description": "Copy confirmation"
+ },
+ "copy": "Copy",
+ "@copy": {
+ "description": "Copy result action"
+ },
+ "clear": "Clear",
+ "@clear": {
+ "description": "Clear result action"
+ },
+ "language": "Language",
+ "@language": {
+ "description": "Display language setting"
+ },
+ "languageSystem": "System default",
+ "@languageSystem": {
+ "description": "Follow the operating system language"
+ },
+ "languageHelp": "Changes apply immediately. Unsupported system languages use English.",
+ "@languageHelp": {
+ "description": "Language preference help"
+ },
+ "languagePreferenceError": "Could not load or save the language preference: {error}",
+ "@languagePreferenceError": {
+ "description": "Visible language preference persistence error",
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "settingsPm3Configuration": "PM3 configuration",
+ "@settingsPm3Configuration": {
+ "description": "Client configuration section"
+ },
+ "settingsPm3Path": "PM3 program path",
+ "@settingsPm3Path": {
+ "description": "Client executable path field"
+ },
+ "settingsWindowsPathHint": "Official distribution root\\pm3.bat",
+ "@settingsWindowsPathHint": {
+ "description": "Windows client path example"
+ },
+ "settingsWindowsPathHelp": "On Windows, select pm3.bat in the official distribution root.",
+ "@settingsWindowsPathHelp": {
+ "description": "Windows launcher guidance"
+ },
+ "settingsPickPm3": "Select PM3 program",
+ "@settingsPickPm3": {
+ "description": "Browse for client executable"
+ },
+ "settingsArguments": "PM3 launch arguments",
+ "@settingsArguments": {
+ "description": "Client argument field"
+ },
+ "settingsWindowsArgumentsHelp": "pm3.bat initializes its environment and detects devices itself; these arguments are not passed to the script. One argument per line; spaces, quotes and backslashes are preserved.",
+ "@settingsWindowsArgumentsHelp": {
+ "description": "Windows launcher argument behavior"
+ },
+ "settingsArgumentsHelp": "One argument per line; spaces, quotes and backslashes are preserved.",
+ "@settingsArgumentsHelp": {
+ "description": "Native executable argument behavior"
+ },
+ "settingsWorkingDirectory": "PM3 working directory (optional)",
+ "@settingsWorkingDirectory": {
+ "description": "Optional client working directory field"
+ },
+ "settingsSaving": "Saving settings…",
+ "@settingsSaving": {
+ "description": "Client settings save progress"
+ },
+ "settingsAbout": "About PM3 GUI",
+ "@settingsAbout": {
+ "description": "About section title"
+ },
+ "settingsDescription": "A Flutter-based Proxmark3 graphical interface",
+ "@settingsDescription": {
+ "description": "Application description"
+ },
+ "settingsPlatform": "Platform",
+ "@settingsPlatform": {
+ "description": "Operating system field"
+ },
+ "settingsArchitecture": "Architecture",
+ "@settingsArchitecture": {
+ "description": "Software architecture field"
+ },
+ "settingsArchitectureDescription": "CLI wrapper (compatible with future updates)",
+ "@settingsArchitectureDescription": {
+ "description": "Software architecture description"
+ },
+ "settingsSupportedFormats": "Supported formats",
+ "@settingsSupportedFormats": {
+ "description": "Supported file formats label"
+ },
+ "settingsMaintenance": "Maintenance",
+ "@settingsMaintenance": {
+ "description": "Maintenance section title"
+ },
+ "terminalClear": "Clear terminal",
+ "@terminalClear": {
+ "description": "Clear terminal output action"
+ },
+ "hardwareVersion": "Hardware version",
+ "@hardwareVersion": {
+ "description": "Read hardware version action"
+ },
+ "antennaTune": "Antenna tuning",
+ "@antennaTune": {
+ "description": "Antenna tuning action"
+ },
+ "terminalHfSearch": "HF search",
+ "@terminalHfSearch": {
+ "description": "High-frequency search quick command"
+ },
+ "terminalLfSearch": "LF search",
+ "@terminalLfSearch": {
+ "description": "Low-frequency search quick command"
+ },
+ "terminal14aInfo": "14A information",
+ "@terminal14aInfo": {
+ "description": "ISO 14443-A information quick command"
+ },
+ "terminalLfRead": "LF read",
+ "@terminalLfRead": {
+ "description": "Low-frequency read quick command"
+ },
+ "terminalAutopwn": "Automatic key recovery",
+ "@terminalAutopwn": {
+ "description": "MIFARE automatic key recovery quick command"
+ },
+ "terminalHint": "Hint: {hint}",
+ "@terminalHint": {
+ "description": "Command hint caption",
+ "placeholders": {
+ "hint": {
+ "type": "String"
+ }
+ }
+ },
+ "terminalInputHint": "Enter a command… (Tab / Shift+Tab cycles completions)",
+ "@terminalInputHint": {
+ "description": "Terminal input placeholder"
+ },
+ "terminalCatalog": "Command library",
+ "@terminalCatalog": {
+ "description": "Open the command library"
+ },
+ "terminalSend": "Send",
+ "@terminalSend": {
+ "description": "Send terminal command"
+ },
+ "terminalSearchHint": "Search commands (e.g. hf mf, autopwn, info)",
+ "@terminalSearchHint": {
+ "description": "Command library search placeholder"
+ },
+ "terminalCatalogError": "Could not load the command library: {error}",
+ "@terminalCatalogError": {
+ "description": "Command catalog loading failure",
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "messageFailedToTerminateTheProcessTreeExit": "Failed to terminate the process tree (exit={value}){value2}",
+ "@messageFailedToTerminateTheProcessTreeExit": {
+ "description": "pm3_process: Failed to terminate the process tree (exit={value}){value2}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageThePm3ProcessManagerHasBeenDisposed": "The PM3 process manager has been disposed",
+ "@messageThePm3ProcessManagerHasBeenDisposed": {
+ "description": "pm3_process: The PM3 process manager has been disposed"
+ },
+ "messagePleaseWaitSecondsBeforeRetrying": "Please wait {value} seconds before retrying",
+ "@messagePleaseWaitSecondsBeforeRetrying": {
+ "description": "pm3_process: Please wait {value} seconds before retrying",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageCoolingDownS": "[Cooling down: {value}s]",
+ "@messageCoolingDownS": {
+ "description": "pm3_process: [Cooling down: {value}s]",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messagePm3ProgramNotFoundOnWindowsSelect": "PM3 program not found: {value}\nOn Windows, select pm3.bat in the official distribution root; on Linux, select proxmark3.",
+ "@messagePm3ProgramNotFoundOnWindowsSelect": {
+ "description": "pm3_process: PM3 program not found: {value}\nOn Windows, select pm3.bat in the official distribution root; on Linux, select proxmark3.",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageError": "[Error] {value}",
+ "@messageError": {
+ "description": "pm3_process: [Error] {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageUsingPm3": "[Using PM3: {value}]",
+ "@messageUsingPm3": {
+ "description": "pm3_process: [Using PM3: {value}]",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageWorkingDirectory": "[Working directory: {value}]",
+ "@messageWorkingDirectory": {
+ "description": "pm3_process: [Working directory: {value}]",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageFatalErrorOnPm3Stdout": "Fatal error on PM3 stdout",
+ "@messageFatalErrorOnPm3Stdout": {
+ "description": "pm3_process: Fatal error on PM3 stdout"
+ },
+ "messageFailedToSendTheReadOnlyPm3": "Failed to send the read-only PM3 BAT handshake: {value}",
+ "@messageFailedToSendTheReadOnlyPm3": {
+ "description": "pm3_process: Failed to send the read-only PM3 BAT handshake: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messagePm3StdoutStreamError": "PM3 stdout stream error: {value}",
+ "@messagePm3StdoutStreamError": {
+ "description": "pm3_process: PM3 stdout stream error: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageFatalErrorOnPm3Stderr": "Fatal error on PM3 stderr",
+ "@messageFatalErrorOnPm3Stderr": {
+ "description": "pm3_process: Fatal error on PM3 stderr"
+ },
+ "messagePm3StderrStreamError": "PM3 stderr stream error: {value}",
+ "@messagePm3StderrStreamError": {
+ "description": "pm3_process: PM3 stderr stream error: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messagePm3ProcessExitedCode": "[PM3 process exited, code={value}]",
+ "@messagePm3ProcessExitedCode": {
+ "description": "pm3_process: [PM3 process exited, code={value}]",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messagePm3ProcessExitedBeforeTheConnectionPrompt": "PM3 process exited before the connection prompt (code={value})",
+ "@messagePm3ProcessExitedBeforeTheConnectionPrompt": {
+ "description": "pm3_process: PM3 process exited before the connection prompt (code={value})",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messagePm3ProcessExitStatusError": "PM3 process exit status error: {value}",
+ "@messagePm3ProcessExitStatusError": {
+ "description": "pm3_process: PM3 process exit status error: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageConnectionTimedOutSCheckThatThe": "Connection timed out ({value}s). Check that the device is connected.",
+ "@messageConnectionTimedOutSCheckThatThe": {
+ "description": "pm3_process: Connection timed out ({value}s). Check that the device is connected.",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageConnectionTimedOut": "[Connection timed out]",
+ "@messageConnectionTimedOut": {
+ "description": "pm3_process: [Connection timed out]"
+ },
+ "messageCouldNotStartPm3PathCheckThe": "Could not start PM3: {value}\nPath: {value2}\nCheck the program path and that the client has been built.",
+ "@messageCouldNotStartPm3PathCheckThe": {
+ "description": "pm3_process: Could not start PM3: {value}\nPath: {value2}\nCheck the program path and that the client has been built.",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageConnectionFailed": "Connection failed: {value}",
+ "@messageConnectionFailed": {
+ "description": "pm3_process: Connection failed: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageNotConnected": "[Not connected]",
+ "@messageNotConnected": {
+ "description": "pm3_process: [Not connected]"
+ },
+ "messageErrorCommandTimedOutTheSessionWas": "[Error] Command timed out; the session was stopped",
+ "@messageErrorCommandTimedOutTheSessionWas": {
+ "description": "pm3_process: [Error] Command timed out; the session was stopped"
+ },
+ "messageThePm3SessionDisconnectedBeforeTheCommand": "The PM3 session disconnected before the command completed",
+ "@messageThePm3SessionDisconnectedBeforeTheCommand": {
+ "description": "pm3_process: The PM3 session disconnected before the command completed"
+ },
+ "messageFailedWhileWaitingForPm3ToStart": "Failed while waiting for PM3 to start: {value}",
+ "@messageFailedWhileWaitingForPm3ToStart": {
+ "description": "pm3_process: Failed while waiting for PM3 to start: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageFailedToSendQuit": "Failed to send quit: {value}",
+ "@messageFailedToSendQuit": {
+ "description": "pm3_process: Failed to send quit: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageCouldNotReadTheExitStatusAfter": "Could not read the exit status after PM3 {value} closed: {value2}",
+ "@messageCouldNotReadTheExitStatusAfter": {
+ "description": "pm3_process: Could not read the exit status after PM3 {value} closed: {value2}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messagePm3StreamClosed": "PM3 {value} stream closed",
+ "@messagePm3StreamClosed": {
+ "description": "pm3_process: PM3 {value} stream closed",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageFailedWhileWaitingForPm3ToExit": "Failed while waiting for PM3 to exit: {value}",
+ "@messageFailedWhileWaitingForPm3ToExit": {
+ "description": "pm3_process: Failed while waiting for PM3 to exit: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageFailedToTerminatePm3TermProcessTree": "Failed to terminate PM3: TERM / process-tree termination failed",
+ "@messageFailedToTerminatePm3TermProcessTree": {
+ "description": "pm3_process: Failed to terminate PM3: TERM / process-tree termination failed"
+ },
+ "messageFailedToForceTerminatePm3KillProcess": "Failed to force-terminate PM3: KILL / process-tree termination failed",
+ "@messageFailedToForceTerminatePm3KillProcess": {
+ "description": "pm3_process: Failed to force-terminate PM3: KILL / process-tree termination failed"
+ },
+ "messageTimedOutWaitingForPm3ToExit": "Timed out waiting for PM3 to exit after termination",
+ "@messageTimedOutWaitingForPm3ToExit": {
+ "description": "pm3_process: Timed out waiting for PM3 to exit after termination"
+ },
+ "messageFailedToTerminatePm3ReturnedFalse": "Failed to terminate PM3: {value} returned false",
+ "@messageFailedToTerminatePm3ReturnedFalse": {
+ "description": "pm3_process: Failed to terminate PM3: {value} returned false",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageFailedToTerminatePm3": "Failed to terminate PM3: {value}",
+ "@messageFailedToTerminatePm3": {
+ "description": "pm3_process: Failed to terminate PM3: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageTheSerialPortIsInUseBy": "The serial port is in use by another process.\nClose other programs using this port, such as another PM3 terminal.",
+ "@messageTheSerialPortIsInUseBy": {
+ "description": "pm3_process: The serial port is in use by another process.\nClose other programs using this port, such as another PM3 terminal."
+ },
+ "messageSerialDeviceNotFoundCheckThePm3": "Serial device not found. Check the PM3 USB connection.",
+ "@messageSerialDeviceNotFoundCheckThePm3": {
+ "description": "pm3_process: Serial device not found. Check the PM3 USB connection."
+ },
+ "messagePermissionDeniedForTheSerialPortTry": "Permission denied for the serial port.\nTry: sudo chmod 666 /dev/ttyACM0\nOr add your user to the dialout group.",
+ "@messagePermissionDeniedForTheSerialPortTry": {
+ "description": "pm3_process: Permission denied for the serial port.\nTry: sudo chmod 666 /dev/ttyACM0\nOr add your user to the dialout group."
+ },
+ "messageSerialConnectionError": "Serial connection error: {value}",
+ "@messageSerialConnectionError": {
+ "description": "pm3_process: Serial connection error: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageEnterHexDigitsUseForUnknownDigits": "Enter {value} hex digits; use ? for unknown digits or clear the field for unread data.",
+ "@messageEnterHexDigitsUseForUnknownDigits": {
+ "description": "command_card: Enter {value} hex digits; use ? for unknown digits or clear the field for unread data.",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageBlockNumberExceedsTheCardCapacity": "Block number exceeds the card capacity",
+ "@messageBlockNumberExceedsTheCardCapacity": {
+ "description": "command_card: Block number exceeds the card capacity"
+ },
+ "messageSectorNumberExceedsTheCardCapacity": "Sector number exceeds the card capacity",
+ "@messageSectorNumberExceedsTheCardCapacity": {
+ "description": "command_card: Sector number exceeds the card capacity"
+ },
+ "messageThisIsNotACommandDataDraft": "This is not a command data draft",
+ "@messageThisIsNotACommandDataDraft": {
+ "description": "command_card: This is not a command data draft"
+ },
+ "messageUnsupportedCardCapacity": "Unsupported card capacity",
+ "@messageUnsupportedCardCapacity": {
+ "description": "command_card: Unsupported card capacity"
+ },
+ "messageInvalidUidFormat": "Invalid UID format",
+ "@messageInvalidUidFormat": {
+ "description": "command_card: Invalid UID format"
+ },
+ "messageBlockIsIncompleteSaveADraftOr": "Block {value} is incomplete. Save a draft or complete the data first.",
+ "@messageBlockIsIncompleteSaveADraftOr": {
+ "description": "command_card: Block {value} is incomplete. Save a draft or complete the data first.",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageSectorHasIncompleteKeysExportADictionary": "Sector {value} has incomplete keys. Export a dictionary or save a draft first.",
+ "@messageSectorHasIncompleteKeysExportADictionary": {
+ "description": "command_card: Sector {value} has incomplete keys. Export a dictionary or save a draft first.",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageThereAreNoCompleteKeysToExport": "There are no complete keys to export",
+ "@messageThereAreNoCompleteKeysToExport": {
+ "description": "command_card: There are no complete keys to export"
+ },
+ "messageEmptyUnallocated": "Empty / unallocated",
+ "@messageEmptyUnallocated": {
+ "description": "dump_analysis: Empty / unallocated"
+ },
+ "messageDefectManagement": "Defect management",
+ "@messageDefectManagement": {
+ "description": "dump_analysis: Defect management"
+ },
+ "messageReserved": "Reserved",
+ "@messageReserved": {
+ "description": "dump_analysis: Reserved"
+ },
+ "messageAdditionalDirectoryEntries": "Additional directory entries",
+ "@messageAdditionalDirectoryEntries": {
+ "description": "dump_analysis: Additional directory entries"
+ },
+ "messageCardholderInformation": "Cardholder information",
+ "@messageCardholderInformation": {
+ "description": "dump_analysis: Cardholder information"
+ },
+ "messageUnallocated": "Unallocated",
+ "@messageUnallocated": {
+ "description": "dump_analysis: Unallocated"
+ },
+ "messageAirlineServices": "Airline services",
+ "@messageAirlineServices": {
+ "description": "dump_analysis: Airline services"
+ },
+ "messageUrbanTransport": "Urban transport",
+ "@messageUrbanTransport": {
+ "description": "dump_analysis: Urban transport"
+ },
+ "messageUrbanTransport2": "Urban transport (2)",
+ "@messageUrbanTransport2": {
+ "description": "dump_analysis: Urban transport (2)"
+ },
+ "messageRailTransport": "Rail transport",
+ "@messageRailTransport": {
+ "description": "dump_analysis: Rail transport"
+ },
+ "messageTransportGeneral": "Transport (general)",
+ "@messageTransportGeneral": {
+ "description": "dump_analysis: Transport (general)"
+ },
+ "messageSecurityServices": "Security services",
+ "@messageSecurityServices": {
+ "description": "dump_analysis: Security services"
+ },
+ "messageUrbanTransport3": "Urban transport (3)",
+ "@messageUrbanTransport3": {
+ "description": "dump_analysis: Urban transport (3)"
+ },
+ "messageEventTicketing": "Event ticketing",
+ "@messageEventTicketing": {
+ "description": "dump_analysis: Event ticketing"
+ },
+ "messageLoyaltySchemes": "Loyalty schemes",
+ "@messageLoyaltySchemes": {
+ "description": "dump_analysis: Loyalty schemes"
+ },
+ "messageParking": "Parking",
+ "@messageParking": {
+ "description": "dump_analysis: Parking"
+ },
+ "messageAccessControl": "Access control",
+ "@messageAccessControl": {
+ "description": "dump_analysis: Access control"
+ },
+ "messageElectronicPurseMicropayments": "Electronic purse / micropayments",
+ "@messageElectronicPurseMicropayments": {
+ "description": "dump_analysis: Electronic purse / micropayments"
+ },
+ "messageElectronicTicketingGeneral": "Electronic ticketing (general)",
+ "@messageElectronicTicketingGeneral": {
+ "description": "dump_analysis: Electronic ticketing (general)"
+ },
+ "messageMobileOffice": "Mobile office",
+ "@messageMobileOffice": {
+ "description": "dump_analysis: Mobile office"
+ },
+ "messageIdentification": "Identification",
+ "@messageIdentification": {
+ "description": "dump_analysis: Identification"
+ },
+ "messageMobilePayments": "Mobile payments",
+ "@messageMobilePayments": {
+ "description": "dump_analysis: Mobile payments"
+ },
+ "messageFelicaIntegration": "FeliCa integration",
+ "@messageFelicaIntegration": {
+ "description": "dump_analysis: FeliCa integration"
+ },
+ "messageFuelServices": "Fuel services",
+ "@messageFuelServices": {
+ "description": "dump_analysis: Fuel services"
+ },
+ "messageInformationProcessing": "Information processing",
+ "@messageInformationProcessing": {
+ "description": "dump_analysis: Information processing"
+ },
+ "messageMifareClassic1kCompatible": "MIFARE Classic 1K (compatible)",
+ "@messageMifareClassic1kCompatible": {
+ "description": "dump_analysis: MIFARE Classic 1K (compatible)"
+ },
+ "messageMifareClassic1kCompatible2": "MIFARE Classic 1K (compatible 2)",
+ "@messageMifareClassic1kCompatible2": {
+ "description": "dump_analysis: MIFARE Classic 1K (compatible 2)"
+ },
+ "messageUnknownSak": "Unknown (SAK={value})",
+ "@messageUnknownSak": {
+ "description": "dump_analysis: Unknown (SAK={value})",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageUnknown0x": "Unknown (0x{value})",
+ "@messageUnknown0x": {
+ "description": "dump_analysis: Unknown (0x{value})",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageApplication0x": "Application 0x{value}",
+ "@messageApplication0x": {
+ "description": "dump_analysis: Application 0x{value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageNxpFactoryDefault": "NXP factory default",
+ "@messageNxpFactoryDefault": {
+ "description": "dump_analysis: NXP factory default"
+ },
+ "messageAllZeros": "All zeros",
+ "@messageAllZeros": {
+ "description": "dump_analysis: All zeros"
+ },
+ "messageInvalidKeyTextFormat": "Invalid key text format",
+ "@messageInvalidKeyTextFormat": {
+ "description": "key_parser: Invalid key text format"
+ },
+ "messageDuplicateSector": "Duplicate sector {value}",
+ "@messageDuplicateSector": {
+ "description": "key_parser: Duplicate sector {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageKeyTextIsMissingSectors": "Key text is missing sectors",
+ "@messageKeyTextIsMissingSectors": {
+ "description": "key_parser: Key text is missing sectors"
+ },
+ "messageBinaryDumpBin": "Binary dump (.bin)",
+ "@messageBinaryDumpBin": {
+ "description": "dump_converter: Binary dump (.bin)"
+ },
+ "messageTextDumpEml": "Text dump (.eml)",
+ "@messageTextDumpEml": {
+ "description": "dump_converter: Text dump (.eml)"
+ },
+ "messageBinaryKeysKeyBin": "Binary keys (.key.bin)",
+ "@messageBinaryKeysKeyBin": {
+ "description": "dump_converter: Binary keys (.key.bin)"
+ },
+ "messageKeyDictionaryDic": "Key dictionary (.dic)",
+ "@messageKeyDictionaryDic": {
+ "description": "dump_converter: Key dictionary (.dic)"
+ },
+ "messageKeyListKeysTxt": "Key list (.keys.txt)",
+ "@messageKeyListKeysTxt": {
+ "description": "dump_converter: Key list (.keys.txt)"
+ },
+ "messageFileNotFound": "File not found: {value}",
+ "@messageFileNotFound": {
+ "description": "dump_converter: File not found: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageAKeyFileCannotBeConvertedTo": "A key file cannot be converted to a full dump (data blocks are missing)",
+ "@messageAKeyFileCannotBeConvertedTo": {
+ "description": "dump_converter: A key file cannot be converted to a full dump (data blocks are missing)"
+ },
+ "messageAKeyFileCannotBeConvertedTo2": "A key file cannot be converted to EML (data blocks are missing)",
+ "@messageAKeyFileCannotBeConvertedTo2": {
+ "description": "dump_converter: A key file cannot be converted to EML (data blocks are missing)"
+ },
+ "messageAKeyFileCannotBeConvertedTo3": "A key file cannot be converted to JSON (data blocks are missing)",
+ "@messageAKeyFileCannotBeConvertedTo3": {
+ "description": "dump_converter: A key file cannot be converted to JSON (data blocks are missing)"
+ },
+ "messageNoKeyData": "No key data",
+ "@messageNoKeyData": {
+ "description": "dump_converter: No key data"
+ },
+ "messageConversionFailed": "Conversion failed: {value}",
+ "@messageConversionFailed": {
+ "description": "dump_converter: Conversion failed: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageUniqueKeys": "Unique keys: {value}",
+ "@messageUniqueKeys": {
+ "description": "dump_converter: Unique keys: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageUnsupportedKeyExportFormat": "Unsupported key export format: {value}",
+ "@messageUnsupportedKeyExportFormat": {
+ "description": "dump_converter: Unsupported key export format: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageSectorsUniqueKeyAUniqueKeyB": "Sectors: {value}; unique Key A: {value2}; unique Key B: {value3}; total unique keys: {value4}",
+ "@messageSectorsUniqueKeyAUniqueKeyB": {
+ "description": "dump_converter: Sectors: {value}; unique Key A: {value2}; unique Key B: {value3}; total unique keys: {value4}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ },
+ "value3": {
+ "type": "String"
+ },
+ "value4": {
+ "type": "String"
+ }
+ }
+ },
+ "messageConnectionLostOperationAborted": "Connection lost; operation aborted",
+ "@messageConnectionLostOperationAborted": {
+ "description": "app_state: Connection lost; operation aborted"
+ },
+ "messageWritingBlock": "Writing block {value}…",
+ "@messageWritingBlock": {
+ "description": "app_state: Writing block {value}…",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageAuthenticationOrWritingFailed": "Authentication or writing failed",
+ "@messageAuthenticationOrWritingFailed": {
+ "description": "app_state: Authentication or writing failed"
+ },
+ "messageWriteSuccessful": "Write successful",
+ "@messageWriteSuccessful": {
+ "description": "app_state: Write successful"
+ },
+ "messageFinishedSucceededFailed": "Finished: {value} succeeded, {value2} failed",
+ "@messageFinishedSucceededFailed": {
+ "description": "app_state: Finished: {value} succeeded, {value2} failed",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageCancelled": "Cancelled",
+ "@messageCancelled": {
+ "description": "app_state: Cancelled"
+ },
+ "messageCouldNotSaveSettings": "Could not save settings: {value}",
+ "@messageCouldNotSaveSettings": {
+ "description": "connection_state: Could not save settings: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageFileScanFailed": "File scan failed: {value}",
+ "@messageFileScanFailed": {
+ "description": "file_state: File scan failed: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageInstalled": "Installed",
+ "@messageInstalled": {
+ "description": "hardware_state: Installed"
+ },
+ "messageOnWindowsUseTheOfficialPm3Bat": "On Windows, use the official pm3.bat launcher.",
+ "@messageOnWindowsUseTheOfficialPm3Bat": {
+ "description": "connection_page: On Windows, use the official pm3.bat launcher."
+ },
+ "messageSerialPort": "Serial port",
+ "@messageSerialPort": {
+ "description": "connection_page: Serial port"
+ },
+ "messageRefreshPorts": "Refresh ports",
+ "@messageRefreshPorts": {
+ "description": "connection_page: Refresh ports"
+ },
+ "messageNoSerialPortFoundConnectYourPm3": "No serial port found. Connect your PM3 device.",
+ "@messageNoSerialPortFoundConnectYourPm3": {
+ "description": "connection_page: No serial port found. Connect your PM3 device."
+ },
+ "messageTheOfficialBatLauncherDetectsDevicesAutomatically": "The official BAT launcher detects devices automatically; this port is only saved in settings.",
+ "@messageTheOfficialBatLauncherDetectsDevicesAutomatically": {
+ "description": "connection_page: The official BAT launcher detects devices automatically; this port is only saved in settings."
+ },
+ "messageConnecting": "Connecting…",
+ "@messageConnecting": {
+ "description": "connection_page: Connecting…"
+ },
+ "messageDisconnect": "Disconnect",
+ "@messageDisconnect": {
+ "description": "connection_page: Disconnect"
+ },
+ "messageConnectPm3": "Connect PM3",
+ "@messageConnectPm3": {
+ "description": "connection_page: Connect PM3"
+ },
+ "messageDeviceInformation": "Device information",
+ "@messageDeviceInformation": {
+ "description": "connection_page: Device information"
+ },
+ "messageRefreshHardwareInformation": "Refresh hardware information",
+ "@messageRefreshHardwareInformation": {
+ "description": "connection_page: Refresh hardware information"
+ },
+ "messagePort": "Port",
+ "@messagePort": {
+ "description": "connection_page: Port"
+ },
+ "messageVersion": "Version",
+ "@messageVersion": {
+ "description": "connection_page: Version"
+ },
+ "messageStatus": "Status",
+ "@messageStatus": {
+ "description": "connection_page: Status"
+ },
+ "messageDeviceModel": "Device model",
+ "@messageDeviceModel": {
+ "description": "connection_page: Device model"
+ },
+ "messageFirmware": "Firmware",
+ "@messageFirmware": {
+ "description": "connection_page: Firmware"
+ },
+ "messageSmartcardModule": "Smartcard module",
+ "@messageSmartcardModule": {
+ "description": "connection_page: Smartcard module"
+ },
+ "messageDeviceId": "Device ID",
+ "@messageDeviceId": {
+ "description": "connection_page: Device ID"
+ },
+ "messageRetrievingHardwareInformation": "Retrieving hardware information…",
+ "@messageRetrievingHardwareInformation": {
+ "description": "connection_page: Retrieving hardware information…"
+ },
+ "messageCommandHistory": "Command history",
+ "@messageCommandHistory": {
+ "description": "connection_page: Command history"
+ },
+ "messageCommands": "{value, plural, =0{No commands} =1{1 command} other{{value} commands}}",
+ "@messageCommands": {
+ "description": "Number of commands in terminal history; follows locale plural rules.",
+ "placeholders": {
+ "value": {
+ "type": "int"
+ }
+ }
+ },
+ "messageTerminalBuffer": "Terminal buffer",
+ "@messageTerminalBuffer": {
+ "description": "connection_page: Terminal buffer"
+ },
+ "messageLines": "Lines: {value}",
+ "@messageLines": {
+ "description": "connection_page: Lines: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageHardwareStatus": "Hardware status",
+ "@messageHardwareStatus": {
+ "description": "connection_page: Hardware status"
+ },
+ "messageDebugLevel": "Debug level",
+ "@messageDebugLevel": {
+ "description": "connection_page: Debug level"
+ },
+ "messageEnvironment": "Environment",
+ "@messageEnvironment": {
+ "description": "connection_page: Environment"
+ },
+ "messagePm3Path": "PM3 path",
+ "@messagePm3Path": {
+ "description": "connection_page: PM3 path"
+ },
+ "messageValid": "✅ Valid",
+ "@messageValid": {
+ "description": "connection_page: ✅ Valid"
+ },
+ "messageInvalid": "❌ Invalid",
+ "@messageInvalid": {
+ "description": "connection_page: ❌ Invalid"
+ },
+ "messagePm3FileCollection": "PM3 file collection",
+ "@messagePm3FileCollection": {
+ "description": "connection_page: PM3 file collection"
+ },
+ "messageFilesCards": "Files: {value}; cards: {value2}",
+ "@messageFilesCards": {
+ "description": "connection_page: Files: {value}; cards: {value2}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageScan": "Scan",
+ "@messageScan": {
+ "description": "connection_page: Scan"
+ },
+ "messageOrganizeFiles": "Organize files",
+ "@messageOrganizeFiles": {
+ "description": "connection_page: Organize files"
+ },
+ "messageNoPm3ExportFilesFound": "No PM3 export files found",
+ "@messageNoPm3ExportFilesFound": {
+ "description": "connection_page: No PM3 export files found"
+ },
+ "messageFilesAreCollectedAutomaticallyAfterConnectingAnd": "Files are collected automatically after connecting and running dump / autopwn.",
+ "@messageFilesAreCollectedAutomaticallyAfterConnectingAnd": {
+ "description": "connection_page: Files are collected automatically after connecting and running dump / autopwn."
+ },
+ "messagePm3Connected": "PM3 connected",
+ "@messagePm3Connected": {
+ "description": "connection_page: PM3 connected"
+ },
+ "messagePm3NotConnected": "PM3 not connected",
+ "@messagePm3NotConnected": {
+ "description": "connection_page: PM3 not connected"
+ },
+ "messageSelectASerialPortAndConnectThe": "Select a serial port and connect the device",
+ "@messageSelectASerialPortAndConnectThe": {
+ "description": "connection_page: Select a serial port and connect the device"
+ },
+ "messageConnectionFailed2": "Connection failed",
+ "@messageConnectionFailed2": {
+ "description": "connection_page: Connection failed"
+ },
+ "messageDumpsKeyFilesFiles": "Dumps: {value} • Key files: {value2} • Files: {value3}",
+ "@messageDumpsKeyFilesFiles": {
+ "description": "connection_page: Dumps: {value} • Key files: {value2} • Files: {value3}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ },
+ "value3": {
+ "type": "String"
+ }
+ }
+ },
+ "messageOpenInDumpViewer": "Open in dump viewer",
+ "@messageOpenInDumpViewer": {
+ "description": "connection_page: Open in dump viewer"
+ },
+ "messageOpenedInDumpViewer": "Opened in dump viewer: {value}",
+ "@messageOpenedInDumpViewer": {
+ "description": "connection_page: Opened in dump viewer: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageOrganizePm3Files": "Organize PM3 files",
+ "@messageOrganizePm3Files": {
+ "description": "connection_page: Organize PM3 files"
+ },
+ "messageOrganizeCollectedPm3ExportFilesIntoSubdirectories": "Organize collected PM3 export files into subdirectories by card UID.\n\nDirectory structure:\n 📁 hf-mf/A991A280/\n ├── hf-mf-A991A280-dump.bin\n ├── hf-mf-A991A280-key.bin\n └── …\n",
+ "@messageOrganizeCollectedPm3ExportFilesIntoSubdirectories": {
+ "description": "connection_page: Organize collected PM3 export files into subdirectories by card UID.\n\nDirectory structure:\n 📁 hf-mf/A991A280/\n ├── hf-mf-A991A280-dump.bin\n ├── hf-mf-A991A280-key.bin\n └── …\n"
+ },
+ "messageDestinationDirectory": "Destination directory",
+ "@messageDestinationDirectory": {
+ "description": "connection_page: Destination directory"
+ },
+ "messageFilesToOrganize": "Files to organize: {value}",
+ "@messageFilesToOrganize": {
+ "description": "connection_page: Files to organize: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageCancel": "Cancel",
+ "@messageCancel": {
+ "description": "connection_page: Cancel"
+ },
+ "messageFilesOrganized": "Files organized: {value}",
+ "@messageFilesOrganized": {
+ "description": "connection_page: Files organized: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageStartOrganizing": "Start organizing",
+ "@messageStartOrganizing": {
+ "description": "connection_page: Start organizing"
+ },
+ "messageFlashUsage": "Flash usage",
+ "@messageFlashUsage": {
+ "description": "connection_page: Flash usage"
+ },
+ "messageBasic": "Basic",
+ "@messageBasic": {
+ "description": "data_page: Basic"
+ },
+ "messageAnalysis": "Analysis",
+ "@messageAnalysis": {
+ "description": "data_page: Analysis"
+ },
+ "messageCompare": "Compare",
+ "@messageCompare": {
+ "description": "data_page: Compare"
+ },
+ "messagePlot": "Plot",
+ "@messagePlot": {
+ "description": "data_page: Plot"
+ },
+ "messageDisplayTheDataGraph": "Display the data graph",
+ "@messageDisplayTheDataGraph": {
+ "description": "data_page: Display the data graph"
+ },
+ "messageClear": "Clear",
+ "@messageClear": {
+ "description": "data_page: Clear"
+ },
+ "messageClearTheGraphBuffer": "Clear the graph buffer",
+ "@messageClearTheGraphBuffer": {
+ "description": "data_page: Clear the graph buffer"
+ },
+ "messageDetectClock": "Detect clock",
+ "@messageDetectClock": {
+ "description": "data_page: Detect clock"
+ },
+ "messageAutomaticallyDetectTheClockRate": "Automatically detect the clock rate",
+ "@messageAutomaticallyDetectTheClockRate": {
+ "description": "data_page: Automatically detect the clock rate"
+ },
+ "messageSampling": "Sampling",
+ "@messageSampling": {
+ "description": "data_page: Sampling"
+ },
+ "messageSampleCount": "Sample count",
+ "@messageSampleCount": {
+ "description": "data_page: Sample count"
+ },
+ "messageReadSamples": "Read samples",
+ "@messageReadSamples": {
+ "description": "data_page: Read samples"
+ },
+ "messageFileOperations": "File operations",
+ "@messageFileOperations": {
+ "description": "data_page: File operations"
+ },
+ "messageFilePath": "File path",
+ "@messageFilePath": {
+ "description": "data_page: File path"
+ },
+ "messageSave": "Save",
+ "@messageSave": {
+ "description": "data_page: Save"
+ },
+ "messageLoad": "Load",
+ "@messageLoad": {
+ "description": "data_page: Load"
+ },
+ "messageAsn1Decoding": "ASN.1 decoding",
+ "@messageAsn1Decoding": {
+ "description": "data_page: ASN.1 decoding"
+ },
+ "messageHexData": "Hex data",
+ "@messageHexData": {
+ "description": "data_page: Hex data"
+ },
+ "messageDecode": "Decode",
+ "@messageDecode": {
+ "description": "data_page: Decode"
+ },
+ "messageFileComparison": "File comparison",
+ "@messageFileComparison": {
+ "description": "data_page: File comparison"
+ },
+ "messageFileA": "File A",
+ "@messageFileA": {
+ "description": "data_page: File A"
+ },
+ "messageFileB": "File B",
+ "@messageFileB": {
+ "description": "data_page: File B"
+ },
+ "messageOpenFileA": "Open file A",
+ "@messageOpenFileA": {
+ "description": "dump_compare_page: Open file A"
+ },
+ "messageOpenFileB": "Open file B",
+ "@messageOpenFileB": {
+ "description": "dump_compare_page: Open file B"
+ },
+ "messageDifferencesOnly": "Differences only",
+ "@messageDifferencesOnly": {
+ "description": "dump_compare_page: Differences only"
+ },
+ "messageFileAError": "File A error: {value}",
+ "@messageFileAError": {
+ "description": "dump_compare_page: File A error: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageFileBError": "File B error: {value}",
+ "@messageFileBError": {
+ "description": "dump_compare_page: File B error: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageOpenTwoDumpFilesToCompareThem": "Open two dump files to compare them",
+ "@messageOpenTwoDumpFilesToCompareThem": {
+ "description": "dump_compare_page: Open two dump files to compare them"
+ },
+ "messageSupportsEmlBinJsonDump": "Supports .eml / .bin / .json / .dump",
+ "@messageSupportsEmlBinJsonDump": {
+ "description": "dump_compare_page: Supports .eml / .bin / .json / .dump"
+ },
+ "messageTotalBlocks": "Total blocks",
+ "@messageTotalBlocks": {
+ "description": "dump_compare_page: Total blocks"
+ },
+ "messageDataDifferences": "Data differences",
+ "@messageDataDifferences": {
+ "description": "dump_compare_page: Data differences"
+ },
+ "messageBlocks": "Blocks: {value}",
+ "@messageBlocks": {
+ "description": "dump_compare_page: Blocks: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageKeyADifferences": "Key A differences",
+ "@messageKeyADifferences": {
+ "description": "dump_compare_page: Key A differences"
+ },
+ "messageKeyBDifferences": "Key B differences",
+ "@messageKeyBDifferences": {
+ "description": "dump_compare_page: Key B differences"
+ },
+ "messageNone": "(none)",
+ "@messageNone": {
+ "description": "dump_compare_page: (none)"
+ },
+ "messageManufacturer": "Manufacturer",
+ "@messageManufacturer": {
+ "description": "dump_compare_page: Manufacturer"
+ },
+ "messageTrailer": "Trailer",
+ "@messageTrailer": {
+ "description": "dump_compare_page: Trailer"
+ },
+ "messageCouldNotOpenFile": "Could not open file: {value}",
+ "@messageCouldNotOpenFile": {
+ "description": "dump_viewer_page: Could not open file: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageDictionaryFileUniqueKeys": "Dictionary file: {value} unique keys",
+ "@messageDictionaryFileUniqueKeys": {
+ "description": "dump_viewer_page: Dictionary file: {value} unique keys",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageDictionaryLoadedKeys": "Dictionary loaded; keys: {value}",
+ "@messageDictionaryLoadedKeys": {
+ "description": "dump_viewer_page: Dictionary loaded; keys: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageMergedKeysForSectorsBlockDataUnchanged": "Merged keys for {value} sectors (block data unchanged)",
+ "@messageMergedKeysForSectorsBlockDataUnchanged": {
+ "description": "dump_viewer_page: Merged keys for {value} sectors (block data unchanged)",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageKeyHandling": "Key handling",
+ "@messageKeyHandling": {
+ "description": "dump_viewer_page: Key handling"
+ },
+ "messageKeyDataIsAlreadyLoadedHowShould": "Key data is already loaded. How should keys be handled when opening another dump?",
+ "@messageKeyDataIsAlreadyLoadedHowShould": {
+ "description": "dump_viewer_page: Key data is already loaded. How should keys be handled when opening another dump?"
+ },
+ "messageDumpKeyFieldsAreOftenFfWhile": "Dump key fields are often FF, while actual keys may come from a separate key file.",
+ "@messageDumpKeyFieldsAreOftenFfWhile": {
+ "description": "dump_viewer_page: Dump key fields are often FF, while actual keys may come from a separate key file."
+ },
+ "messageKeepCurrentKeys": "Keep current keys",
+ "@messageKeepCurrentKeys": {
+ "description": "dump_viewer_page: Keep current keys"
+ },
+ "messageReplaceWithDumpKeys": "Replace with dump keys",
+ "@messageReplaceWithDumpKeys": {
+ "description": "dump_viewer_page: Replace with dump keys"
+ },
+ "messageDefaultKeyFileSet": "Default key file set: {value}",
+ "@messageDefaultKeyFileSet": {
+ "description": "dump_viewer_page: Default key file set: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageDefaultDumpFileSet": "Default dump file set: {value}",
+ "@messageDefaultDumpFileSet": {
+ "description": "dump_viewer_page: Default dump file set: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageSectorView": "Sector view",
+ "@messageSectorView": {
+ "description": "dump_viewer_page: Sector view"
+ },
+ "messageKeysEdit": "Keys / edit",
+ "@messageKeysEdit": {
+ "description": "dump_viewer_page: Keys / edit"
+ },
+ "messageDetailedAnalysis": "Detailed analysis",
+ "@messageDetailedAnalysis": {
+ "description": "dump_viewer_page: Detailed analysis"
+ },
+ "messageWriteClear": "Write / clear",
+ "@messageWriteClear": {
+ "description": "dump_viewer_page: Write / clear"
+ },
+ "messageCommandData": "Command data",
+ "@messageCommandData": {
+ "description": "dump_viewer_page: Command data"
+ },
+ "messageOpenADumpFileToViewSector": "Open a dump file to view sector data",
+ "@messageOpenADumpFileToViewSector": {
+ "description": "dump_viewer_page: Open a dump file to view sector data"
+ },
+ "messageOpenADumpOrKeyFileTo": "Open a dump or key file to edit it",
+ "@messageOpenADumpOrKeyFileTo": {
+ "description": "dump_viewer_page: Open a dump or key file to edit it"
+ },
+ "messageOpenADumpFileToViewDetailed": "Open a dump file to view detailed analysis",
+ "@messageOpenADumpFileToViewDetailed": {
+ "description": "dump_viewer_page: Open a dump file to view detailed analysis"
+ },
+ "messageSupportsEmlBinJsonDumpDic": "Supports .eml / .bin / .json / .dump / .dic",
+ "@messageSupportsEmlBinJsonDumpDic": {
+ "description": "dump_viewer_page: Supports .eml / .bin / .json / .dump / .dic"
+ },
+ "messageOpenFile": "Open file",
+ "@messageOpenFile": {
+ "description": "dump_viewer_page: Open file"
+ },
+ "messageSetAsDefaultKeys": "Set as default keys",
+ "@messageSetAsDefaultKeys": {
+ "description": "dump_viewer_page: Set as default keys"
+ },
+ "messageSetAsDefaultDump": "Set as default dump",
+ "@messageSetAsDefaultDump": {
+ "description": "dump_viewer_page: Set as default dump"
+ },
+ "messageExportAs": "Export as",
+ "@messageExportAs": {
+ "description": "dump_viewer_page: Export as"
+ },
+ "messageEmlTextDump": ".eml (text dump)",
+ "@messageEmlTextDump": {
+ "description": "dump_viewer_page: .eml (text dump)"
+ },
+ "messageBinBinaryDump": ".bin (binary dump)",
+ "@messageBinBinaryDump": {
+ "description": "dump_viewer_page: .bin (binary dump)"
+ },
+ "messageJsonPm3Format": ".json (PM3 format)",
+ "@messageJsonPm3Format": {
+ "description": "dump_viewer_page: .json (PM3 format)"
+ },
+ "messageKeyBinBinaryKeys": ".key.bin (binary keys)",
+ "@messageKeyBinBinaryKeys": {
+ "description": "dump_viewer_page: .key.bin (binary keys)"
+ },
+ "messageDicKeyDictionary": ".dic (key dictionary)",
+ "@messageDicKeyDictionary": {
+ "description": "dump_viewer_page: .dic (key dictionary)"
+ },
+ "messageKeysTxtKeyList": ".keys.txt (key list)",
+ "@messageKeysTxtKeyList": {
+ "description": "dump_viewer_page: .keys.txt (key list)"
+ },
+ "messageExportFailed": "Export failed: {value}",
+ "@messageExportFailed": {
+ "description": "dump_viewer_page: Export failed: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageExportedAndOrganized": "Exported and organized: {value}",
+ "@messageExportedAndOrganized": {
+ "description": "dump_viewer_page: Exported and organized: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageExported": "Exported: {value}",
+ "@messageExported": {
+ "description": "dump_viewer_page: Exported: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageNoCardData": "No card data",
+ "@messageNoCardData": {
+ "description": "dump_viewer_page: No card data"
+ },
+ "messageSectorBlocks": "Sector {value} (blocks {value2}–{value3})",
+ "@messageSectorBlocks": {
+ "description": "dump_viewer_page: Sector {value} (blocks {value2}–{value3})",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ },
+ "value3": {
+ "type": "String"
+ }
+ }
+ },
+ "messageAccessConditions": "Access conditions",
+ "@messageAccessConditions": {
+ "description": "dump_viewer_page: Access conditions"
+ },
+ "messageManufacturerBlock": "Manufacturer block",
+ "@messageManufacturerBlock": {
+ "description": "dump_viewer_page: Manufacturer block"
+ },
+ "messageBlock": "Block",
+ "@messageBlock": {
+ "description": "dump_viewer_page: Block"
+ },
+ "messageDescription": "Description",
+ "@messageDescription": {
+ "description": "dump_viewer_page: Description"
+ },
+ "messageData": "Data {value}",
+ "@messageData": {
+ "description": "dump_viewer_page: Data {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageKeyAWriteKeyAAccessBits": "Key A: write Key A / access bits / Key B",
+ "@messageKeyAWriteKeyAAccessBits": {
+ "description": "dump_viewer_page: Key A: write Key A / access bits / Key B"
+ },
+ "messageKeyAWriteKeyAAccessBits2": "Key A: write Key A / access bits; Key B: read / write",
+ "@messageKeyAWriteKeyAAccessBits2": {
+ "description": "dump_viewer_page: Key A: write Key A / access bits; Key B: read / write"
+ },
+ "messageReadAccessBitsOnly": "Read access bits only",
+ "@messageReadAccessBitsOnly": {
+ "description": "dump_viewer_page: Read access bits only"
+ },
+ "messageKeyAWriteAccessBitsKeyB": "Key A: write access bits; Key B: read / write",
+ "@messageKeyAWriteAccessBitsKeyB": {
+ "description": "dump_viewer_page: Key A: write access bits; Key B: read / write"
+ },
+ "messageKeyBWriteAll": "Key B: write all",
+ "@messageKeyBWriteAll": {
+ "description": "dump_viewer_page: Key B: write all"
+ },
+ "messageLocked": "Locked",
+ "@messageLocked": {
+ "description": "dump_viewer_page: Locked"
+ },
+ "messageUnknown": "Unknown",
+ "@messageUnknown": {
+ "description": "dump_viewer_page: Unknown"
+ },
+ "messageReadWriteWithKeyAOrB": "Read / write with Key A or B",
+ "@messageReadWriteWithKeyAOrB": {
+ "description": "dump_viewer_page: Read / write with Key A or B"
+ },
+ "messageReadWithABNoWriting": "Read with A / B; no writing",
+ "@messageReadWithABNoWriting": {
+ "description": "dump_viewer_page: Read with A / B; no writing"
+ },
+ "messageReadWriteWithKeyBOnly": "Read / write with Key B only",
+ "@messageReadWriteWithKeyBOnly": {
+ "description": "dump_viewer_page: Read / write with Key B only"
+ },
+ "messageReadABWriteBDecrementA": "Read: A / B; write: B; decrement: A / B (value block)",
+ "@messageReadABWriteBDecrementA": {
+ "description": "dump_viewer_page: Read: A / B; write: B; decrement: A / B (value block)"
+ },
+ "messageReadWithKeyBOnly": "Read with Key B only",
+ "@messageReadWithKeyBOnly": {
+ "description": "dump_viewer_page: Read with Key B only"
+ },
+ "messageReadABWriteIncrementBDecrement": "Read: A / B; write / increment: B; decrement: A / B (value block)",
+ "@messageReadABWriteIncrementBDecrement": {
+ "description": "dump_viewer_page: Read: A / B; write / increment: B; decrement: A / B (value block)"
+ },
+ "messageNoData": "No data",
+ "@messageNoData": {
+ "description": "dump_viewer_page: No data"
+ },
+ "messageOverview": "📋 Overview",
+ "@messageOverview": {
+ "description": "dump_viewer_page: 📋 Overview"
+ },
+ "messageCardType": "Card type",
+ "@messageCardType": {
+ "description": "dump_viewer_page: Card type"
+ },
+ "messageEmptyDataBlocks": "Empty data blocks",
+ "@messageEmptyDataBlocks": {
+ "description": "dump_viewer_page: Empty data blocks"
+ },
+ "messageDataUsage": "Data usage",
+ "@messageDataUsage": {
+ "description": "dump_viewer_page: Data usage"
+ },
+ "messageValueBlocks": "Value blocks",
+ "@messageValueBlocks": {
+ "description": "dump_viewer_page: Value blocks"
+ },
+ "messageManufacturerBlockBlock0": "🏭 Manufacturer block (block 0)",
+ "@messageManufacturerBlockBlock0": {
+ "description": "dump_viewer_page: 🏭 Manufacturer block (block 0)"
+ },
+ "messageChipType": "Chip type",
+ "@messageChipType": {
+ "description": "dump_viewer_page: Chip type"
+ },
+ "messageRawData": "Raw data",
+ "@messageRawData": {
+ "description": "dump_viewer_page: Raw data"
+ },
+ "messageKeyAnalysis": "🔑 Key analysis",
+ "@messageKeyAnalysis": {
+ "description": "dump_viewer_page: 🔑 Key analysis"
+ },
+ "messageTotalSectors": "Total sectors",
+ "@messageTotalSectors": {
+ "description": "dump_viewer_page: Total sectors"
+ },
+ "messageKnownKeyA": "Known Key A",
+ "@messageKnownKeyA": {
+ "description": "dump_viewer_page: Known Key A"
+ },
+ "messageKnownKeyB": "Known Key B",
+ "@messageKnownKeyB": {
+ "description": "dump_viewer_page: Known Key B"
+ },
+ "messageAllKeysIdentical": "All keys identical",
+ "@messageAllKeysIdentical": {
+ "description": "dump_viewer_page: All keys identical"
+ },
+ "messageYes": "Yes",
+ "@messageYes": {
+ "description": "dump_viewer_page: Yes"
+ },
+ "messageNo": "No",
+ "@messageNo": {
+ "description": "dump_viewer_page: No"
+ },
+ "messageContainsBlankKeys": "Contains blank keys",
+ "@messageContainsBlankKeys": {
+ "description": "dump_viewer_page: Contains blank keys"
+ },
+ "messageYes2": "Yes ⚠️",
+ "@messageYes2": {
+ "description": "dump_viewer_page: Yes ⚠️"
+ },
+ "messageKeyAGroups": "Key A groups:",
+ "@messageKeyAGroups": {
+ "description": "dump_viewer_page: Key A groups:"
+ },
+ "messageSectors": "{value} → sectors {value2}",
+ "@messageSectors": {
+ "description": "dump_viewer_page: {value} → sectors {value2}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageKeyBGroups": "Key B groups:",
+ "@messageKeyBGroups": {
+ "description": "dump_viewer_page: Key B groups:"
+ },
+ "messageDefaultKeyMatches": "⚠️ Default key matches:",
+ "@messageDefaultKeyMatches": {
+ "description": "dump_viewer_page: ⚠️ Default key matches:"
+ },
+ "messageSectorKey": "Sector {value} Key {value2}: {value3} ({value4})",
+ "@messageSectorKey": {
+ "description": "dump_viewer_page: Sector {value} Key {value2}: {value3} ({value4})",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ },
+ "value3": {
+ "type": "String"
+ },
+ "value4": {
+ "type": "String"
+ }
+ }
+ },
+ "messageMadApplicationDirectoryV": "📂 MAD application directory (v{value})",
+ "@messageMadApplicationDirectoryV": {
+ "description": "dump_viewer_page: 📂 MAD application directory (v{value})",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageSector": "Sector {value}",
+ "@messageSector": {
+ "description": "dump_viewer_page: Sector {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageValueBlocks2": "💰 Value blocks",
+ "@messageValueBlocks2": {
+ "description": "dump_viewer_page: 💰 Value blocks"
+ },
+ "messageBlock2": "Block {value}",
+ "@messageBlock2": {
+ "description": "dump_viewer_page: Block {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageValueAddress": "Value: {value} Address: {value2}",
+ "@messageValueAddress": {
+ "description": "dump_viewer_page: Value: {value} Address: {value2}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageSectorOverview": "📊 Sector overview",
+ "@messageSectorOverview": {
+ "description": "dump_viewer_page: 📊 Sector overview"
+ },
+ "messageValidAccessBits": "✅ Valid access bits",
+ "@messageValidAccessBits": {
+ "description": "dump_viewer_page: ✅ Valid access bits"
+ },
+ "messageInvalidAccessBits": "❌ Invalid access bits",
+ "@messageInvalidAccessBits": {
+ "description": "dump_viewer_page: ❌ Invalid access bits"
+ },
+ "messageNoAccessBits": "- No access bits",
+ "@messageNoAccessBits": {
+ "description": "dump_viewer_page: - No access bits"
+ },
+ "messageDataBlockAsciiView": "📝 Data block ASCII view",
+ "@messageDataBlockAsciiView": {
+ "description": "dump_viewer_page: 📝 Data block ASCII view"
+ },
+ "messageExportAllFormats": "📦 Export all formats",
+ "@messageExportAllFormats": {
+ "description": "dump_viewer_page: 📦 Export all formats"
+ },
+ "messageExportTheCurrentDumpInAllSupported": "Export the current dump in all supported formats:",
+ "@messageExportTheCurrentDumpInAllSupported": {
+ "description": "dump_viewer_page: Export the current dump in all supported formats:"
+ },
+ "messageExportEml": "Export .eml",
+ "@messageExportEml": {
+ "description": "dump_viewer_page: Export .eml"
+ },
+ "messageExportBin": "Export .bin",
+ "@messageExportBin": {
+ "description": "dump_viewer_page: Export .bin"
+ },
+ "messageExportJson": "Export .json",
+ "@messageExportJson": {
+ "description": "dump_viewer_page: Export .json"
+ },
+ "messageExportKeyBin": "Export .key.bin",
+ "@messageExportKeyBin": {
+ "description": "dump_viewer_page: Export .key.bin"
+ },
+ "messageExportDic": "Export .dic",
+ "@messageExportDic": {
+ "description": "dump_viewer_page: Export .dic"
+ },
+ "messageExportKeysTxt": "Export .keys.txt",
+ "@messageExportKeysTxt": {
+ "description": "dump_viewer_page: Export .keys.txt"
+ },
+ "messageKeyExtraction": "🔑 Key extraction",
+ "@messageKeyExtraction": {
+ "description": "dump_viewer_page: 🔑 Key extraction"
+ },
+ "messageAllKeysExtractedFromTheCurrentDump": "All keys extracted from the current dump:",
+ "@messageAllKeysExtractedFromTheCurrentDump": {
+ "description": "dump_viewer_page: All keys extracted from the current dump:"
+ },
+ "messageCopyAllKeys": "Copy all keys",
+ "@messageCopyAllKeys": {
+ "description": "dump_viewer_page: Copy all keys"
+ },
+ "messageKeyAOnly": "Key A only",
+ "@messageKeyAOnly": {
+ "description": "dump_viewer_page: Key A only"
+ },
+ "messageKeyBOnly": "Key B only",
+ "@messageKeyBOnly": {
+ "description": "dump_viewer_page: Key B only"
+ },
+ "messageFileFormatConversion": "🔄 File format conversion",
+ "@messageFileFormatConversion": {
+ "description": "dump_viewer_page: 🔄 File format conversion"
+ },
+ "messageSelectADumpOrKeyFileTo": "Select a dump or key file to convert to another format:",
+ "@messageSelectADumpOrKeyFileTo": {
+ "description": "dump_viewer_page: Select a dump or key file to convert to another format:"
+ },
+ "messageSelectSourceFile": "Select source file",
+ "@messageSelectSourceFile": {
+ "description": "dump_viewer_page: Select source file"
+ },
+ "messageTargetFormat": "Target format:",
+ "@messageTargetFormat": {
+ "description": "dump_viewer_page: Target format:"
+ },
+ "messageConverting": "Converting…",
+ "@messageConverting": {
+ "description": "dump_viewer_page: Converting…"
+ },
+ "messageStartConversion": "Start conversion",
+ "@messageStartConversion": {
+ "description": "dump_viewer_page: Start conversion"
+ },
+ "messageExternalKeyFile": "📋 External key file",
+ "@messageExternalKeyFile": {
+ "description": "dump_viewer_page: 📋 External key file"
+ },
+ "messageLoadedExternalKeys": "Loaded external keys:",
+ "@messageLoadedExternalKeys": {
+ "description": "dump_viewer_page: Loaded external keys:"
+ },
+ "messageFormatReference": "ℹ️ Format reference",
+ "@messageFormatReference": {
+ "description": "dump_viewer_page: ℹ️ Format reference"
+ },
+ "messageRawBinaryDump16BytesPerBlock": "Raw binary dump (16 bytes per block); the default PM3 dump format",
+ "@messageRawBinaryDump16BytesPerBlock": {
+ "description": "dump_viewer_page: Raw binary dump (16 bytes per block); the default PM3 dump format"
+ },
+ "messageTextDump32HexDigitsPerLine": "Text dump (32 hex digits per line); used for emulator loading",
+ "@messageTextDump32HexDigitsPerLine": {
+ "description": "dump_viewer_page: Text dump (32 hex digits per line); used for emulator loading"
+ },
+ "messagePm3JsonFormatIncludingCardMetadataAnd": "PM3 JSON format, including card metadata and keys",
+ "@messagePm3JsonFormatIncludingCardMetadataAnd": {
+ "description": "dump_viewer_page: PM3 JSON format, including card metadata and keys"
+ },
+ "messageBinaryKeyFile6ByteKeyA": "Binary key file: (6-byte Key A + 6-byte Key B) × sector count",
+ "@messageBinaryKeyFile6ByteKeyA": {
+ "description": "dump_viewer_page: Binary key file: (6-byte Key A + 6-byte Key B) × sector count"
+ },
+ "messageKeyDictionaryOneKeyPerLineUsed": "Key dictionary (one key per line); used for brute-force recovery",
+ "@messageKeyDictionaryOneKeyPerLineUsed": {
+ "description": "dump_viewer_page: Key dictionary (one key per line); used for brute-force recovery"
+ },
+ "messageReadableKeyTableSectorNumberKeyA": "Readable key table (sector number + Key A + Key B)",
+ "@messageReadableKeyTableSectorNumberKeyA": {
+ "description": "dump_viewer_page: Readable key table (sector number + Key A + Key B)"
+ },
+ "messageSector2": "Sector",
+ "@messageSector2": {
+ "description": "dump_viewer_page: Sector"
+ },
+ "messageSectorAB": "Sector {value} A:{value2} B:{value3}",
+ "@messageSectorAB": {
+ "description": "dump_viewer_page: Sector {value} A:{value2} B:{value3}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ },
+ "value3": {
+ "type": "String"
+ }
+ }
+ },
+ "messageSaveConvertedFile": "Save converted file",
+ "@messageSaveConvertedFile": {
+ "description": "dump_viewer_page: Save converted file"
+ },
+ "messageConversionSuccessfulOutput": "✅ Conversion successful!\nOutput: {value}\n{value2}",
+ "@messageConversionSuccessfulOutput": {
+ "description": "dump_viewer_page: ✅ Conversion successful!\nOutput: {value}\n{value2}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageConversionError": "❌ Conversion error: {value}",
+ "@messageConversionError": {
+ "description": "dump_viewer_page: ❌ Conversion error: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageEnterHexDigits": "Enter {value} hex digits",
+ "@messageEnterHexDigits": {
+ "description": "dump_viewer_page: Enter {value} hex digits",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageChangesNotApplied": "Changes not applied: {value}",
+ "@messageChangesNotApplied": {
+ "description": "dump_viewer_page: Changes not applied: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageKeyEditor": "🔑 Key editor",
+ "@messageKeyEditor": {
+ "description": "dump_viewer_page: 🔑 Key editor"
+ },
+ "messageApplyChanges": "Apply changes",
+ "@messageApplyChanges": {
+ "description": "dump_viewer_page: Apply changes"
+ },
+ "messageAllDefaults": "All defaults",
+ "@messageAllDefaults": {
+ "description": "dump_viewer_page: All defaults"
+ },
+ "messageBlockDataEditor": "📝 Block data editor",
+ "@messageBlockDataEditor": {
+ "description": "dump_viewer_page: 📝 Block data editor"
+ },
+ "messageData2": "Data",
+ "@messageData2": {
+ "description": "dump_viewer_page: Data"
+ },
+ "messageOpenADumpFileFirst": "Open a dump file first",
+ "@messageOpenADumpFileFirst": {
+ "description": "dump_viewer_page: Open a dump file first"
+ },
+ "messageCuidCardWritingAndClearing": "CUID card writing and clearing",
+ "@messageCuidCardWritingAndClearing": {
+ "description": "dump_viewer_page: CUID card writing and clearing"
+ },
+ "messageCuidGen2CardsHaveNoBackdoorCommands": "CUID (Gen2) cards have no backdoor commands; each block must be authenticated with a known key before writing.\n• Clear card: authenticate with the current card keys and zero data blocks.\n• Write card: authenticate with the target card keys and write the dump block by block.\n• Write sector: write only the selected sector.\n• Gen1A backdoor restore: restore the entire card using hf mf restore (Gen1A cards only).\n\n⚠️ Writing CUID block 0 automatically adds --force.",
+ "@messageCuidGen2CardsHaveNoBackdoorCommands": {
+ "description": "dump_viewer_page: CUID (Gen2) cards have no backdoor commands; each block must be authenticated with a known key before writing.\n• Clear card: authenticate with the current card keys and zero data blocks.\n• Write card: authenticate with the target card keys and write the dump block by block.\n• Write sector: write only the selected sector.\n• Gen1A backdoor restore: restore the entire card using hf mf restore (Gen1A cards only).\n\n⚠️ Writing CUID block 0 automatically adds --force."
+ },
+ "messageConnectYourPm3DeviceFirst": "Connect your PM3 device first",
+ "@messageConnectYourPm3DeviceFirst": {
+ "description": "dump_viewer_page: Connect your PM3 device first"
+ },
+ "messageWriteOptions": "⚙️ Write options",
+ "@messageWriteOptions": {
+ "description": "dump_viewer_page: ⚙️ Write options"
+ },
+ "messageAuthenticationKeyType": "Authentication key type:",
+ "@messageAuthenticationKeyType": {
+ "description": "dump_viewer_page: Authentication key type:"
+ },
+ "messageSkipBlock0ManufacturerBlock": "Skip block 0 (manufacturer block)",
+ "@messageSkipBlock0ManufacturerBlock": {
+ "description": "dump_viewer_page: Skip block 0 (manufacturer block)"
+ },
+ "messageBlock0CannotBeWrittenUnlessThis": "Block 0 cannot be written unless this is a magic card",
+ "@messageBlock0CannotBeWrittenUnlessThis": {
+ "description": "dump_viewer_page: Block 0 cannot be written unless this is a magic card"
+ },
+ "messageWriteTrailersKeysAccessBits": "Write trailers (keys + access bits)",
+ "@messageWriteTrailersKeysAccessBits": {
+ "description": "dump_viewer_page: Write trailers (keys + access bits)"
+ },
+ "messageWriteTheDumpTrailerDataToThe": "Write the dump trailer data to the target card",
+ "@messageWriteTheDumpTrailerDataToThe": {
+ "description": "dump_viewer_page: Write the dump trailer data to the target card"
+ },
+ "messageDumpKeysForAuthentication": "🔑 Dump keys (for authentication)",
+ "@messageDumpKeysForAuthentication": {
+ "description": "dump_viewer_page: 🔑 Dump keys (for authentication)"
+ },
+ "messageSmartWrite": "🚀 Smart write",
+ "@messageSmartWrite": {
+ "description": "dump_viewer_page: 🚀 Smart write"
+ },
+ "messageSelectTheBestWritePathForThe": "Select the best write path for the target card type (UID / CUID / FUID / Gen1A / Gen3 / Gen4).",
+ "@messageSelectTheBestWritePathForThe": {
+ "description": "dump_viewer_page: Select the best write path for the target card type (UID / CUID / FUID / Gen1A / Gen3 / Gen4)."
+ },
+ "messageTargetCardTypeMode": "Target card type / mode",
+ "@messageTargetCardTypeMode": {
+ "description": "dump_viewer_page: Target card type / mode"
+ },
+ "messageAutoDefault": "AUTO (default)",
+ "@messageAutoDefault": {
+ "description": "dump_viewer_page: AUTO (default)"
+ },
+ "messageStandardUidCard": "Standard UID card",
+ "@messageStandardUidCard": {
+ "description": "dump_viewer_page: Standard UID card"
+ },
+ "messageFuidUidCanBeChangedOnce": "FUID (UID can be changed once)",
+ "@messageFuidUidCanBeChangedOnce": {
+ "description": "dump_viewer_page: FUID (UID can be changed once)"
+ },
+ "messageGen1aBackdoorCard": "Gen1A backdoor card",
+ "@messageGen1aBackdoorCard": {
+ "description": "dump_viewer_page: Gen1A backdoor card"
+ },
+ "messageGen3MagicCard": "Gen3 magic card",
+ "@messageGen3MagicCard": {
+ "description": "dump_viewer_page: Gen3 magic card"
+ },
+ "messageGen4PasswordOptional": "Gen4 password (optional)",
+ "@messageGen4PasswordOptional": {
+ "description": "dump_viewer_page: Gen4 password (optional)"
+ },
+ "message8HexDigitsEG00000000": "8 hex digits, e.g. 00000000",
+ "@message8HexDigitsEG00000000": {
+ "description": "dump_viewer_page: 8 hex digits, e.g. 00000000"
+ },
+ "messageSmartWrite2": "⚡ Smart write",
+ "@messageSmartWrite2": {
+ "description": "dump_viewer_page: ⚡ Smart write"
+ },
+ "messageAuthenticateWithTheDumpKeysAndZero": "Authenticate with the dump keys and zero all data blocks.\nReset trailers to default keys and access bits.\nFor CUID / Gen2 cards (no backdoor).",
+ "@messageAuthenticateWithTheDumpKeysAndZero": {
+ "description": "dump_viewer_page: Authenticate with the dump keys and zero all data blocks.\nReset trailers to default keys and access bits.\nFor CUID / Gen2 cards (no backdoor)."
+ },
+ "messageClearEntireCuidCard": "Clear entire CUID card",
+ "@messageClearEntireCuidCard": {
+ "description": "dump_viewer_page: Clear entire CUID card"
+ },
+ "messageAuthenticateWithTheTargetCardKeysAnd": "Authenticate with the target card keys and write the dump block by block.\nFor CUID / Gen2 cards (no backdoor).\nKnown target card keys are required.",
+ "@messageAuthenticateWithTheTargetCardKeysAnd": {
+ "description": "dump_viewer_page: Authenticate with the target card keys and write the dump block by block.\nFor CUID / Gen2 cards (no backdoor).\nKnown target card keys are required."
+ },
+ "messageWriteEntireCuidCard": "Write entire CUID card",
+ "@messageWriteEntireCuidCard": {
+ "description": "dump_viewer_page: Write entire CUID card"
+ },
+ "messageWriteOnlySelectedSectorAuthenticateWithThe": "Write only selected sector {value}.\nAuthenticate with the dump keys and write each block in this sector.\nUse for partial updates after editing one sector.",
+ "@messageWriteOnlySelectedSectorAuthenticateWithThe": {
+ "description": "dump_viewer_page: Write only selected sector {value}.\nAuthenticate with the dump keys and write each block in this sector.\nUse for partial updates after editing one sector.",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageWriteSector": "Write sector {value}",
+ "@messageWriteSector": {
+ "description": "dump_viewer_page: Write sector {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageRestoreTheEntireCardUsingThePm3": "Restore the entire card using the PM3 hf mf restore command.\nFor Gen1A backdoor cards only; no keys required.\nWrites all blocks directly using the 0x40 / 0x43 backdoor commands.",
+ "@messageRestoreTheEntireCardUsingThePm3": {
+ "description": "dump_viewer_page: Restore the entire card using the PM3 hf mf restore command.\nFor Gen1A backdoor cards only; no keys required.\nWrites all blocks directly using the 0x40 / 0x43 backdoor commands."
+ },
+ "messageGen1aBackdoorRestore": "Gen1A backdoor restore",
+ "@messageGen1aBackdoorRestore": {
+ "description": "dump_viewer_page: Gen1A backdoor restore"
+ },
+ "messageWriteProgress": "📊 Write progress",
+ "@messageWriteProgress": {
+ "description": "dump_viewer_page: 📊 Write progress"
+ },
+ "messageFormatConversionAndKeyExtraction": "📦 Format conversion and key extraction",
+ "@messageFormatConversionAndKeyExtraction": {
+ "description": "dump_viewer_page: 📦 Format conversion and key extraction"
+ },
+ "messageEditedDataIsIncomplete": "Edited data is incomplete",
+ "@messageEditedDataIsIncomplete": {
+ "description": "dump_viewer_page: Edited data is incomplete"
+ },
+ "messageNoValidUidFoundCannotWriteUid": "No valid UID found; cannot write UID",
+ "@messageNoValidUidFoundCannotWriteUid": {
+ "description": "dump_viewer_page: No valid UID found; cannot write UID"
+ },
+ "messageClearCuidBlockByBlock": "Clear CUID block by block",
+ "@messageClearCuidBlockByBlock": {
+ "description": "dump_viewer_page: Clear CUID block by block"
+ },
+ "messageAuthenticateTheTargetCardWithTheDump": "Authenticate the target card with the dump keys,\nthen zero each data block.\n\nTrailers will be reset to defaults:\nKey A = FFFFFFFFFFFF\nAccess bits = FF078069\nKey B = FFFFFFFFFFFF\n\n⚠️ This operation cannot be undone!",
+ "@messageAuthenticateTheTargetCardWithTheDump": {
+ "description": "dump_viewer_page: Authenticate the target card with the dump keys,\nthen zero each data block.\n\nTrailers will be reset to defaults:\nKey A = FFFFFFFFFFFF\nAccess bits = FF078069\nKey B = FFFFFFFFFFFF\n\n⚠️ This operation cannot be undone!"
+ },
+ "messageBlocksToClear": "Blocks to clear: {value} ({value2})",
+ "@messageBlocksToClear": {
+ "description": "dump_viewer_page: Blocks to clear: {value} ({value2})",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageSkipBlock0": "skip block 0",
+ "@messageSkipBlock0": {
+ "description": "dump_viewer_page: skip block 0"
+ },
+ "messageIncludeBlock0": "include block 0",
+ "@messageIncludeBlock0": {
+ "description": "dump_viewer_page: include block 0"
+ },
+ "messageStartClearing": "Start clearing",
+ "@messageStartClearing": {
+ "description": "dump_viewer_page: Start clearing"
+ },
+ "messageWriteCuidBlockByBlock": "Write CUID block by block",
+ "@messageWriteCuidBlockByBlock": {
+ "description": "dump_viewer_page: Write CUID block by block"
+ },
+ "messageAuthenticateTheTargetCardWithTheDump2": "Authenticate the target card with the dump keys,\nthen write the dump block by block.\n\n✅ Data blocks: write original dump data.\n✅ Trailers: write complete dump trailer data\n (Key A + access bits + Key B).\n\n💡 Use when a CUID / standard card has known keys\n and its data needs to be replaced.",
+ "@messageAuthenticateTheTargetCardWithTheDump2": {
+ "description": "dump_viewer_page: Authenticate the target card with the dump keys,\nthen write the dump block by block.\n\n✅ Data blocks: write original dump data.\n✅ Trailers: write complete dump trailer data\n (Key A + access bits + Key B).\n\n💡 Use when a CUID / standard card has known keys\n and its data needs to be replaced."
+ },
+ "messageBlocksToWrite": "Blocks to write: {value} ({value2}, {value3})",
+ "@messageBlocksToWrite": {
+ "description": "dump_viewer_page: Blocks to write: {value} ({value2}, {value3})",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ },
+ "value3": {
+ "type": "String"
+ }
+ }
+ },
+ "messageIncludeTrailers": "include trailers",
+ "@messageIncludeTrailers": {
+ "description": "dump_viewer_page: include trailers"
+ },
+ "messageSkipTrailers": "skip trailers",
+ "@messageSkipTrailers": {
+ "description": "dump_viewer_page: skip trailers"
+ },
+ "messageStartWriting": "Start writing",
+ "@messageStartWriting": {
+ "description": "dump_viewer_page: Start writing"
+ },
+ "messageAuthenticateWithTheDumpKeysThenWrite": "Authenticate with the dump keys,\nthen write all blocks in sector {value} to the target card.\n\n{value2} Authentication key: Key {value3}",
+ "@messageAuthenticateWithTheDumpKeysThenWrite": {
+ "description": "dump_viewer_page: Authenticate with the dump keys,\nthen write all blocks in sector {value} to the target card.\n\n{value2} Authentication key: Key {value3}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ },
+ "value3": {
+ "type": "String"
+ }
+ }
+ },
+ "messageIncludeTrailers2": "Include trailers",
+ "@messageIncludeTrailers2": {
+ "description": "dump_viewer_page: Include trailers"
+ },
+ "messageExcludeTrailers": "Exclude trailers",
+ "@messageExcludeTrailers": {
+ "description": "dump_viewer_page: Exclude trailers"
+ },
+ "messageInformation": "Information",
+ "@messageInformation": {
+ "description": "hf_14b_page: Information"
+ },
+ "messageReadWrite": "Read / write",
+ "@messageReadWrite": {
+ "description": "hf_14b_page: Read / write"
+ },
+ "messageReader": "Reader",
+ "@messageReader": {
+ "description": "hf_14b_page: Reader"
+ },
+ "messageReadA14443BCard": "Read a 14443-B card",
+ "@messageReadA14443BCard": {
+ "description": "hf_14b_page: Read a 14443-B card"
+ },
+ "messageTagInformation": "Tag information",
+ "@messageTagInformation": {
+ "description": "hf_14b_page: Tag information"
+ },
+ "messageReadTagDetails": "Read tag details",
+ "@messageReadTagDetails": {
+ "description": "hf_14b_page: Read tag details"
+ },
+ "messageSniff": "Sniff",
+ "@messageSniff": {
+ "description": "hf_14b_page: Sniff"
+ },
+ "messageCapture14443BCommunication": "Capture 14443-B communication",
+ "@messageCapture14443BCommunication": {
+ "description": "hf_14b_page: Capture 14443-B communication"
+ },
+ "messageDump": "Dump",
+ "@messageDump": {
+ "description": "hf_14b_page: Dump"
+ },
+ "messageReadAllData": "Read all data",
+ "@messageReadAllData": {
+ "description": "hf_14b_page: Read all data"
+ },
+ "messageReadNdefData": "Read NDEF data",
+ "@messageReadNdefData": {
+ "description": "hf_14b_page: Read NDEF data"
+ },
+ "messageBlockOperations": "Block operations",
+ "@messageBlockOperations": {
+ "description": "hf_14b_page: Block operations"
+ },
+ "messageBlockNumber": "Block number",
+ "@messageBlockNumber": {
+ "description": "hf_14b_page: Block number"
+ },
+ "messageReadBlock": "Read block",
+ "@messageReadBlock": {
+ "description": "hf_14b_page: Read block"
+ },
+ "messageWriteDataHex": "Write data (hex)",
+ "@messageWriteDataHex": {
+ "description": "hf_14b_page: Write data (hex)"
+ },
+ "messageWriteBlock": "Write block",
+ "@messageWriteBlock": {
+ "description": "hf_14b_page: Write block"
+ },
+ "messageRawCommand": "Raw command",
+ "@messageRawCommand": {
+ "description": "hf_14b_page: Raw command"
+ },
+ "messageRawHexData": "Raw hex data",
+ "@messageRawHexData": {
+ "description": "hf_14b_page: Raw hex data"
+ },
+ "messageSimulateCard": "Simulate card",
+ "@messageSimulateCard": {
+ "description": "hf_14b_page: Simulate card"
+ },
+ "messageSimulateA14443BTag": "Simulate a 14443-B tag",
+ "@messageSimulateA14443BTag": {
+ "description": "hf_14b_page: Simulate a 14443-B tag"
+ },
+ "messageAutomaticallyReadIso15693": "Automatically read ISO 15693",
+ "@messageAutomaticallyReadIso15693": {
+ "description": "hf_15_page: Automatically read ISO 15693"
+ },
+ "messageReadBasicTagInformation": "Read basic tag information",
+ "@messageReadBasicTagInformation": {
+ "description": "hf_15_page: Read basic tag information"
+ },
+ "messageDumpCard": "Dump card",
+ "@messageDumpCard": {
+ "description": "hf_15_page: Dump card"
+ },
+ "messageEraseCard": "Erase card",
+ "@messageEraseCard": {
+ "description": "hf_15_page: Erase card"
+ },
+ "messageClearCardData": "Clear card data",
+ "@messageClearCardData": {
+ "description": "hf_15_page: Clear card data"
+ },
+ "messageConfirmErase": "Confirm erase",
+ "@messageConfirmErase": {
+ "description": "hf_15_page: Confirm erase"
+ },
+ "messageThisOperationCannotBeUndone": "This operation cannot be undone!",
+ "@messageThisOperationCannotBeUndone": {
+ "description": "hf_15_page: This operation cannot be undone!"
+ },
+ "messageWriteData4ByteHex": "Write data (4-byte hex)",
+ "@messageWriteData4ByteHex": {
+ "description": "hf_15_page: Write data (4-byte hex)"
+ },
+ "messageFindAfi": "Find AFI",
+ "@messageFindAfi": {
+ "description": "hf_15_page: Find AFI"
+ },
+ "messageScanApplicationFamilyIdentifiers": "Scan application family identifiers",
+ "@messageScanApplicationFamilyIdentifiers": {
+ "description": "hf_15_page: Scan application family identifiers"
+ },
+ "messageSimulateAnIso15693Tag": "Simulate an ISO 15693 tag",
+ "@messageSimulateAnIso15693Tag": {
+ "description": "hf_15_page: Simulate an ISO 15693 tag"
+ },
+ "messageSniffCommunication": "Sniff communication",
+ "@messageSniffCommunication": {
+ "description": "hf_15_page: Sniff communication"
+ },
+ "messageCaptureReaderCommunication": "Capture reader communication",
+ "@messageCaptureReaderCommunication": {
+ "description": "hf_15_page: Capture reader communication"
+ },
+ "messageSetUid": "Set UID",
+ "@messageSetUid": {
+ "description": "hf_15_page: Set UID"
+ },
+ "messageUid8ByteHex": "UID (8-byte hex)",
+ "@messageUid8ByteHex": {
+ "description": "hf_15_page: UID (8-byte hex)"
+ },
+ "messageConfirm": "Confirm",
+ "@messageConfirm": {
+ "description": "hf_15_page: Confirm"
+ },
+ "messageSearch": "Search",
+ "@messageSearch": {
+ "description": "hf_emv_page: Search"
+ },
+ "messageSearchForEmvCards": "Search for EMV cards",
+ "@messageSearchForEmvCards": {
+ "description": "hf_emv_page: Search for EMV cards"
+ },
+ "messageSelectPpseApplication": "Select PPSE application",
+ "@messageSelectPpseApplication": {
+ "description": "hf_emv_page: Select PPSE application"
+ },
+ "messageExecuteTransaction": "Execute transaction",
+ "@messageExecuteTransaction": {
+ "description": "hf_emv_page: Execute transaction"
+ },
+ "messageSimulateTheEmvTransactionFlow": "Simulate the EMV transaction flow",
+ "@messageSimulateTheEmvTransactionFlow": {
+ "description": "hf_emv_page: Simulate the EMV transaction flow"
+ },
+ "messageTest": "Test",
+ "@messageTest": {
+ "description": "hf_emv_page: Test"
+ },
+ "messageEmvSelfTest": "EMV self-test",
+ "@messageEmvSelfTest": {
+ "description": "hf_emv_page: EMV self-test"
+ },
+ "messageReadAFelicaCard": "Read a FeliCa card",
+ "@messageReadAFelicaCard": {
+ "description": "hf_felica_page: Read a FeliCa card"
+ },
+ "messageGetFelicaDetails": "Get FeliCa details",
+ "@messageGetFelicaDetails": {
+ "description": "hf_felica_page: Get FeliCa details"
+ },
+ "messageDumpCardData": "Dump card data",
+ "@messageDumpCardData": {
+ "description": "hf_felica_page: Dump card data"
+ },
+ "messageLiteDump": "Lite dump",
+ "@messageLiteDump": {
+ "description": "hf_felica_page: Lite dump"
+ },
+ "messageDumpFelicaLite": "Dump FeliCa Lite",
+ "@messageDumpFelicaLite": {
+ "description": "hf_felica_page: Dump FeliCa Lite"
+ },
+ "messageCaptureFelicaCommunication": "Capture FeliCa communication",
+ "@messageCaptureFelicaCommunication": {
+ "description": "hf_felica_page: Capture FeliCa communication"
+ },
+ "messageScServiceCode": "SC (service code)",
+ "@messageScServiceCode": {
+ "description": "hf_felica_page: SC (service code)"
+ },
+ "messageBlBlockNumber": "BL (block number)",
+ "@messageBlBlockNumber": {
+ "description": "hf_felica_page: BL (block number)"
+ },
+ "messageReadFidoInformation": "Read FIDO information",
+ "@messageReadFidoInformation": {
+ "description": "hf_fido_page: Read FIDO information"
+ },
+ "messageRegister": "Register",
+ "@messageRegister": {
+ "description": "hf_fido_page: Register"
+ },
+ "messageFido2Registration": "FIDO2 registration",
+ "@messageFido2Registration": {
+ "description": "hf_fido_page: FIDO2 registration"
+ },
+ "messageAuthenticate": "Authenticate",
+ "@messageAuthenticate": {
+ "description": "hf_fido_page: Authenticate"
+ },
+ "messageFido2Authentication": "FIDO2 authentication",
+ "@messageFido2Authentication": {
+ "description": "hf_fido_page: FIDO2 authentication"
+ },
+ "messageList": "List",
+ "@messageList": {
+ "description": "hf_fido_page: List"
+ },
+ "messageListFidoCommunication": "List FIDO communication",
+ "@messageListFidoCommunication": {
+ "description": "hf_fido_page: List FIDO communication"
+ },
+ "messageKeyRecovery": "Key recovery",
+ "@messageKeyRecovery": {
+ "description": "hf_iclass_page: Key recovery"
+ },
+ "messageEmulator": "Emulator",
+ "@messageEmulator": {
+ "description": "hf_iclass_page: Emulator"
+ },
+ "messageGetInformation": "Get information",
+ "@messageGetInformation": {
+ "description": "hf_iclass_page: Get information"
+ },
+ "messageReadAnIclassTag": "Read an iCLASS tag",
+ "@messageReadAnIclassTag": {
+ "description": "hf_iclass_page: Read an iCLASS tag"
+ },
+ "messageReadCard": "Read card",
+ "@messageReadCard": {
+ "description": "hf_iclass_page: Read card"
+ },
+ "messageReaderMode": "Reader mode",
+ "@messageReaderMode": {
+ "description": "hf_iclass_page: Reader mode"
+ },
+ "messageDumpAllBlocksToAFile": "Dump all blocks to a file",
+ "@messageDumpAllBlocksToAFile": {
+ "description": "hf_iclass_page: Dump all blocks to a file"
+ },
+ "messageKey8ByteHex": "Key (8-byte hex)",
+ "@messageKey8ByteHex": {
+ "description": "hf_iclass_page: Key (8-byte hex)"
+ },
+ "messageWriteData8ByteHex": "Write data (8-byte hex)",
+ "@messageWriteData8ByteHex": {
+ "description": "hf_iclass_page: Write data (8-byte hex)"
+ },
+ "messageCheckKeys": "Check keys",
+ "@messageCheckKeys": {
+ "description": "hf_iclass_page: Check keys"
+ },
+ "messageCheckCommonDefaultKeys": "Check common default keys",
+ "@messageCheckCommonDefaultKeys": {
+ "description": "hf_iclass_page: Check common default keys"
+ },
+ "messageLoclassAttack": "Loclass attack",
+ "@messageLoclassAttack": {
+ "description": "hf_iclass_page: Loclass attack"
+ },
+ "messageRecoverIclassKeysOffline": "Recover iCLASS keys offline",
+ "@messageRecoverIclassKeysOffline": {
+ "description": "hf_iclass_page: Recover iCLASS keys offline"
+ },
+ "messageCaptureCardReaderCommunication": "Capture card / reader communication",
+ "@messageCaptureCardReaderCommunication": {
+ "description": "hf_iclass_page: Capture card / reader communication"
+ },
+ "messageEmulatorOperations": "Emulator operations",
+ "@messageEmulatorOperations": {
+ "description": "hf_iclass_page: Emulator operations"
+ },
+ "messageFilePathOptional": "File path (optional)",
+ "@messageFilePathOptional": {
+ "description": "hf_iclass_page: File path (optional)"
+ },
+ "messageLeaveBlankToUseTheDefault": "Leave blank to use the default",
+ "@messageLeaveBlankToUseTheDefault": {
+ "description": "hf_iclass_page: Leave blank to use the default"
+ },
+ "messageLoadIntoEmulator": "Load into emulator",
+ "@messageLoadIntoEmulator": {
+ "description": "hf_iclass_page: Load into emulator"
+ },
+ "messageView": "View",
+ "@messageView": {
+ "description": "hf_iclass_page: View"
+ },
+ "messageSimulate": "Simulate",
+ "@messageSimulate": {
+ "description": "hf_iclass_page: Simulate"
+ },
+ "messageReadALegicTag": "Read a Legic tag",
+ "@messageReadALegicTag": {
+ "description": "hf_legic_page: Read a Legic tag"
+ },
+ "messageDumpAllData": "Dump all data",
+ "@messageDumpAllData": {
+ "description": "hf_legic_page: Dump all data"
+ },
+ "messageErase": "Erase",
+ "@messageErase": {
+ "description": "hf_legic_page: Erase"
+ },
+ "messageSimulateALegicTag": "Simulate a Legic tag",
+ "@messageSimulateALegicTag": {
+ "description": "hf_legic_page: Simulate a Legic tag"
+ },
+ "messageReadWriteOperations": "Read / write operations",
+ "@messageReadWriteOperations": {
+ "description": "hf_legic_page: Read / write operations"
+ },
+ "messageOffset": "Offset",
+ "@messageOffset": {
+ "description": "hf_legic_page: Offset"
+ },
+ "messageLength": "Length",
+ "@messageLength": {
+ "description": "hf_legic_page: Length"
+ },
+ "messageRead": "Read",
+ "@messageRead": {
+ "description": "hf_legic_page: Read"
+ },
+ "messageWrite": "Write",
+ "@messageWrite": {
+ "description": "hf_legic_page: Write"
+ },
+ "messageApply": "Apply",
+ "@messageApply": {
+ "description": "hf_mfdes_page: Apply"
+ },
+ "messageFiles": "Files",
+ "@messageFiles": {
+ "description": "hf_mfdes_page: Files"
+ },
+ "messageManagement": "Management",
+ "@messageManagement": {
+ "description": "hf_mfdes_page: Management"
+ },
+ "messageDetect": "Detect",
+ "@messageDetect": {
+ "description": "hf_mfdes_page: Detect"
+ },
+ "messageDetectADesfireCard": "Detect a DESFire card",
+ "@messageDetectADesfireCard": {
+ "description": "hf_mfdes_page: Detect a DESFire card"
+ },
+ "messageReadBasicCardInformation": "Read basic card information",
+ "@messageReadBasicCardInformation": {
+ "description": "hf_mfdes_page: Read basic card information"
+ },
+ "messageGetUid": "Get UID",
+ "@messageGetUid": {
+ "description": "hf_mfdes_page: Get UID"
+ },
+ "messageGetCardUid": "Get card UID",
+ "@messageGetCardUid": {
+ "description": "hf_mfdes_page: Get card UID"
+ },
+ "messageFreeMemory": "Free memory",
+ "@messageFreeMemory": {
+ "description": "hf_mfdes_page: Free memory"
+ },
+ "messageViewAvailableMemory": "View available memory",
+ "@messageViewAvailableMemory": {
+ "description": "hf_mfdes_page: View available memory"
+ },
+ "messageCheckDefaultKeys": "Check default keys",
+ "@messageCheckDefaultKeys": {
+ "description": "hf_mfdes_page: Check default keys"
+ },
+ "messageViewApplicationDirectory": "View application directory",
+ "@messageViewApplicationDirectory": {
+ "description": "hf_mfdes_page: View application directory"
+ },
+ "messageListAids": "List AIDs",
+ "@messageListAids": {
+ "description": "hf_mfdes_page: List AIDs"
+ },
+ "messageListApplications": "List applications",
+ "@messageListApplications": {
+ "description": "hf_mfdes_page: List applications"
+ },
+ "messageSelectApplication": "Select application",
+ "@messageSelectApplication": {
+ "description": "hf_mfdes_page: Select application"
+ },
+ "messageAid3ByteHex": "AID (3-byte hex)",
+ "@messageAid3ByteHex": {
+ "description": "hf_mfdes_page: AID (3-byte hex)"
+ },
+ "messageSelect": "Select",
+ "@messageSelect": {
+ "description": "hf_mfdes_page: Select"
+ },
+ "messageListFiles": "List files",
+ "@messageListFiles": {
+ "description": "hf_mfdes_page: List files"
+ },
+ "messageFileDetails": "File details",
+ "@messageFileDetails": {
+ "description": "hf_mfdes_page: File details"
+ },
+ "messageDumpApplication": "Dump application",
+ "@messageDumpApplication": {
+ "description": "hf_mfdes_page: Dump application"
+ },
+ "messageFileReadWrite": "File read / write",
+ "@messageFileReadWrite": {
+ "description": "hf_mfdes_page: File read / write"
+ },
+ "messageKeyNumber": "Key number",
+ "@messageKeyNumber": {
+ "description": "hf_mfdes_page: Key number"
+ },
+ "messageAlgorithm": "Algorithm",
+ "@messageAlgorithm": {
+ "description": "hf_mfdes_page: Algorithm"
+ },
+ "messageKeyHex": "Key (hex)",
+ "@messageKeyHex": {
+ "description": "hf_mfdes_page: Key (hex)"
+ },
+ "messageKeySettings": "Key settings",
+ "@messageKeySettings": {
+ "description": "hf_mfdes_page: Key settings"
+ },
+ "messageGetKeySettings": "Get key settings",
+ "@messageGetKeySettings": {
+ "description": "hf_mfdes_page: Get key settings"
+ },
+ "messageFormatPicc": "Format PICC",
+ "@messageFormatPicc": {
+ "description": "hf_mfdes_page: Format PICC"
+ },
+ "messageDeleteAllApplicationsAndData": "Delete all applications and data",
+ "@messageDeleteAllApplicationsAndData": {
+ "description": "hf_mfdes_page: Delete all applications and data"
+ },
+ "messageConfirmFormat": "Confirm format",
+ "@messageConfirmFormat": {
+ "description": "hf_mfdes_page: Confirm format"
+ },
+ "messageThisWillDeleteAllApplicationsAndData": "This will delete all applications and data. This operation cannot be undone!",
+ "@messageThisWillDeleteAllApplicationsAndData": {
+ "description": "hf_mfdes_page: This will delete all applications and data. This operation cannot be undone!"
+ },
+ "messageReadAnMfuNtagTag": "Read an MFU / NTAG tag",
+ "@messageReadAnMfuNtagTag": {
+ "description": "hf_mfu_page: Read an MFU / NTAG tag"
+ },
+ "messageReadAllDataToAFile": "Read all data to a file",
+ "@messageReadAllDataToAFile": {
+ "description": "hf_mfu_page: Read all data to a file"
+ },
+ "messagePasswordAuthentication": "Password authentication",
+ "@messagePasswordAuthentication": {
+ "description": "hf_mfu_page: Password authentication"
+ },
+ "messagePassword4ByteHex": "Password (4-byte hex)",
+ "@messagePassword4ByteHex": {
+ "description": "hf_mfu_page: Password (4-byte hex)"
+ },
+ "messageReadNdef": "Read NDEF",
+ "@messageReadNdef": {
+ "description": "hf_mfu_page: Read NDEF"
+ },
+ "messageReadNfcDataExchangeFormat": "Read NFC Data Exchange Format",
+ "@messageReadNfcDataExchangeFormat": {
+ "description": "hf_mfu_page: Read NFC Data Exchange Format"
+ },
+ "messageSaveEmulator": "Save emulator",
+ "@messageSaveEmulator": {
+ "description": "hf_mfu_page: Save emulator"
+ },
+ "messageGenerateKeys": "Generate keys",
+ "@messageGenerateKeys": {
+ "description": "hf_mfu_page: Generate keys"
+ },
+ "messageKeygenDeriveKeysFromUid": "keygen — derive keys from UID",
+ "@messageKeygenDeriveKeysFromUid": {
+ "description": "hf_mfu_page: keygen — derive keys from UID"
+ },
+ "messageDerivePassword": "Derive password",
+ "@messageDerivePassword": {
+ "description": "hf_mfu_page: Derive password"
+ },
+ "messagePwdgenDeriveTheNtagPassword": "pwdgen — derive the NTAG password",
+ "@messagePwdgenDeriveTheNtagPassword": {
+ "description": "hf_mfu_page: pwdgen — derive the NTAG password"
+ },
+ "messageKeyCheck": "Key check",
+ "@messageKeyCheck": {
+ "description": "hf_mfu_page: Key check"
+ },
+ "messageCchkCheckCommonKeys": "cchk — check common keys",
+ "@messageCchkCheckCommonKeys": {
+ "description": "hf_mfu_page: cchk — check common keys"
+ },
+ "messageUid7ByteHex": "UID (7-byte hex)",
+ "@messageUid7ByteHex": {
+ "description": "hf_mfu_page: UID (7-byte hex)"
+ },
+ "messageReadSeosInformation": "Read SEOS information",
+ "@messageReadSeosInformation": {
+ "description": "hf_seos_page: Read SEOS information"
+ },
+ "messageReadPacsData": "Read PACS data",
+ "@messageReadPacsData": {
+ "description": "hf_seos_page: Read PACS data"
+ },
+ "messageSimulateASeosCard": "Simulate a SEOS card",
+ "@messageSimulateASeosCard": {
+ "description": "hf_seos_page: Simulate a SEOS card"
+ },
+ "messageHfSearch": "HF search",
+ "@messageHfSearch": {
+ "description": "hf_sniff_page: HF search"
+ },
+ "messageSearchForHighFrequencyTags": "Search for high-frequency tags",
+ "@messageSearchForHighFrequencyTags": {
+ "description": "hf_sniff_page: Search for high-frequency tags"
+ },
+ "messageHfSniff": "HF sniff",
+ "@messageHfSniff": {
+ "description": "hf_sniff_page: HF sniff"
+ },
+ "messageCaptureHighFrequencyCommunication": "Capture high-frequency communication",
+ "@messageCaptureHighFrequencyCommunication": {
+ "description": "hf_sniff_page: Capture high-frequency communication"
+ },
+ "messageHfTuning": "HF tuning",
+ "@messageHfTuning": {
+ "description": "hf_sniff_page: HF tuning"
+ },
+ "messageTuneTheHfAntenna": "Tune the HF antenna",
+ "@messageTuneTheHfAntenna": {
+ "description": "hf_sniff_page: Tune the HF antenna"
+ },
+ "messageProtocolList": "Protocol list",
+ "@messageProtocolList": {
+ "description": "hf_sniff_page: Protocol list"
+ },
+ "messageProtocol": "Protocol",
+ "@messageProtocol": {
+ "description": "hf_sniff_page: Protocol"
+ },
+ "messageDecodeList": "Decode list",
+ "@messageDecodeList": {
+ "description": "hf_sniff_page: Decode list"
+ },
+ "messageReadAnAwidCard": "Read an AWID card",
+ "@messageReadAnAwidCard": {
+ "description": "lf_awid_page: Read an AWID card"
+ },
+ "messageCloneSimulate": "Clone / simulate",
+ "@messageCloneSimulate": {
+ "description": "lf_awid_page: Clone / simulate"
+ },
+ "messageFcFacilityCode": "FC (facility code)",
+ "@messageFcFacilityCode": {
+ "description": "lf_awid_page: FC (facility code)"
+ },
+ "messageCnCardNumber": "CN (card number)",
+ "@messageCnCardNumber": {
+ "description": "lf_awid_page: CN (card number)"
+ },
+ "messageClone": "Clone",
+ "@messageClone": {
+ "description": "lf_awid_page: Clone"
+ },
+ "messageReadAnFdxBTag": "Read an FDX-B tag",
+ "@messageReadAnFdxBTag": {
+ "description": "lf_fdxb_page: Read an FDX-B tag"
+ },
+ "messageRawData2": "Raw data",
+ "@messageRawData2": {
+ "description": "lf_fdxb_page: Raw data"
+ },
+ "messageCloneToT55xx": "Clone to T55xx",
+ "@messageCloneToT55xx": {
+ "description": "lf_fdxb_page: Clone to T55xx"
+ },
+ "messageCloneSimulate2": "Clone / simulate",
+ "@messageCloneSimulate2": {
+ "description": "lf_hid_page: Clone / simulate"
+ },
+ "messageReadHidCard": "Read HID card",
+ "@messageReadHidCard": {
+ "description": "lf_hid_page: Read HID card"
+ },
+ "messageReadAHidProxCard": "Read a HID Prox card",
+ "@messageReadAHidProxCard": {
+ "description": "lf_hid_page: Read a HID Prox card"
+ },
+ "messageDemodulateSignal": "Demodulate signal",
+ "@messageDemodulateSignal": {
+ "description": "lf_hid_page: Demodulate signal"
+ },
+ "messageDemodulateTheHidLfSignal": "Demodulate the HID LF signal",
+ "@messageDemodulateTheHidLfSignal": {
+ "description": "lf_hid_page: Demodulate the HID LF signal"
+ },
+ "messageWiegandData": "Wiegand data",
+ "@messageWiegandData": {
+ "description": "lf_hid_page: Wiegand data"
+ },
+ "messageForExample2006ec0c86": "For example: 2006ec0c86",
+ "@messageForExample2006ec0c86": {
+ "description": "lf_hid_page: For example: 2006ec0c86"
+ },
+ "messageBruteForceRecovery": "Brute-force recovery",
+ "@messageBruteForceRecovery": {
+ "description": "lf_hid_page: Brute-force recovery"
+ },
+ "messageFcOptional": "FC (optional)",
+ "@messageFcOptional": {
+ "description": "lf_hid_page: FC (optional)"
+ },
+ "messageLeaveBlankToTryAllValues": "Leave blank to try all values",
+ "@messageLeaveBlankToTryAllValues": {
+ "description": "lf_hid_page: Leave blank to try all values"
+ },
+ "messageStartRecovery": "Start recovery",
+ "@messageStartRecovery": {
+ "description": "lf_hid_page: Start recovery"
+ },
+ "messageReadAHitagCard": "Read a Hitag card",
+ "@messageReadAHitagCard": {
+ "description": "lf_hitag_page: Read a Hitag card"
+ },
+ "messageGetTagDetails": "Get tag details",
+ "@messageGetTagDetails": {
+ "description": "lf_hitag_page: Get tag details"
+ },
+ "messageCaptureCommunicationData": "Capture communication data",
+ "@messageCaptureCommunicationData": {
+ "description": "lf_hitag_page: Capture communication data"
+ },
+ "messageCheckCommonKeys": "Check common keys",
+ "@messageCheckCommonKeys": {
+ "description": "lf_hitag_page: Check common keys"
+ },
+ "messageKeyRecoveryAttack": "Key recovery attack",
+ "@messageKeyRecoveryAttack": {
+ "description": "lf_hitag_page: Key recovery attack"
+ },
+ "messageSimulateAHitagTag": "Simulate a Hitag tag",
+ "@messageSimulateAHitagTag": {
+ "description": "lf_hitag_page: Simulate a Hitag tag"
+ },
+ "messageReadAnIndalaCard": "Read an Indala card",
+ "@messageReadAnIndalaCard": {
+ "description": "lf_indala_page: Read an Indala card"
+ },
+ "messageReadAnIoproxCard": "Read an ioProx card",
+ "@messageReadAnIoproxCard": {
+ "description": "lf_io_page: Read an ioProx card"
+ },
+ "messageReadAKeriCard": "Read a Keri card",
+ "@messageReadAKeriCard": {
+ "description": "lf_keri_page: Read a Keri card"
+ },
+ "messageGeneral": "General",
+ "@messageGeneral": {
+ "description": "lf_page: General"
+ },
+ "messageAutomaticallyDetectLowFrequencyTags": "Automatically detect low-frequency tags",
+ "@messageAutomaticallyDetectLowFrequencyTags": {
+ "description": "lf_page: Automatically detect low-frequency tags"
+ },
+ "messageReadTheRawLowFrequencySignal": "Read the raw low-frequency signal",
+ "@messageReadTheRawLowFrequencySignal": {
+ "description": "lf_page: Read the raw low-frequency signal"
+ },
+ "messageLfSniff": "LF sniff",
+ "@messageLfSniff": {
+ "description": "lf_page: LF sniff"
+ },
+ "messageCaptureLowFrequencyCommunication": "Capture low-frequency communication",
+ "@messageCaptureLowFrequencyCommunication": {
+ "description": "lf_page: Capture low-frequency communication"
+ },
+ "messageCheckAntennaTuning": "Check antenna tuning",
+ "@messageCheckAntennaTuning": {
+ "description": "lf_page: Check antenna tuning"
+ },
+ "messageReadEm410x": "Read EM410x",
+ "@messageReadEm410x": {
+ "description": "lf_page: Read EM410x"
+ },
+ "messageReadTheEm4100TagId": "Read the EM4100 tag ID",
+ "@messageReadTheEm4100TagId": {
+ "description": "lf_page: Read the EM4100 tag ID"
+ },
+ "messageCloneEm410x": "Clone EM410x",
+ "@messageCloneEm410x": {
+ "description": "lf_page: Clone EM410x"
+ },
+ "messageTagId10HexDigits": "Tag ID (10 hex digits)",
+ "@messageTagId10HexDigits": {
+ "description": "lf_page: Tag ID (10 hex digits)"
+ },
+ "messageForExample0102030405": "For example: 0102030405",
+ "@messageForExample0102030405": {
+ "description": "lf_page: For example: 0102030405"
+ },
+ "messageIdMustContain10HexDigits": "ID must contain 10 hex digits",
+ "@messageIdMustContain10HexDigits": {
+ "description": "lf_page: ID must contain 10 hex digits"
+ },
+ "messageDetectT55xxChipConfiguration": "Detect T55xx chip configuration",
+ "@messageDetectT55xxChipConfiguration": {
+ "description": "lf_page: Detect T55xx chip configuration"
+ },
+ "messageShowT55xxConfiguration": "Show T55xx configuration",
+ "@messageShowT55xxConfiguration": {
+ "description": "lf_page: Show T55xx configuration"
+ },
+ "messageDumpAllT55xxBlocks": "Dump all T55xx blocks",
+ "@messageDumpAllT55xxBlocks": {
+ "description": "lf_page: Dump all T55xx blocks"
+ },
+ "messageReadAPyramidCard": "Read a Pyramid card",
+ "@messageReadAPyramidCard": {
+ "description": "lf_pyramid_page: Read a Pyramid card"
+ },
+ "messageCardType2": "Card type:",
+ "@messageCardType2": {
+ "description": "mifare_page: Card type:"
+ },
+ "messageQuickActions": "Quick actions",
+ "@messageQuickActions": {
+ "description": "mifare_page: Quick actions"
+ },
+ "messageKeyRecoveryAttacks": "Key recovery attacks",
+ "@messageKeyRecoveryAttacks": {
+ "description": "mifare_page: Key recovery attacks"
+ },
+ "messageReadWriteBlocks": "Read / write blocks",
+ "@messageReadWriteBlocks": {
+ "description": "mifare_page: Read / write blocks"
+ },
+ "messageMagicCards": "Magic cards",
+ "@messageMagicCards": {
+ "description": "mifare_page: Magic cards"
+ },
+ "messageDumpRestoreParametersOptional": "Dump / restore parameters (optional)",
+ "@messageDumpRestoreParametersOptional": {
+ "description": "mifare_page: Dump / restore parameters (optional)"
+ },
+ "messageKeyFileKeysOptional": "Key file --keys (optional)",
+ "@messageKeyFileKeysOptional": {
+ "description": "mifare_page: Key file --keys (optional)"
+ },
+ "messageLeaveBlankToUseThePm3Default": "Leave blank to use the PM3 default",
+ "@messageLeaveBlankToUseThePm3Default": {
+ "description": "mifare_page: Leave blank to use the PM3 default"
+ },
+ "messageDumpFileFileOptional": "Dump file --file (optional)",
+ "@messageDumpFileFileOptional": {
+ "description": "mifare_page: Dump file --file (optional)"
+ },
+ "messageDumpOutputFileRestoreInputFile": "dump: output file / restore: input file",
+ "@messageDumpOutputFileRestoreInputFile": {
+ "description": "mifare_page: dump: output file / restore: input file"
+ },
+ "messageSelectKeyFile": "Select key file",
+ "@messageSelectKeyFile": {
+ "description": "mifare_page: Select key file"
+ },
+ "messageSelectDumpFile": "Select dump file",
+ "@messageSelectDumpFile": {
+ "description": "mifare_page: Select dump file"
+ },
+ "messageClearCustomSettings": "Clear custom settings",
+ "@messageClearCustomSettings": {
+ "description": "mifare_page: Clear custom settings"
+ },
+ "messageDefaultSourcesKeyDump": "Default sources: key={value}; dump={value2}",
+ "@messageDefaultSourcesKeyDump": {
+ "description": "mifare_page: Default sources: key={value}; dump={value2}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageDetectCard": "Detect card",
+ "@messageDetectCard": {
+ "description": "mifare_page: Detect card"
+ },
+ "messageScanHf14aTags": "Scan HF 14A tags",
+ "@messageScanHf14aTags": {
+ "description": "mifare_page: Scan HF 14A tags"
+ },
+ "messageCardInformation": "Card information",
+ "@messageCardInformation": {
+ "description": "mifare_page: Card information"
+ },
+ "messageGetDetailedCardInformation": "Get detailed card information",
+ "@messageGetDetailedCardInformation": {
+ "description": "mifare_page: Get detailed card information"
+ },
+ "messageAutomaticKeyRecovery": "Automatic key recovery",
+ "@messageAutomaticKeyRecovery": {
+ "description": "mifare_page: Automatic key recovery"
+ },
+ "messageReadAllSectorsToAFile": "Read all sectors to a file",
+ "@messageReadAllSectorsToAFile": {
+ "description": "mifare_page: Read all sectors to a file"
+ },
+ "messageRestoreCard": "Restore card",
+ "@messageRestoreCard": {
+ "description": "mifare_page: Restore card"
+ },
+ "messageWriteTheDumpBackToTheCard": "Write the dump back to the card",
+ "@messageWriteTheDumpBackToTheCard": {
+ "description": "mifare_page: Write the dump back to the card"
+ },
+ "messageSniff2": "Sniff",
+ "@messageSniff2": {
+ "description": "mifare_page: Sniff"
+ },
+ "messageCaptureCardCommunication": "Capture card communication",
+ "@messageCaptureCardCommunication": {
+ "description": "mifare_page: Capture card communication"
+ },
+ "messageMoreCardRecoveryDiagnosticsSeeCommandReference": "More card recovery / diagnostics (see command reference)",
+ "@messageMoreCardRecoveryDiagnosticsSeeCommandReference": {
+ "description": "mifare_page: More card recovery / diagnostics (see command reference)"
+ },
+ "messageCollectCuid": "Collect CUID",
+ "@messageCollectCuid": {
+ "description": "mifare_page: Collect CUID"
+ },
+ "messagePm3ScriptNameScriptRunName": "PM3 script name (script run )",
+ "@messagePm3ScriptNameScriptRunName": {
+ "description": "mifare_page: PM3 script name (script run )"
+ },
+ "messageForExampleMifareMfAutopwn": "For example: mifare/mf_autopwn",
+ "@messageForExampleMifareMfAutopwn": {
+ "description": "mifare_page: For example: mifare/mf_autopwn"
+ },
+ "messageListScripts": "List scripts",
+ "@messageListScripts": {
+ "description": "mifare_page: List scripts"
+ },
+ "messageRunScript": "Run script",
+ "@messageRunScript": {
+ "description": "mifare_page: Run script"
+ },
+ "messageKnownKey": "Known key",
+ "@messageKnownKey": {
+ "description": "mifare_page: Known key"
+ },
+ "messageTryDefaultKeysOnAllSectors": "Try default keys on all sectors",
+ "@messageTryDefaultKeysOnAllSectors": {
+ "description": "mifare_page: Try default keys on all sectors"
+ },
+ "messageNestedAttack": "Nested attack",
+ "@messageNestedAttack": {
+ "description": "mifare_page: Nested attack"
+ },
+ "messageUseAKnownKeyToFindOther": "Use a known key to find other keys",
+ "@messageUseAKnownKeyToFindOther": {
+ "description": "mifare_page: Use a known key to find other keys"
+ },
+ "messageForCardsWithStaticNonces": "For cards with static nonces",
+ "@messageForCardsWithStaticNonces": {
+ "description": "mifare_page: For cards with static nonces"
+ },
+ "messageAdvancedKeyRecovery": "Advanced key recovery",
+ "@messageAdvancedKeyRecovery": {
+ "description": "mifare_page: Advanced key recovery"
+ },
+ "messageNoKnownKeyRequired": "No known key required",
+ "@messageNoKnownKeyRequired": {
+ "description": "mifare_page: No known key required"
+ },
+ "messageReadDataFromOneBlock": "Read data from one block",
+ "@messageReadDataFromOneBlock": {
+ "description": "mifare_page: Read data from one block"
+ },
+ "messageWriteData": "Write data",
+ "@messageWriteData": {
+ "description": "mifare_page: Write data"
+ },
+ "messageData32HexDigits": "Data (32 hex digits)",
+ "@messageData32HexDigits": {
+ "description": "mifare_page: Data (32 hex digits)"
+ },
+ "messageForExample00000000000000000000000000000000": "For example: 00000000000000000000000000000000",
+ "@messageForExample00000000000000000000000000000000": {
+ "description": "mifare_page: For example: 00000000000000000000000000000000"
+ },
+ "messageMagicCardOperations": "Magic card operations",
+ "@messageMagicCardOperations": {
+ "description": "mifare_page: Magic card operations"
+ },
+ "messageTheseCommandsOnlyApplyToGen1aGen2": "These commands only apply to Gen1A / Gen2 (CUID) magic cards.\nWriting block 0 may damage a non-magic card!",
+ "@messageTheseCommandsOnlyApplyToGen1aGen2": {
+ "description": "mifare_page: These commands only apply to Gen1A / Gen2 (CUID) magic cards.\nWriting block 0 may damage a non-magic card!"
+ },
+ "messageReadBlock0": "Read block 0",
+ "@messageReadBlock0": {
+ "description": "mifare_page: Read block 0"
+ },
+ "messageReadManufacturerBlock": "Read manufacturer block",
+ "@messageReadManufacturerBlock": {
+ "description": "mifare_page: Read manufacturer block"
+ },
+ "messageRestoreFactoryDefaults": "Restore factory defaults",
+ "@messageRestoreFactoryDefaults": {
+ "description": "mifare_page: Restore factory defaults"
+ },
+ "messageEraseCard2": "Erase card?",
+ "@messageEraseCard2": {
+ "description": "mifare_page: Erase card?"
+ },
+ "messageThisWillResetAllBlocksAreYou": "This will reset all blocks. Are you sure?",
+ "@messageThisWillResetAllBlocksAreYou": {
+ "description": "mifare_page: This will reset all blocks. Are you sure?"
+ },
+ "messageLoadEmulator": "Load emulator",
+ "@messageLoadEmulator": {
+ "description": "mifare_page: Load emulator"
+ },
+ "messageLoadADumpIntoTheEmulator": "Load a dump into the emulator",
+ "@messageLoadADumpIntoTheEmulator": {
+ "description": "mifare_page: Load a dump into the emulator"
+ },
+ "messageNdefDecoding": "NDEF decoding",
+ "@messageNdefDecoding": {
+ "description": "nfc_page: NDEF decoding"
+ },
+ "messageListAvailableScripts": "List available scripts",
+ "@messageListAvailableScripts": {
+ "description": "script_page: List available scripts"
+ },
+ "messageScriptName": "Script name",
+ "@messageScriptName": {
+ "description": "script_page: Script name"
+ },
+ "messageForExampleHfMfAutopwn": "For example: hf_mf_autopwn",
+ "@messageForExampleHfMfAutopwn": {
+ "description": "script_page: For example: hf_mf_autopwn"
+ },
+ "messageRun": "Run",
+ "@messageRun": {
+ "description": "script_page: Run"
+ },
+ "messageHint": "Hint: {value}",
+ "@messageHint": {
+ "description": "terminal_page: Hint: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageClearTerminal": "Clear terminal",
+ "@messageClearTerminal": {
+ "description": "terminal_page: Clear terminal"
+ },
+ "messageListDecode": "List / decode",
+ "@messageListDecode": {
+ "description": "trace_page: List / decode"
+ },
+ "messageProtocolType": "Protocol type",
+ "@messageProtocolType": {
+ "description": "trace_page: Protocol type"
+ },
+ "messageDecodeList2": "Decode list",
+ "@messageDecodeList2": {
+ "description": "trace_page: Decode list"
+ },
+ "messageExtract": "Extract",
+ "@messageExtract": {
+ "description": "trace_page: Extract"
+ },
+ "messageExtractTraceData": "Extract trace data",
+ "@messageExtractTraceData": {
+ "description": "trace_page: Extract trace data"
+ },
+ "messageSpacesAreAllowedMeansUnknownClearThe": "Spaces are allowed; ? means unknown. Clear the field to remove the entry.",
+ "@messageSpacesAreAllowedMeansUnknownClearThe": {
+ "description": "command_card_editor: Spaces are allowed; ? means unknown. Clear the field to remove the entry."
+ },
+ "messageSaved": "Saved: {value}",
+ "@messageSaved": {
+ "description": "command_card_editor: Saved: {value}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageCommandResultsAreCollectedHereAutomaticallyClick": "Command results are collected here automatically. Click a key or block to edit it. Select “New card” before changing cards; ? means unknown.",
+ "@messageCommandResultsAreCollectedHereAutomaticallyClick": {
+ "description": "command_card_editor: Command results are collected here automatically. Click a key or block to edit it. Select “New card” before changing cards; ? means unknown."
+ },
+ "messageUidBlocksKeys": "{value} · UID {value2} · Blocks: {value3} · Keys: {value4}",
+ "@messageUidBlocksKeys": {
+ "description": "command_card_editor: {value} · UID {value2} · Blocks: {value3} · Keys: {value4}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ },
+ "value3": {
+ "type": "String"
+ },
+ "value4": {
+ "type": "String"
+ }
+ }
+ },
+ "messageNotIdentified": "Not identified",
+ "@messageNotIdentified": {
+ "description": "command_card_editor: Not identified"
+ },
+ "messageNewCardSelectCapacity": "New card / select capacity",
+ "@messageNewCardSelectCapacity": {
+ "description": "command_card_editor: New card / select capacity"
+ },
+ "messageNewCardClearTheCurrentDraft": "New {value} card (clear the current draft)",
+ "@messageNewCardClearTheCurrentDraft": {
+ "description": "command_card_editor: New {value} card (clear the current draft)",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "messageNewCardCapacity": "New card / capacity",
+ "@messageNewCardCapacity": {
+ "description": "command_card_editor: New card / capacity"
+ },
+ "messageOpenDraft": "Open draft",
+ "@messageOpenDraft": {
+ "description": "command_card_editor: Open draft"
+ },
+ "messageSaveDraft": "Save draft",
+ "@messageSaveDraft": {
+ "description": "command_card_editor: Save draft"
+ },
+ "messageExportKeyDictionary": "Export key dictionary",
+ "@messageExportKeyDictionary": {
+ "description": "command_card_editor: Export key dictionary"
+ },
+ "messageExportKeyBin2": "Export key BIN",
+ "@messageExportKeyBin2": {
+ "description": "command_card_editor: Export key BIN"
+ },
+ "messageReplaceDraftWithTheOpenFile": "Replace draft with the open file",
+ "@messageReplaceDraftWithTheOpenFile": {
+ "description": "command_card_editor: Replace draft with the open file"
+ },
+ "messageExportEml2": "Export EML",
+ "@messageExportEml2": {
+ "description": "command_card_editor: Export EML"
+ },
+ "messageExportBin2": "Export BIN",
+ "@messageExportBin2": {
+ "description": "command_card_editor: Export BIN"
+ },
+ "messageDirectCommand": "Direct command",
+ "@messageDirectCommand": {
+ "description": "command_card_editor: Direct command"
+ },
+ "messageForExampleHfMfChk1k": "For example: hf mf chk --1k",
+ "@messageForExampleHfMfChk1k": {
+ "description": "command_card_editor: For example: hf mf chk --1k"
+ },
+ "messageRunning": "Running",
+ "@messageRunning": {
+ "description": "command_card_editor: Running"
+ },
+ "messageExecute": "Execute",
+ "@messageExecute": {
+ "description": "command_card_editor: Execute"
+ },
+ "messageKeysTrailers": "Keys → trailers",
+ "@messageKeysTrailers": {
+ "description": "command_card_editor: Keys → trailers"
+ },
+ "messageTrailersKeys": "Trailers → keys",
+ "@messageTrailersKeys": {
+ "description": "command_card_editor: Trailers → keys"
+ },
+ "messageNotRead": "Not read",
+ "@messageNotRead": {
+ "description": "command_card_editor: Not read"
+ },
+ "messageSectorKey2": "Sector {value} · Key {value2}",
+ "@messageSectorKey2": {
+ "description": "command_card_editor: Sector {value} · Key {value2}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageBlock3": "Block {value}{value2}",
+ "@messageBlock3": {
+ "description": "command_card_editor: Block {value}{value2}",
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "value2": {
+ "type": "String"
+ }
+ }
+ },
+ "messageTrailer2": " · Trailer",
+ "@messageTrailer2": {
+ "description": "command_card_editor: · Trailer"
+ }
+}
diff --git a/lib/l10n/app_locale_controller.dart b/lib/l10n/app_locale_controller.dart
new file mode 100644
index 0000000..be7ba32
--- /dev/null
+++ b/lib/l10n/app_locale_controller.dart
@@ -0,0 +1,90 @@
+import 'dart:io';
+
+import 'package:flutter/widgets.dart';
+import 'package:pm3gui/services/locale_settings_store.dart';
+import 'app_localizations.dart';
+
+class AppLocaleController extends ChangeNotifier {
+ AppLocaleController({LocaleSettingsRepository? repository})
+ : _repository = repository ?? LocaleSettingsStore();
+
+ final LocaleSettingsRepository _repository;
+ Locale? _locale;
+ Object? _error;
+ bool _changed = false;
+ bool _disposed = false;
+ int _pendingSaves = 0;
+ Future? _initialization;
+ Future _queue = Future.value();
+
+ Locale? get locale => _locale;
+ Object? get error => _error;
+ bool get isSaving => _pendingSaves > 0;
+
+ Future initialize() => _initialization ??= _restore();
+
+ Future _restore() async {
+ try {
+ final languageCode = await _repository.load();
+ if (languageCode != null &&
+ !AppLocalizations.supportedLocales
+ .any((locale) => locale.languageCode == languageCode)) {
+ throw FormatException('Unsupported saved language: $languageCode');
+ }
+ if (!_changed) {
+ _locale = languageCode == null ? null : Locale(languageCode);
+ }
+ } on FileSystemException catch (error) {
+ _error = error;
+ } on FormatException catch (error) {
+ _error = error;
+ }
+ _notify();
+ }
+
+ Future setLocale(Locale? locale) async {
+ if (locale != null && !AppLocalizations.supportedLocales.contains(locale)) {
+ throw ArgumentError.value(locale, 'locale', 'Unsupported app locale');
+ }
+ _changed = true;
+ _locale = locale;
+ _error = null;
+ _pendingSaves++;
+ _notify();
+ final previous = _queue;
+ final saving = () async {
+ await previous;
+ await initialize();
+ try {
+ await _repository.save(locale?.languageCode);
+ _error = null;
+ } on FileSystemException catch (error) {
+ _error = error;
+ } finally {
+ _pendingSaves--;
+ _notify();
+ }
+ }();
+ _queue = saving;
+ await saving;
+ }
+
+ Future flush() async {
+ await initialize();
+ while (true) {
+ final pending = _queue;
+ await pending;
+ if (identical(pending, _queue)) return;
+ }
+ }
+
+ void _notify() {
+ if (!_disposed) notifyListeners();
+ }
+
+ @override
+ void dispose() {
+ _disposed = true;
+ super.dispose();
+ }
+}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
new file mode 100644
index 0000000..e037689
--- /dev/null
+++ b/lib/l10n/app_zh.arb
@@ -0,0 +1,702 @@
+{
+ "@@locale": "zh",
+ "appTitle": "PM3 图形界面",
+ "navConnection": "连接",
+ "navTerminal": "终端",
+ "navDumpViewer": "Dump 查看",
+ "navDumpCompare": "Dump 对比",
+ "navHfSniff": "HF 嗅探/调谐",
+ "navLfGeneral": "LF 通用/EM/T55",
+ "navData": "数据处理",
+ "navScripts": "脚本",
+ "navSettings": "设置",
+ "navCollapse": "收起菜单",
+ "navHighFrequency": "高频 HF",
+ "navLowFrequency": "低频 LF",
+ "navTools": "工具",
+ "connected": "已连接",
+ "disconnected": "未连接",
+ "pm3NotConnected": "未连接 PM3",
+ "pm3Busy": "PM3 命令仍在执行,请等待完成或断开连接",
+ "hexDigits": "{count} 位 hex",
+ "maxCharacters": "最大 {count} 字符",
+ "executingCommand": "执行: {command}",
+ "executeToViewResults": "执行命令查看结果",
+ "copiedToClipboard": "已复制到剪贴板",
+ "copy": "复制",
+ "clear": "清空",
+ "language": "语言",
+ "languageSystem": "跟随系统",
+ "languageHelp": "更改立即生效。系统语言不受支持时使用英语。",
+ "languagePreferenceError": "无法读取或保存语言设置:{error}",
+ "settingsPm3Configuration": "PM3 配置",
+ "settingsPm3Path": "PM3 程序路径",
+ "settingsWindowsPathHint": "官方发行包根目录\\pm3.bat",
+ "settingsWindowsPathHelp": "Windows 推荐选择发行包根目录的 pm3.bat",
+ "settingsPickPm3": "选择 PM3 程序",
+ "settingsArguments": "PM3 启动参数",
+ "settingsWindowsArgumentsHelp": "选择 pm3.bat 时由脚本自行初始化并自动检测设备,此处参数不会传给脚本。每行一个参数;空格、引号和反斜杠按原样保留",
+ "settingsArgumentsHelp": "每行一个参数;空格、引号和反斜杠按原样保留",
+ "settingsWorkingDirectory": "PM3 工作目录(可选)",
+ "settingsSaving": "正在保存设置…",
+ "settingsAbout": "关于 PM3 GUI",
+ "settingsDescription": "基于 Flutter 的 Proxmark3 图形界面",
+ "settingsPlatform": "平台",
+ "settingsArchitecture": "架构",
+ "settingsArchitectureDescription": "CLI Wrapper (兼容未来更新)",
+ "settingsSupportedFormats": "支持格式",
+ "settingsMaintenance": "维护",
+ "terminalClear": "清除终端",
+ "hardwareVersion": "硬件版本",
+ "antennaTune": "天线调谐",
+ "terminalHfSearch": "高频搜索",
+ "terminalLfSearch": "低频搜索",
+ "terminal14aInfo": "14A 信息",
+ "terminalLfRead": "低频读取",
+ "terminalAutopwn": "自动破解",
+ "terminalHint": "提示:{hint}",
+ "terminalInputHint": "输入命令...(Tab/Shift+Tab 循环补全)",
+ "terminalCatalog": "命令库",
+ "terminalSend": "发送",
+ "terminalSearchHint": "搜索命令(如 hf mf, autopwn, info)",
+ "terminalCatalogError": "无法加载命令库:{error}",
+ "messageFailedToTerminateTheProcessTreeExit": "整树终止失败 (exit={value}){value2}",
+ "messageThePm3ProcessManagerHasBeenDisposed": "PM3 进程管理器已释放",
+ "messagePleaseWaitSecondsBeforeRetrying": "请等待 {value} 秒后再试",
+ "messageCoolingDownS": "[请等待冷却: {value}s]",
+ "messagePm3ProgramNotFoundOnWindowsSelect": "找不到 PM3 程序: {value}\nWindows 请优先选择官方发行包根目录的 pm3.bat;Linux 请选择 proxmark3",
+ "messageError": "[错误] {value}",
+ "messageUsingPm3": "[使用 PM3: {value}]",
+ "messageWorkingDirectory": "[工作目录: {value}]",
+ "messageFatalErrorOnPm3Stdout": "PM3 stdout 出现致命错误",
+ "messageFailedToSendTheReadOnlyPm3": "PM3 BAT 只读握手写入失败: {value}",
+ "messagePm3StdoutStreamError": "PM3 stdout 流错误: {value}",
+ "messageFatalErrorOnPm3Stderr": "PM3 stderr 出现致命错误",
+ "messagePm3StderrStreamError": "PM3 stderr 流错误: {value}",
+ "messagePm3ProcessExitedCode": "[PM3 进程退出, code={value}]",
+ "messagePm3ProcessExitedBeforeTheConnectionPrompt": "PM3 进程在连接提示前退出 (code={value})",
+ "messagePm3ProcessExitStatusError": "PM3 进程退出状态错误: {value}",
+ "messageConnectionTimedOutSCheckThatThe": "连接超时 ({value}s),请检查设备是否已连接",
+ "messageConnectionTimedOut": "[连接超时]",
+ "messageCouldNotStartPm3PathCheckThe": "无法启动 PM3 程序: {value}\n路径: {value2}\n请检查路径是否正确,程序是否已编译",
+ "messageConnectionFailed": "连接失败: {value}",
+ "messageNotConnected": "[未连接]",
+ "messageErrorCommandTimedOutTheSessionWas": "[错误] 命令超时,会话已停止",
+ "messageThePm3SessionDisconnectedBeforeTheCommand": "PM3 会话在命令完成前断开",
+ "messageFailedWhileWaitingForPm3ToStart": "等待 PM3 启动失败: {value}",
+ "messageFailedToSendQuit": "发送 quit 失败: {value}",
+ "messageCouldNotReadTheExitStatusAfter": "PM3 {value} 流关闭后读取退出状态失败: {value2}",
+ "messagePm3StreamClosed": "PM3 {value} 流已关闭",
+ "messageFailedWhileWaitingForPm3ToExit": "等待 PM3 退出失败: {value}",
+ "messageFailedToTerminatePm3TermProcessTree": "终止 PM3 进程失败: TERM/整树终止未成功",
+ "messageFailedToForceTerminatePm3KillProcess": "强制终止 PM3 进程失败: KILL/整树终止未成功",
+ "messageTimedOutWaitingForPm3ToExit": "终止 PM3 后等待退出超时",
+ "messageFailedToTerminatePm3ReturnedFalse": "终止 PM3 进程失败: {value} 返回 false",
+ "messageFailedToTerminatePm3": "终止 PM3 进程失败: {value}",
+ "messageTheSerialPortIsInUseBy": "串口 已被其他进程占用\n请关闭其他使用该端口的程序(如另一个 pm3 终端)",
+ "messageSerialDeviceNotFoundCheckThePm3": "串口设备不存在,请检查 PM3 是否已通过 USB 连接",
+ "messagePermissionDeniedForTheSerialPortTry": "串口权限不足\n请尝试: sudo chmod 666 /dev/ttyACM0\n或将用户加入 dialout 组",
+ "messageSerialConnectionError": "串口连接错误: {value}",
+ "messageEnterHexDigitsUseForUnknownDigits": "需要 {value} 位十六进制字符;未知位用 ?,清空表示未读取",
+ "messageBlockNumberExceedsTheCardCapacity": "块号超出卡容量",
+ "messageSectorNumberExceedsTheCardCapacity": "扇区号超出卡容量",
+ "messageThisIsNotACommandDataDraft": "不是命令数据草稿",
+ "messageUnsupportedCardCapacity": "不支持的卡容量",
+ "messageInvalidUidFormat": "UID 格式不正确",
+ "messageBlockIsIncompleteSaveADraftOr": "块 {value} 尚未完整,先保存草稿或补齐数据",
+ "messageSectorHasIncompleteKeysExportADictionary": "扇区 {value} 密钥不完整,可先导出字典或保存草稿",
+ "messageThereAreNoCompleteKeysToExport": "没有完整密钥可导出",
+ "messageEmptyUnallocated": "空/未分配",
+ "messageDefectManagement": "缺陷管理",
+ "messageReserved": "备用",
+ "messageAdditionalDirectoryEntries": "额外的目录条目",
+ "messageCardholderInformation": "卡持有者信息",
+ "messageUnallocated": "未分配",
+ "messageAirlineServices": "航空服务",
+ "messageUrbanTransport": "城市交通",
+ "messageUrbanTransport2": "城市交通 (2)",
+ "messageRailTransport": "铁路交通",
+ "messageTransportGeneral": "运输 (通用)",
+ "messageSecurityServices": "安防方案",
+ "messageUrbanTransport3": "城市交通 (3)",
+ "messageEventTicketing": "事件票务",
+ "messageLoyaltySchemes": "忠诚方案",
+ "messageParking": "停车场",
+ "messageAccessControl": "门禁控制",
+ "messageElectronicPurseMicropayments": "电子钱包 / 小额支付",
+ "messageElectronicTicketingGeneral": "电子票务 (通用)",
+ "messageMobileOffice": "移动办公",
+ "messageIdentification": "身份识别",
+ "messageMobilePayments": "手机支付",
+ "messageFelicaIntegration": "FeliCa集成",
+ "messageFuelServices": "加油",
+ "messageInformationProcessing": "信息处理",
+ "messageMifareClassic1kCompatible": "MIFARE Classic 1K (兼容)",
+ "messageMifareClassic1kCompatible2": "MIFARE Classic 1K (兼容2)",
+ "messageUnknownSak": "未知 (SAK={value})",
+ "messageUnknown0x": "未知 (0x{value})",
+ "messageApplication0x": "应用 0x{value}",
+ "messageNxpFactoryDefault": "NXP 出厂默认",
+ "messageAllZeros": "全零",
+ "messageInvalidKeyTextFormat": "密钥文本格式错误",
+ "messageDuplicateSector": "扇区 {value} 重复",
+ "messageKeyTextIsMissingSectors": "密钥文本缺少扇区",
+ "messageBinaryDumpBin": "二进制转储 (.bin)",
+ "messageTextDumpEml": "文本转储 (.eml)",
+ "messageBinaryKeysKeyBin": "二进制密钥 (.key.bin)",
+ "messageKeyDictionaryDic": "密钥字典 (.dic)",
+ "messageKeyListKeysTxt": "密钥列表 (.keys.txt)",
+ "messageFileNotFound": "文件不存在: {value}",
+ "messageAKeyFileCannotBeConvertedTo": "密钥文件无法转换为完整dump(缺少数据块)",
+ "messageAKeyFileCannotBeConvertedTo2": "密钥文件无法转换为EML(缺少数据块)",
+ "messageAKeyFileCannotBeConvertedTo3": "密钥文件无法转换为JSON(缺少数据块)",
+ "messageNoKeyData": "无密钥数据",
+ "messageConversionFailed": "转换失败: {value}",
+ "messageUniqueKeys": "{value} 个唯一密钥",
+ "messageUnsupportedKeyExportFormat": "不支持的密钥导出格式: {value}",
+ "messageSectorsUniqueKeyAUniqueKeyB": "{value} 扇区, {value2} 种 KeyA, {value3} 种 KeyB, 共 {value4} 种唯一密钥",
+ "messageConnectionLostOperationAborted": "连接断开,操作中止",
+ "messageWritingBlock": "正在写入 块 {value} ...",
+ "messageAuthenticationOrWritingFailed": "认证失败或写入失败",
+ "messageWriteSuccessful": "写入成功",
+ "messageFinishedSucceededFailed": "完成: {value} 成功, {value2} 失败",
+ "messageCancelled": "已取消",
+ "messageCouldNotSaveSettings": "设置保存失败: {value}",
+ "messageFileScanFailed": "文件扫描失败: {value}",
+ "messageInstalled": "已安装",
+ "messageOnWindowsUseTheOfficialPm3Bat": "Windows 推荐使用官方 pm3.bat",
+ "messageSerialPort": "串口选择",
+ "messageRefreshPorts": "刷新端口",
+ "messageNoSerialPortFoundConnectYourPm3": "未找到串口,请连接 PM3 设备",
+ "messageTheOfficialBatLauncherDetectsDevicesAutomatically": "官方 BAT 会自动检测设备;此端口值仅保留于设置中",
+ "messageConnecting": "连接中...",
+ "messageDisconnect": "断开连接",
+ "messageConnectPm3": "连接 PM3",
+ "messageDeviceInformation": "设备信息",
+ "messageRefreshHardwareInformation": "刷新硬件信息",
+ "messagePort": "端口",
+ "messageVersion": "版本",
+ "messageStatus": "状态",
+ "messageDeviceModel": "设备型号",
+ "messageFirmware": "固件",
+ "messageSmartcardModule": "智能卡模块",
+ "messageDeviceId": "设备 ID",
+ "messageRetrievingHardwareInformation": "正在获取硬件信息...",
+ "messageCommandHistory": "命令历史",
+ "messageCommands": "{value} 条",
+ "messageTerminalBuffer": "终端缓冲",
+ "messageLines": "{value} 行",
+ "messageHardwareStatus": "硬件状态",
+ "messageDebugLevel": "调试级别",
+ "messageEnvironment": "环境信息",
+ "messagePm3Path": "PM3 路径",
+ "messageValid": "✅ 有效",
+ "messageInvalid": "❌ 无效",
+ "messagePm3FileCollection": "PM3 文件收集",
+ "messageFilesCards": "{value} 个文件, {value2} 张卡",
+ "messageScan": "扫描",
+ "messageOrganizeFiles": "归类整理",
+ "messageNoPm3ExportFilesFound": "未发现 PM3 导出文件",
+ "messageFilesAreCollectedAutomaticallyAfterConnectingAnd": "连接设备并执行 dump/autopwn 后自动收集",
+ "messagePm3Connected": "PM3 已连接",
+ "messagePm3NotConnected": "PM3 未连接",
+ "messageSelectASerialPortAndConnectThe": "请选择串口并连接设备",
+ "messageConnectionFailed2": "连接失败",
+ "messageDumpsKeyFilesFiles": "{value} 份转储 • {value2} 份密钥 • {value3} 个文件",
+ "messageOpenInDumpViewer": "在 Dump 查看器中打开",
+ "messageOpenedInDumpViewer": "已切换到 Dump 查看器: {value}",
+ "messageOrganizePm3Files": "归类整理 PM3 文件",
+ "messageOrganizeCollectedPm3ExportFilesIntoSubdirectories": "将收集到的 PM3 导出文件按卡片 UID 分类整理到子目录中。\n\n目录结构:\n 📁 hf-mf/A991A280/\n ├── hf-mf-A991A280-dump.bin\n ├── hf-mf-A991A280-key.bin\n └── ...\n",
+ "messageDestinationDirectory": "目标目录",
+ "messageFilesToOrganize": "共 {value} 个文件将被整理",
+ "messageCancel": "取消",
+ "messageFilesOrganized": "已整理 {value} 个文件",
+ "messageStartOrganizing": "开始整理",
+ "messageFlashUsage": "Flash 用量",
+ "messageBasic": "基础",
+ "messageAnalysis": "分析",
+ "messageCompare": "比较",
+ "messagePlot": "绘图",
+ "messageDisplayTheDataGraph": "显示数据图表",
+ "messageClear": "清除",
+ "messageClearTheGraphBuffer": "清除绘图缓冲区",
+ "messageDetectClock": "检测时钟",
+ "messageAutomaticallyDetectTheClockRate": "自动检测时钟速率",
+ "messageSampling": "采样",
+ "messageSampleCount": "采样数",
+ "messageReadSamples": "读取采样",
+ "messageFileOperations": "文件操作",
+ "messageFilePath": "文件路径",
+ "messageSave": "保存",
+ "messageLoad": "加载",
+ "messageAsn1Decoding": "ASN.1 解码",
+ "messageHexData": "Hex 数据",
+ "messageDecode": "解码",
+ "messageFileComparison": "文件比较",
+ "messageFileA": "文件 A",
+ "messageFileB": "文件 B",
+ "messageOpenFileA": "打开文件 A",
+ "messageOpenFileB": "打开文件 B",
+ "messageDifferencesOnly": "仅差异",
+ "messageFileAError": "文件 A 错误: {value}",
+ "messageFileBError": "文件 B 错误: {value}",
+ "messageOpenTwoDumpFilesToCompareThem": "分别打开两个转储文件进行对比",
+ "messageSupportsEmlBinJsonDump": "支持 .eml / .bin / .json / .dump",
+ "messageTotalBlocks": "总块数",
+ "messageDataDifferences": "数据差异",
+ "messageBlocks": "{value} 块",
+ "messageKeyADifferences": "Key A 差异",
+ "messageKeyBDifferences": "Key B 差异",
+ "messageNone": "(无)",
+ "messageManufacturer": "制造商",
+ "messageTrailer": "尾块",
+ "messageCouldNotOpenFile": "打开失败: {value}",
+ "messageDictionaryFileUniqueKeys": "字典文件: {value} 个唯一密钥",
+ "messageDictionaryLoadedKeys": "加载字典: {value} 个密钥",
+ "messageMergedKeysForSectorsBlockDataUnchanged": "已合并 {value} 个扇区的密钥(块数据保持不变)",
+ "messageKeyHandling": "密钥处理",
+ "messageKeyDataIsAlreadyLoadedHowShould": "当前已有密钥数据。加载新的转储文件时,如何处理密钥?",
+ "messageDumpKeyFieldsAreOftenFfWhile": "转储文件的密钥区通常为 FF,而实际密钥可能来自单独的 key 文件",
+ "messageKeepCurrentKeys": "保留当前密钥",
+ "messageReplaceWithDumpKeys": "用转储覆盖",
+ "messageDefaultKeyFileSet": "已设为默认密钥文件: {value}",
+ "messageDefaultDumpFileSet": "已设为默认转储文件: {value}",
+ "messageSectorView": "扇区视图",
+ "messageKeysEdit": "密钥/编辑",
+ "messageDetailedAnalysis": "深度分析",
+ "messageWriteClear": "回写/清空",
+ "messageCommandData": "命令数据",
+ "messageOpenADumpFileToViewSector": "打开转储文件查看扇区数据",
+ "messageOpenADumpOrKeyFileTo": "打开转储文件或密钥文件以编辑",
+ "messageOpenADumpFileToViewDetailed": "打开转储文件后可查看深度分析",
+ "messageSupportsEmlBinJsonDumpDic": "支持 .eml / .bin / .json / .dump / .dic",
+ "messageOpenFile": "打开文件",
+ "messageSetAsDefaultKeys": "设为默认密钥",
+ "messageSetAsDefaultDump": "设为默认转储",
+ "messageExportAs": "导出为",
+ "messageEmlTextDump": ".eml (文本转储)",
+ "messageBinBinaryDump": ".bin (二进制转储)",
+ "messageJsonPm3Format": ".json (PM3 格式)",
+ "messageKeyBinBinaryKeys": ".key.bin (二进制密钥)",
+ "messageDicKeyDictionary": ".dic (密钥字典)",
+ "messageKeysTxtKeyList": ".keys.txt (密钥列表)",
+ "messageExportFailed": "导出失败: {value}",
+ "messageExportedAndOrganized": "已自动归类导出: {value}",
+ "messageExported": "已导出: {value}",
+ "messageNoCardData": "无卡片数据",
+ "messageSectorBlocks": "扩区 {value} (块 {value2}–{value3})",
+ "messageAccessConditions": "访问控制条件",
+ "messageManufacturerBlock": "制造商块",
+ "messageBlock": "块",
+ "messageDescription": "说明",
+ "messageData": "数据 {value}",
+ "messageKeyAWriteKeyAAccessBits": "KeyA: 写 KeyA|访控|KeyB",
+ "messageKeyAWriteKeyAAccessBits2": "KeyA: 写 KeyA|访控; KeyB: 读写",
+ "messageReadAccessBitsOnly": "仅可读访控",
+ "messageKeyAWriteAccessBitsKeyB": "KeyA: 写访控; KeyB: 读写",
+ "messageKeyBWriteAll": "KeyB: 写全部",
+ "messageLocked": "已锁定",
+ "messageUnknown": "未知",
+ "messageReadWriteWithKeyAOrB": "读写 Key A 或 B",
+ "messageReadWithABNoWriting": "可读 A/B,不可写",
+ "messageReadWriteWithKeyBOnly": "读写仅 Key B",
+ "messageReadABWriteBDecrementA": "读: A/B,写: B,减: A/B (值块)",
+ "messageReadWithKeyBOnly": "仅 Key B 可读",
+ "messageReadABWriteIncrementBDecrement": "读: A/B,写/加: B,减: A/B (值块)",
+ "messageNoData": "无数据",
+ "messageOverview": "📋 概览",
+ "messageCardType": "卡片类型",
+ "messageEmptyDataBlocks": "空白数据块",
+ "messageDataUsage": "数据使用率",
+ "messageValueBlocks": "值块数",
+ "messageManufacturerBlockBlock0": "🏭 制造商块 (Block 0)",
+ "messageChipType": "芯片类型",
+ "messageRawData": "原始数据",
+ "messageKeyAnalysis": "🔑 密钥分析",
+ "messageTotalSectors": "扇区总数",
+ "messageKnownKeyA": "已知 Key A",
+ "messageKnownKeyB": "已知 Key B",
+ "messageAllKeysIdentical": "所有密钥相同",
+ "messageYes": "是",
+ "messageNo": "否",
+ "messageContainsBlankKeys": "存在空白密钥",
+ "messageYes2": "是 ⚠️",
+ "messageKeyAGroups": "Key A 分组:",
+ "messageSectors": "{value} → 扇区 {value2}",
+ "messageKeyBGroups": "Key B 分组:",
+ "messageDefaultKeyMatches": "⚠️ 默认密钥匹配:",
+ "messageSectorKey": "扇区 {value} Key{value2}: {value3} ({value4})",
+ "messageMadApplicationDirectoryV": "📂 MAD 应用目录 (v{value})",
+ "messageSector": "扇区 {value}",
+ "messageValueBlocks2": "💰 值块",
+ "messageBlock2": "块 {value}",
+ "messageValueAddress": "值: {value} 地址: {value2}",
+ "messageSectorOverview": "📊 扇区概览",
+ "messageValidAccessBits": "✅ 访控有效",
+ "messageInvalidAccessBits": "❌ 访控无效",
+ "messageNoAccessBits": "- 无访控",
+ "messageDataBlockAsciiView": "📝 数据块 ASCII 视图",
+ "messageExportAllFormats": "📦 一键导出全部格式",
+ "messageExportTheCurrentDumpInAllSupported": "从当前打开的转储文件导出为所有支持的格式:",
+ "messageExportEml": "导出 .eml",
+ "messageExportBin": "导出 .bin",
+ "messageExportJson": "导出 .json",
+ "messageExportKeyBin": "导出 .key.bin",
+ "messageExportDic": "导出 .dic",
+ "messageExportKeysTxt": "导出 .keys.txt",
+ "messageKeyExtraction": "🔑 密钥提取",
+ "messageAllKeysExtractedFromTheCurrentDump": "从当前转储中提取的所有密钥:",
+ "messageCopyAllKeys": "复制全部密钥",
+ "messageKeyAOnly": "仅 Key A",
+ "messageKeyBOnly": "仅 Key B",
+ "messageFileFormatConversion": "🔄 文件格式转换",
+ "messageSelectADumpOrKeyFileTo": "选择任意dump/密钥文件, 转换为其他格式:",
+ "messageSelectSourceFile": "选择源文件",
+ "messageTargetFormat": "目标格式:",
+ "messageConverting": "转换中...",
+ "messageStartConversion": "开始转换",
+ "messageExternalKeyFile": "📋 外部密钥文件",
+ "messageLoadedExternalKeys": "已加载的外部密钥:",
+ "messageFormatReference": "ℹ️ 格式说明",
+ "messageRawBinaryDump16BytesPerBlock": "原始二进制转储 (16字节/块), PM3默认dump格式",
+ "messageTextDump32HexDigitsPerLine": "文本转储 (每行32位十六进制), 用于模拟器加载",
+ "messagePm3JsonFormatIncludingCardMetadataAnd": "PM3 JSON格式, 含卡片元数据和密钥",
+ "messageBinaryKeyFile6ByteKeyA": "二进制密钥文件 (6字节KeyA + 6字节KeyB) × 扇区数",
+ "messageKeyDictionaryOneKeyPerLineUsed": "密钥字典 (每行一个密钥), 用于暴力破解",
+ "messageReadableKeyTableSectorNumberKeyA": "可读密钥表 (扇区号 + KeyA + KeyB)",
+ "messageSector2": "扇区",
+ "messageSectorAB": "扇区 {value} A:{value2} B:{value3}",
+ "messageSaveConvertedFile": "保存转换文件",
+ "messageConversionSuccessfulOutput": "✅ 转换成功!\n输出: {value}\n{value2}",
+ "messageConversionError": "❌ 转换异常: {value}",
+ "messageEnterHexDigits": "需要 {value} 位十六进制字符",
+ "messageChangesNotApplied": "修改未应用:{value}",
+ "messageKeyEditor": "🔑 密钥编辑",
+ "messageApplyChanges": "应用修改",
+ "messageAllDefaults": "全部默认",
+ "messageBlockDataEditor": "📝 块数据编辑",
+ "messageData2": "数据",
+ "messageOpenADumpFileFirst": "请先打开转储文件",
+ "messageCuidCardWritingAndClearing": "CUID 卡回写 & 清空",
+ "messageCuidGen2CardsHaveNoBackdoorCommands": "CUID(Gen2)卡没有后门命令,必须使用已知密钥认证后逐块写入。\n• 整卡清空:使用卡片当前密钥认证,将数据块写为全 0\n• 整卡回写:使用目标卡密钥认证,将 Dump 数据逐块写入\n• 回写扇区:仅写入当前选中的单个扇区\n• Gen1A 后门恢复:使用 hf mf restore 后门命令整卡恢复(仅 Gen1A 卡)\n\n⚠️ CUID 卡写入块 0 会自动加 --force 参数",
+ "messageConnectYourPm3DeviceFirst": "请先连接 PM3 设备",
+ "messageWriteOptions": "⚙️ 写入选项",
+ "messageAuthenticationKeyType": "认证密钥类型:",
+ "messageSkipBlock0ManufacturerBlock": "跳过块 0 (制造商块)",
+ "messageBlock0CannotBeWrittenUnlessThis": "除非是魔术卡,否则块 0 无法写入",
+ "messageWriteTrailersKeysAccessBits": "写入尾块 (密钥+访问控制)",
+ "messageWriteTheDumpTrailerDataToThe": "将 Dump 中的尾块数据写入目标卡",
+ "messageDumpKeysForAuthentication": "🔑 Dump 密钥 (用于认证)",
+ "messageSmartWrite": "🚀 一键智能回写",
+ "messageSelectTheBestWritePathForThe": "按目标卡类型选择最佳回写路径(可用于 UID/CUID/FUID/Gen1A/Gen3/Gen4)。",
+ "messageTargetCardTypeMode": "目标卡类型/模式",
+ "messageAutoDefault": "AUTO (默认)",
+ "messageStandardUidCard": "UID 普通卡",
+ "messageFuidUidCanBeChangedOnce": "FUID(一次性 UID 可改)",
+ "messageGen1aBackdoorCard": "Gen1A 后门卡",
+ "messageGen3MagicCard": "Gen3 魔术卡",
+ "messageGen4PasswordOptional": "Gen4 密码(可选)",
+ "message8HexDigitsEG00000000": "8 hex,例如 00000000",
+ "messageSmartWrite2": "⚡ 一键智能回写",
+ "messageAuthenticateWithTheDumpKeysAndZero": "使用 Dump 中的密钥认证,将所有数据块写为全 0\n尾块恢复为默认密钥和访问控制位\n适用于 CUID / Gen2 卡(无后门)",
+ "messageClearEntireCuidCard": "CUID 整卡清空",
+ "messageAuthenticateWithTheTargetCardKeysAnd": "使用目标卡密钥认证,将 Dump 数据逐块写入\n适用于 CUID / Gen2 卡(无后门)\n需要已知目标卡的密钥才能写入",
+ "messageWriteEntireCuidCard": "CUID 整卡回写",
+ "messageWriteOnlySelectedSectorAuthenticateWithThe": "仅回写当前选中的扇区 {value}\n使用 Dump 密钥认证,逐块写入该扇区\n适合修改单个扇区后的局部更新",
+ "messageWriteSector": "回写扇区 {value}",
+ "messageRestoreTheEntireCardUsingThePm3": "使用 PM3 的 hf mf restore 命令整卡恢复\n仅适用于 Gen1A 后门卡,无需密钥\n通过 0x40/0x43 后门指令直接写入所有块",
+ "messageGen1aBackdoorRestore": "Gen1A 后门恢复",
+ "messageWriteProgress": "📊 写入进度",
+ "messageFormatConversionAndKeyExtraction": "📦 格式转换 & 密钥提取",
+ "messageEditedDataIsIncomplete": "编辑数据不完整",
+ "messageNoValidUidFoundCannotWriteUid": "未找到有效 UID,无法执行 UID 写入",
+ "messageClearCuidBlockByBlock": "CUID 逐块清空",
+ "messageAuthenticateTheTargetCardWithTheDump": "将使用 Dump 中的密钥对目标卡进行认证,\n然后将每个数据块写为全 0。\n\n尾块将被重置为默认值:\nKey A = FFFFFFFFFFFF\n访问控制 = FF078069\nKey B = FFFFFFFFFFFF\n\n⚠️ 此操作不可逆!",
+ "messageBlocksToClear": "将清空 {value} 块 ({value2})",
+ "messageSkipBlock0": "跳过块0",
+ "messageIncludeBlock0": "含块0",
+ "messageStartClearing": "开始清空",
+ "messageWriteCuidBlockByBlock": "CUID 逐块回写",
+ "messageAuthenticateTheTargetCardWithTheDump2": "将使用 Dump 中的密钥对目标卡进行认证,\n然后将 Dump 数据逐块写入目标卡。\n\n✅ 数据块: 写入 Dump 中的原始数据\n✅ 尾块: 写入 Dump 中的完整尾块数据\n (包含 Key A + 访问控制 + Key B)\n\n💡 使用场景: CUID/普通卡的密钥已知,\n 需要覆盖写入新数据。",
+ "messageBlocksToWrite": "将写入 {value} 块 ({value2}, {value3})",
+ "messageIncludeTrailers": "含尾块",
+ "messageSkipTrailers": "跳过尾块",
+ "messageStartWriting": "开始回写",
+ "messageAuthenticateWithTheDumpKeysThenWrite": "将使用 Dump 中的密钥认证,\n回写扇区 {value} 的所有块到目标卡。\n\n{value2} 认证密钥: Key {value3}",
+ "messageIncludeTrailers2": "包含尾块",
+ "messageExcludeTrailers": "不含尾块",
+ "messageInformation": "信息",
+ "messageReadWrite": "读写",
+ "messageReader": "读取器",
+ "messageReadA14443BCard": "读取 14443-B 卡片",
+ "messageTagInformation": "标签信息",
+ "messageReadTagDetails": "读取标签详情",
+ "messageSniff": "嗅探",
+ "messageCapture14443BCommunication": "捕获 14443-B 通信",
+ "messageDump": "转储",
+ "messageReadAllData": "读取全部数据",
+ "messageReadNdefData": "读取 NDEF 数据",
+ "messageBlockOperations": "块操作",
+ "messageBlockNumber": "块号",
+ "messageReadBlock": "读取块",
+ "messageWriteDataHex": "写入数据 (hex)",
+ "messageWriteBlock": "写入块",
+ "messageRawCommand": "裸命令",
+ "messageRawHexData": "原始 hex 数据",
+ "messageSimulateCard": "模拟卡片",
+ "messageSimulateA14443BTag": "模拟 14443-B 标签",
+ "messageAutomaticallyReadIso15693": "自动读取 ISO15693",
+ "messageReadBasicTagInformation": "读取标签基本信息",
+ "messageDumpCard": "转储卡片",
+ "messageEraseCard": "擦除卡片",
+ "messageClearCardData": "清空卡片数据",
+ "messageConfirmErase": "确认擦除",
+ "messageThisOperationCannotBeUndone": "此操作不可恢复!",
+ "messageWriteData4ByteHex": "写入数据 (4 字节 hex)",
+ "messageFindAfi": "查找 AFI",
+ "messageScanApplicationFamilyIdentifiers": "扫描应用族标识符",
+ "messageSimulateAnIso15693Tag": "模拟 ISO15693 标签",
+ "messageSniffCommunication": "嗅探通信",
+ "messageCaptureReaderCommunication": "捕获读卡器通信",
+ "messageSetUid": "设置 UID",
+ "messageUid8ByteHex": "UID (8 字节 hex)",
+ "messageConfirm": "确认",
+ "messageSearch": "搜索",
+ "messageSearchForEmvCards": "搜索 EMV 卡片",
+ "messageSelectPpseApplication": "选择 PPSE 应用",
+ "messageExecuteTransaction": "执行交易",
+ "messageSimulateTheEmvTransactionFlow": "模拟 EMV 交易流程",
+ "messageTest": "测试",
+ "messageEmvSelfTest": "EMV 自检",
+ "messageReadAFelicaCard": "读取 FeliCa 卡片",
+ "messageGetFelicaDetails": "获取 FeliCa 详情",
+ "messageDumpCardData": "转储卡片数据",
+ "messageLiteDump": "Lite 转储",
+ "messageDumpFelicaLite": "转储 FeliCa Lite",
+ "messageCaptureFelicaCommunication": "捕获 FeliCa 通信",
+ "messageScServiceCode": "SC (服务代码)",
+ "messageBlBlockNumber": "BL (块号)",
+ "messageReadFidoInformation": "读取 FIDO 信息",
+ "messageRegister": "注册",
+ "messageFido2Registration": "FIDO2 注册",
+ "messageAuthenticate": "认证",
+ "messageFido2Authentication": "FIDO2 认证",
+ "messageList": "列表",
+ "messageListFidoCommunication": "列出 FIDO 通信",
+ "messageKeyRecovery": "破解",
+ "messageEmulator": "模拟器",
+ "messageGetInformation": "获取信息",
+ "messageReadAnIclassTag": "读取 iCLASS 标签",
+ "messageReadCard": "读取卡片",
+ "messageReaderMode": "reader 模式",
+ "messageDumpAllBlocksToAFile": "转储全部块到文件",
+ "messageKey8ByteHex": "密钥 (8 字节 hex)",
+ "messageWriteData8ByteHex": "写入数据 (8 字节 hex)",
+ "messageCheckKeys": "检查密钥",
+ "messageCheckCommonDefaultKeys": "检查常用默认密钥",
+ "messageLoclassAttack": "Loclass 攻击",
+ "messageRecoverIclassKeysOffline": "离线破解 iCLASS 密钥",
+ "messageCaptureCardReaderCommunication": "捕获卡片与读卡器通信",
+ "messageEmulatorOperations": "模拟器操作",
+ "messageFilePathOptional": "文件路径(可选)",
+ "messageLeaveBlankToUseTheDefault": "留空使用默认",
+ "messageLoadIntoEmulator": "加载到模拟器",
+ "messageView": "查看",
+ "messageSimulate": "模拟",
+ "messageReadALegicTag": "读取 Legic 标签",
+ "messageDumpAllData": "转储全部数据",
+ "messageErase": "擦除",
+ "messageSimulateALegicTag": "模拟 Legic 标签",
+ "messageReadWriteOperations": "读写操作",
+ "messageOffset": "偏移",
+ "messageLength": "长度",
+ "messageRead": "读取",
+ "messageWrite": "写入",
+ "messageApply": "应用",
+ "messageFiles": "文件",
+ "messageManagement": "管理",
+ "messageDetect": "检测",
+ "messageDetectADesfireCard": "检测 DESFire 卡片",
+ "messageReadBasicCardInformation": "读取卡片基本信息",
+ "messageGetUid": "获取 UID",
+ "messageGetCardUid": "获取卡片 UID",
+ "messageFreeMemory": "空闲内存",
+ "messageViewAvailableMemory": "查看可用内存",
+ "messageCheckDefaultKeys": "检查默认密钥",
+ "messageViewApplicationDirectory": "查看应用目录",
+ "messageListAids": "列出 AID",
+ "messageListApplications": "列出应用",
+ "messageSelectApplication": "选择应用",
+ "messageAid3ByteHex": "AID (3 字节 hex)",
+ "messageSelect": "选择",
+ "messageListFiles": "列出文件",
+ "messageFileDetails": "文件详情",
+ "messageDumpApplication": "转储应用",
+ "messageFileReadWrite": "文件读写",
+ "messageKeyNumber": "密钥号",
+ "messageAlgorithm": "算法",
+ "messageKeyHex": "密钥 (hex)",
+ "messageKeySettings": "密钥设置",
+ "messageGetKeySettings": "获取密钥设置信息",
+ "messageFormatPicc": "格式化 PICC",
+ "messageDeleteAllApplicationsAndData": "删除所有应用和数据",
+ "messageConfirmFormat": "确认格式化",
+ "messageThisWillDeleteAllApplicationsAndData": "此操作将删除所有应用和数据,不可恢复!",
+ "messageReadAnMfuNtagTag": "读取 MFU/NTAG 标签",
+ "messageReadAllDataToAFile": "读取全部数据到文件",
+ "messagePasswordAuthentication": "密码认证",
+ "messagePassword4ByteHex": "密码 (4 字节 hex)",
+ "messageReadNdef": "读取 NDEF",
+ "messageReadNfcDataExchangeFormat": "读取 NFC 数据交换格式",
+ "messageSaveEmulator": "保存模拟器",
+ "messageGenerateKeys": "生成密钥",
+ "messageKeygenDeriveKeysFromUid": "keygen — 根据 UID 推导密钥",
+ "messageDerivePassword": "密码推导",
+ "messagePwdgenDeriveTheNtagPassword": "pwdgen — 推导 NTAG 密码",
+ "messageKeyCheck": "密钥检查",
+ "messageCchkCheckCommonKeys": "cchk — 检查常用密钥",
+ "messageUid7ByteHex": "UID (7 字节 hex)",
+ "messageReadSeosInformation": "读取 SEOS 信息",
+ "messageReadPacsData": "读取 PACS 数据",
+ "messageSimulateASeosCard": "模拟 SEOS 卡片",
+ "messageHfSearch": "HF 搜索",
+ "messageSearchForHighFrequencyTags": "搜索高频标签",
+ "messageHfSniff": "HF 嗅探",
+ "messageCaptureHighFrequencyCommunication": "捕获高频通信",
+ "messageHfTuning": "HF 调谐",
+ "messageTuneTheHfAntenna": "调谐高频天线",
+ "messageProtocolList": "协议列表",
+ "messageProtocol": "协议",
+ "messageDecodeList": "列表解码",
+ "messageReadAnAwidCard": "读取 AWID 卡片",
+ "messageCloneSimulate": "克隆 / 模拟",
+ "messageFcFacilityCode": "FC (设施码)",
+ "messageCnCardNumber": "CN (卡号)",
+ "messageClone": "克隆",
+ "messageReadAnFdxBTag": "读取 FDX-B 标签",
+ "messageRawData2": "RAW 数据",
+ "messageCloneToT55xx": "克隆到 T55xx",
+ "messageCloneSimulate2": "克隆/模拟",
+ "messageReadHidCard": "读取 HID 卡",
+ "messageReadAHidProxCard": "读取 HID Prox 卡片",
+ "messageDemodulateSignal": "解调信号",
+ "messageDemodulateTheHidLfSignal": "解调 HID 低频信号",
+ "messageWiegandData": "Wiegand 数据",
+ "messageForExample2006ec0c86": "例如: 2006ec0c86",
+ "messageBruteForceRecovery": "暴力破解",
+ "messageFcOptional": "FC(可选)",
+ "messageLeaveBlankToTryAllValues": "留空则遍历所有",
+ "messageStartRecovery": "开始破解",
+ "messageReadAHitagCard": "读取 Hitag 卡片",
+ "messageGetTagDetails": "获取标签详情",
+ "messageCaptureCommunicationData": "捕获通信数据",
+ "messageCheckCommonKeys": "检查常用密钥",
+ "messageKeyRecoveryAttack": "密钥恢复攻击",
+ "messageSimulateAHitagTag": "模拟 Hitag 标签",
+ "messageReadAnIndalaCard": "读取 Indala 卡片",
+ "messageReadAnIoproxCard": "读取 ioProx 卡片",
+ "messageReadAKeriCard": "读取 Keri 卡片",
+ "messageGeneral": "通用",
+ "messageAutomaticallyDetectLowFrequencyTags": "自动检测低频标签",
+ "messageReadTheRawLowFrequencySignal": "读取低频原始信号",
+ "messageLfSniff": "低频喗探",
+ "messageCaptureLowFrequencyCommunication": "捕获低频通信",
+ "messageCheckAntennaTuning": "检查天线调谐状态",
+ "messageReadEm410x": "读取 EM410x",
+ "messageReadTheEm4100TagId": "读取 EM4100 标签 ID",
+ "messageCloneEm410x": "克隆 EM410x",
+ "messageTagId10HexDigits": "标签 ID (10位 hex)",
+ "messageForExample0102030405": "例如 0102030405",
+ "messageIdMustContain10HexDigits": "ID 必须是 10 位 hex 字符",
+ "messageDetectT55xxChipConfiguration": "检测 T55xx 芯片配置",
+ "messageShowT55xxConfiguration": "显示 T55xx 配置信息",
+ "messageDumpAllT55xxBlocks": "转储所有 T55xx 块",
+ "messageReadAPyramidCard": "读取 Pyramid 卡片",
+ "messageCardType2": "卡片类型:",
+ "messageQuickActions": "快捷操作",
+ "messageKeyRecoveryAttacks": "密钥攻击",
+ "messageReadWriteBlocks": "读写块",
+ "messageMagicCards": "魔术卡",
+ "messageDumpRestoreParametersOptional": "Dump/Restore 参数(可选)",
+ "messageKeyFileKeysOptional": "密钥文件 --keys (可选)",
+ "messageLeaveBlankToUseThePm3Default": "留空则使用 PM3 默认",
+ "messageDumpFileFileOptional": "转储文件 --file (可选)",
+ "messageDumpOutputFileRestoreInputFile": "dump:输出文件 / restore:输入文件",
+ "messageSelectKeyFile": "选择密钥文件",
+ "messageSelectDumpFile": "选择转储文件",
+ "messageClearCustomSettings": "清空自定义",
+ "messageDefaultSourcesKeyDump": "默认来源:key={value}; dump={value2}",
+ "messageDetectCard": "检测卡片",
+ "messageScanHf14aTags": "扫描 HF 14A 标签",
+ "messageCardInformation": "卡片信息",
+ "messageGetDetailedCardInformation": "获取详细卡片信息",
+ "messageAutomaticKeyRecovery": "自动密钥恢复",
+ "messageReadAllSectorsToAFile": "读取所有扇区到文件",
+ "messageRestoreCard": "恢复卡片",
+ "messageWriteTheDumpBackToTheCard": "将转储写回卡片",
+ "messageSniff2": "喗探",
+ "messageCaptureCardCommunication": "捕获卡片通信",
+ "messageMoreCardRecoveryDiagnosticsSeeCommandReference": "更多卡型破解/诊断(参考命令文档)",
+ "messageCollectCuid": "采集 CUID",
+ "messagePm3ScriptNameScriptRunName": "PM3 脚本名 (script run )",
+ "messageForExampleMifareMfAutopwn": "例如: mifare/mf_autopwn",
+ "messageListScripts": "列出脚本",
+ "messageRunScript": "运行脚本",
+ "messageKnownKey": "已知密钥",
+ "messageTryDefaultKeysOnAllSectors": "用默认密钥尝试所有扇区",
+ "messageNestedAttack": "Nested 攻击",
+ "messageUseAKnownKeyToFindOther": "用已知密钥查找其他密钥",
+ "messageForCardsWithStaticNonces": "适用于静态 nonce 卡片",
+ "messageAdvancedKeyRecovery": "高级密钥恢复",
+ "messageNoKnownKeyRequired": "无需已知密钥",
+ "messageReadDataFromOneBlock": "读取单个块数据",
+ "messageWriteData": "写入数据",
+ "messageData32HexDigits": "数据 (32位 hex)",
+ "messageForExample00000000000000000000000000000000": "例如 00000000000000000000000000000000",
+ "messageMagicCardOperations": "魔术卡操作",
+ "messageTheseCommandsOnlyApplyToGen1aGen2": "这些命令仅适用于 Gen1A/Gen2 (CUID) 魔术卡。\n写入块 0 可能会损坏非魔术卡!",
+ "messageReadBlock0": "读取块 0",
+ "messageReadManufacturerBlock": "读取制造商块",
+ "messageRestoreFactoryDefaults": "恢复出厂状态",
+ "messageEraseCard2": "擦除卡片?",
+ "messageThisWillResetAllBlocksAreYou": "这将重置所有块。确定吗?",
+ "messageLoadEmulator": "加载模拟器",
+ "messageLoadADumpIntoTheEmulator": "将转储加载到模拟器",
+ "messageNdefDecoding": "NDEF 解码",
+ "messageListAvailableScripts": "列出可用脚本",
+ "messageScriptName": "脚本名称",
+ "messageForExampleHfMfAutopwn": "例如: hf_mf_autopwn",
+ "messageRun": "运行",
+ "messageHint": "提示:{value}",
+ "messageClearTerminal": "清屏",
+ "messageListDecode": "列表/解码",
+ "messageProtocolType": "协议类型",
+ "messageDecodeList2": "解码列表",
+ "messageExtract": "提取",
+ "messageExtractTraceData": "提取 Trace 数据",
+ "messageSpacesAreAllowedMeansUnknownClearThe": "允许空格;? 表示未知。清空移除该项。",
+ "messageSaved": "已保存:{value}",
+ "messageCommandResultsAreCollectedHereAutomaticallyClick": "命令结果自动采集到此处,点击密钥或块数据即可编辑。换卡前请选择“新卡”;? 表示未知。",
+ "messageUidBlocksKeys": "{value} · UID {value2} · {value3} 块 · {value4} 项密钥",
+ "messageNotIdentified": "未识别",
+ "messageNewCardSelectCapacity": "新卡 / 选择容量",
+ "messageNewCardClearTheCurrentDraft": "新卡 {value}(清空当前草稿)",
+ "messageNewCardCapacity": "新卡 / 容量",
+ "messageOpenDraft": "打开草稿",
+ "messageSaveDraft": "保存草稿",
+ "messageExportKeyDictionary": "导出密钥字典",
+ "messageExportKeyBin2": "导出密钥 BIN",
+ "messageReplaceDraftWithTheOpenFile": "以已打开文件替换草稿",
+ "messageExportEml2": "导出 EML",
+ "messageExportBin2": "导出 BIN",
+ "messageDirectCommand": "直接命令",
+ "messageForExampleHfMfChk1k": "例如 hf mf chk --1k",
+ "messageRunning": "执行中",
+ "messageExecute": "执行",
+ "messageKeysTrailers": "密钥 → 尾块",
+ "messageTrailersKeys": "尾块 → 密钥",
+ "messageNotRead": "未读取",
+ "messageSectorKey2": "扇区 {value} · Key {value2}",
+ "messageBlock3": "块 {value}{value2}",
+ "messageTrailer2": " · 尾块"
+}
diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart
new file mode 100644
index 0000000..98cb646
--- /dev/null
+++ b/lib/l10n/l10n.dart
@@ -0,0 +1,8 @@
+import 'package:flutter/widgets.dart';
+import 'app_localizations.dart';
+
+export 'app_localizations.dart';
+
+extension L10nContext on BuildContext {
+ AppLocalizations get l10n => AppLocalizations.of(this)!;
+}
diff --git a/lib/l10n/localized_messages.dart b/lib/l10n/localized_messages.dart
new file mode 100644
index 0000000..ed97f90
--- /dev/null
+++ b/lib/l10n/localized_messages.dart
@@ -0,0 +1,15 @@
+import 'package:flutter/widgets.dart';
+import 'app_localizations.dart';
+
+export 'app_localizations.dart';
+
+final AppLocalizations defaultLocalizations =
+ lookupAppLocalizations(const Locale('zh'));
+
+mixin LocalizedMessages {
+ AppLocalizations l10n = defaultLocalizations;
+
+ void updateLocalizations(AppLocalizations value) {
+ l10n = value;
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index a31dee0..5f747eb 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -11,29 +11,35 @@
library;
import 'dart:ui' show AppExitResponse;
+import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:pm3gui/state/app_state.dart';
import 'package:pm3gui/ui/theme.dart';
import 'package:pm3gui/ui/home_page.dart';
+import 'package:pm3gui/l10n/l10n.dart';
+import 'package:pm3gui/l10n/app_locale_controller.dart';
/// 应用入口函数
///
/// 初始化应用并提供全局状态管理
-void main() {
+Future main() async {
WidgetsFlutterBinding.ensureInitialized();
+ final localeController = AppLocaleController();
+ await localeController.initialize();
final appState = AppState();
AppLifecycleListener(
onExitRequested: () async {
await appState.shutdown();
+ await localeController.flush();
return AppExitResponse.exit;
},
);
runApp(
ChangeNotifierProvider(
create: (_) => appState,
- child: const PM3GuiApp(),
+ child: PM3GuiApp(localeController: localeController),
),
);
}
@@ -41,17 +47,51 @@ void main() {
/// 应用根组件
///
/// 负责应用的主题管理和页面导航
-class PM3GuiApp extends StatelessWidget {
- const PM3GuiApp({super.key});
+class PM3GuiApp extends StatefulWidget {
+ const PM3GuiApp({super.key, this.localeController});
+
+ final AppLocaleController? localeController;
+
+ @override
+ State createState() => _PM3GuiAppState();
+}
+
+class _PM3GuiAppState extends State {
+ late final AppLocaleController _localeController;
+
+ @override
+ void initState() {
+ super.initState();
+ _localeController = widget.localeController ?? AppLocaleController();
+ unawaited(_localeController.initialize());
+ }
+
+ @override
+ void dispose() {
+ if (widget.localeController == null) _localeController.dispose();
+ super.dispose();
+ }
@override
Widget build(BuildContext context) {
- return MaterialApp(
- title: 'PM3 图形界面',
- debugShowCheckedModeBanner: false,
- theme: AppTheme.theme(),
- // 应用主页面
- home: const HomePage(),
+ return ChangeNotifierProvider.value(
+ value: _localeController,
+ child: AnimatedBuilder(
+ animation: _localeController,
+ builder: (context, _) => MaterialApp(
+ onGenerateTitle: (context) => context.l10n.appTitle,
+ locale: _localeController.locale,
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
+ debugShowCheckedModeBanner: false,
+ theme: AppTheme.theme(),
+ builder: (context, child) {
+ context.read().updateLocalizations(context.l10n);
+ return child!;
+ },
+ home: const HomePage(),
+ ),
+ ),
);
}
}
diff --git a/lib/models/command_card.dart b/lib/models/command_card.dart
index 058f7f1..dde4e2b 100644
--- a/lib/models/command_card.dart
+++ b/lib/models/command_card.dart
@@ -4,10 +4,11 @@ import 'dart:typed_data';
import 'mifare_card.dart';
import '../parsers/output_parser.dart';
import '../parsers/key_parser.dart';
+import 'package:pm3gui/l10n/localized_messages.dart';
/// 命令采集的未完成卡片。缺项保持未知,不能当作全零块或默认密钥。
/// 文件转储仍由 MifareCard 表示;只有完整草稿才能导出为转储。
-class CommandCard {
+class CommandCard with LocalizedMessages {
CardType type = card1K;
String uid = '';
final Map blocks = {};
@@ -144,14 +145,15 @@ class CommandCard {
if (normalized.isNotEmpty &&
(normalized.length != length ||
!RegExp(r'^[0-9A-F?]+$').hasMatch(normalized))) {
- throw FormatException('需要 $length 位十六进制字符;未知位用 ?,清空表示未读取');
+ throw FormatException(
+ l10n.messageEnterHexDigitsUseForUnknownDigits((length).toString()));
}
return normalized;
}
void editBlock(int block, String value) {
if (block < 0 || block >= type.blockCount) {
- throw const FormatException('块号超出卡容量');
+ throw FormatException(l10n.messageBlockNumberExceedsTheCardCapacity);
}
final data = normalize(value, 32);
if (data.isEmpty) {
@@ -163,7 +165,7 @@ class CommandCard {
void editKey(int sector, bool a, String value) {
if (sector < 0 || sector >= type.sectorCount) {
- throw const FormatException('扇区号超出卡容量');
+ throw FormatException(l10n.messageSectorNumberExceedsTheCardCapacity);
}
final key = normalize(value, 12);
final target = a ? keysA : keysB;
@@ -207,16 +209,18 @@ class CommandCard {
void importDraft(String text) {
final json = jsonDecode(text) as Map;
if (json['format'] != 'pm3gui-command-card-v1') {
- throw const FormatException('不是命令数据草稿');
+ throw FormatException(l10n.messageThisIsNotACommandDataDraft);
}
final cardType = CardType.fromBlockCount(json['blocksCount'] as int);
- if (cardType == null) throw const FormatException('不支持的卡容量');
+ if (cardType == null) {
+ throw FormatException(l10n.messageUnsupportedCardCapacity);
+ }
final parsed = CommandCard()..clear(cardType: cardType);
parsed.uid = json['uid'] as String;
if (parsed.uid.isNotEmpty &&
!RegExp(r'^(?:[0-9A-F]{8}|[0-9A-F]{14}|[0-9A-F]{20})$')
.hasMatch(parsed.uid)) {
- throw const FormatException('UID 格式不正确');
+ throw FormatException(l10n.messageInvalidUidFormat);
}
for (final item in (json['blocks'] as Map).entries) {
parsed.editBlock(int.parse(item.key), item.value as String);
@@ -237,7 +241,8 @@ class CommandCard {
String exportEml() {
for (var b = 0; b < type.blockCount; b++) {
if (!RegExp(r'^[0-9A-F]{32}$').hasMatch(blocks[b] ?? '')) {
- throw FormatException('块 $b 尚未完整,先保存草稿或补齐数据');
+ throw FormatException(
+ l10n.messageBlockIsIncompleteSaveADraftOr((b).toString()));
}
}
return '${List.generate(type.blockCount, (b) => blocks[b]).join('\n')}\n';
@@ -270,7 +275,8 @@ class CommandCard {
for (var s = 0; s < type.sectorCount; s++) {
if (!RegExp(r'^[0-9A-F]{12}$').hasMatch(keysA[s] ?? '') ||
!RegExp(r'^[0-9A-F]{12}$').hasMatch(keysB[s] ?? '')) {
- throw FormatException('扇区 $s 密钥不完整,可先导出字典或保存草稿');
+ throw FormatException(l10n
+ .messageSectorHasIncompleteKeysExportADictionary((s).toString()));
}
keys.add(SectorKey(keyA: keysA[s]!, keyB: keysB[s]!));
}
@@ -280,7 +286,9 @@ class CommandCard {
String exportDictionary() {
final keys = {...keysA.values, ...keysB.values}
.where((k) => RegExp(r'^[0-9A-F]{12}$').hasMatch(k));
- if (keys.isEmpty) throw const FormatException('没有完整密钥可导出');
+ if (keys.isEmpty) {
+ throw FormatException(l10n.messageThereAreNoCompleteKeysToExport);
+ }
return '${keys.join('\n')}\n';
}
}
diff --git a/lib/models/dump_analysis.dart b/lib/models/dump_analysis.dart
index b808614..a33299d 100644
--- a/lib/models/dump_analysis.dart
+++ b/lib/models/dump_analysis.dart
@@ -7,6 +7,7 @@ library;
import 'dart:typed_data';
import 'mifare_card.dart';
import 'access_bits.dart';
+import 'package:pm3gui/l10n/localized_messages.dart';
// ==========================================================================
// 常量
@@ -36,56 +37,61 @@ const List knownDefaultKeys = [
];
/// MAD AID 描述 (功能代码 → 说明)
-const Map madAidDescriptions = {
- 0x0000: '空/未分配',
- 0x0001: '缺陷管理',
- 0x0002: '备用',
- 0x0003: '额外的目录条目',
- 0x0004: '卡持有者信息',
- 0x0005: '未分配',
- 0x0008: '航空服务',
- 0x0010: '城市交通',
- 0x0011: '城市交通 (2)',
- 0x0012: '铁路交通',
- 0x0014: '运输 (通用)',
- 0x0015: '安防方案',
- 0x0018: '城市交通 (3)',
- 0x0020: '事件票务',
- 0x0028: '忠诚方案',
- 0x0030: '停车场',
- 0x0038: '门禁控制',
- 0x0040: '电子钱包 / 小额支付',
- 0x0047: '电子票务 (通用)',
- 0x0048: '移动办公',
- 0x0049: '身份识别',
- 0x004A: '手机支付',
- 0x004B: 'FeliCa集成',
- 0x0060: '加油',
- 0x0068: '信息处理',
- 0x0070: 'NFC Forum NDEF',
-};
+Map get madAidDescriptions =>
+ madAidDescriptionsFor(defaultLocalizations);
+
+Map madAidDescriptionsFor(AppLocalizations l10n) => {
+ 0x0000: l10n.messageEmptyUnallocated,
+ 0x0001: l10n.messageDefectManagement,
+ 0x0002: l10n.messageReserved,
+ 0x0003: l10n.messageAdditionalDirectoryEntries,
+ 0x0004: l10n.messageCardholderInformation,
+ 0x0005: l10n.messageUnallocated,
+ 0x0008: l10n.messageAirlineServices,
+ 0x0010: l10n.messageUrbanTransport,
+ 0x0011: l10n.messageUrbanTransport2,
+ 0x0012: l10n.messageRailTransport,
+ 0x0014: l10n.messageTransportGeneral,
+ 0x0015: l10n.messageSecurityServices,
+ 0x0018: l10n.messageUrbanTransport3,
+ 0x0020: l10n.messageEventTicketing,
+ 0x0028: l10n.messageLoyaltySchemes,
+ 0x0030: l10n.messageParking,
+ 0x0038: l10n.messageAccessControl,
+ 0x0040: l10n.messageElectronicPurseMicropayments,
+ 0x0047: l10n.messageElectronicTicketingGeneral,
+ 0x0048: l10n.messageMobileOffice,
+ 0x0049: l10n.messageIdentification,
+ 0x004A: l10n.messageMobilePayments,
+ 0x004B: l10n.messageFelicaIntegration,
+ 0x0060: l10n.messageFuelServices,
+ 0x0068: l10n.messageInformationProcessing,
+ 0x0070: 'NFC Forum NDEF',
+ };
/// NXP 芯片类型 (根据SAK/ATQA)
-const Map nxpChipTypes = {
- '04_0044': 'MIFARE Ultralight',
- '44_0044': 'MIFARE Ultralight C',
- '04_0344': 'MIFARE Ultralight EV1 (640bit)',
- '44_0344': 'MIFARE Ultralight EV1 (1312bit)',
- '08_0004': 'MIFARE Classic 1K',
- '18_0002': 'MIFARE Classic 4K',
- '09_0004': 'MIFARE Mini',
- '10_0004': 'MIFARE Plus 2K SL2',
- '11_0004': 'MIFARE Plus 4K SL2',
- '20_0344': 'MIFARE DESFire',
- '20_0444': 'MIFARE DESFire EV1',
- '60_0004': 'MIFARE Classic 1K (Infineon)',
- '00_0004': 'MIFARE Classic 1K (兼容)',
- '28_0004': 'MIFARE Classic 1K (Emulated)',
- '38_0002': 'MIFARE Classic 4K (SmartMX)',
- '88_0004': 'MIFARE Classic 1K (Infineon SLE 66R35)',
- '08_0044': 'MIFARE Classic 1K (兼容2)',
- '02_0004': 'MIFARE Classic 1K (NXP)',
-};
+Map get nxpChipTypes => nxpChipTypesFor(defaultLocalizations);
+
+Map nxpChipTypesFor(AppLocalizations l10n) => {
+ '04_0044': 'MIFARE Ultralight',
+ '44_0044': 'MIFARE Ultralight C',
+ '04_0344': 'MIFARE Ultralight EV1 (640bit)',
+ '44_0344': 'MIFARE Ultralight EV1 (1312bit)',
+ '08_0004': 'MIFARE Classic 1K',
+ '18_0002': 'MIFARE Classic 4K',
+ '09_0004': 'MIFARE Mini',
+ '10_0004': 'MIFARE Plus 2K SL2',
+ '11_0004': 'MIFARE Plus 4K SL2',
+ '20_0344': 'MIFARE DESFire',
+ '20_0444': 'MIFARE DESFire EV1',
+ '60_0004': 'MIFARE Classic 1K (Infineon)',
+ '00_0004': l10n.messageMifareClassic1kCompatible,
+ '28_0004': 'MIFARE Classic 1K (Emulated)',
+ '38_0002': 'MIFARE Classic 4K (SmartMX)',
+ '88_0004': 'MIFARE Classic 1K (Infineon SLE 66R35)',
+ '08_0044': l10n.messageMifareClassic1kCompatible2,
+ '02_0004': 'MIFARE Classic 1K (NXP)',
+ };
// ==========================================================================
// 分析结果类
@@ -270,10 +276,12 @@ class DumpAnalysis {
// ==========================================================================
class DumpAnalyzer {
/// 执行完整 dump 分析
- static DumpAnalysis analyze(MifareCard card) {
- final mfr = _decodeManufacturerBlock(card);
- final mad = _parseMad(card);
- final keys = _analyzeKeys(card);
+ static DumpAnalysis analyze(MifareCard card,
+ {AppLocalizations? localizations}) {
+ final l10n = localizations ?? defaultLocalizations;
+ final mfr = _decodeManufacturerBlock(card, l10n);
+ final mad = _parseMad(card, l10n);
+ final keys = _analyzeKeys(card, l10n);
final sectors = [];
final values = [];
int emptyCount = 0;
@@ -349,7 +357,8 @@ class DumpAnalyzer {
}
// --- 制造商块解码 ---
- static ManufacturerBlockInfo? _decodeManufacturerBlock(MifareCard card) {
+ static ManufacturerBlockInfo? _decodeManufacturerBlock(
+ MifareCard card, AppLocalizations l10n) {
if (card.blocks.isEmpty || card.blocks[0].length < 32) return null;
final hex = card.blocks[0].toUpperCase();
final bytes = _hexToBytes(hex);
@@ -383,10 +392,11 @@ class DumpAnalyzer {
// 芯片类型
final chipLookup = '${sak}_${_reverseEndian16(atqa)}';
- final chipType = nxpChipTypes[chipLookup] ?? _guessChipType(sak);
+ final chipType =
+ nxpChipTypesFor(l10n)[chipLookup] ?? _guessChipType(sak, l10n);
// 制造商
- final mfr = _getManufacturer(bytes[0]);
+ final mfr = _getManufacturer(bytes[0], l10n);
return ManufacturerBlockInfo(
uid: uid,
@@ -405,7 +415,7 @@ class DumpAnalyzer {
return '${hex4.substring(2, 4)}${hex4.substring(0, 2)}';
}
- static String _guessChipType(String sak) {
+ static String _guessChipType(String sak, AppLocalizations l10n) {
switch (sak.toUpperCase()) {
case '08':
return 'MIFARE Classic 1K';
@@ -428,11 +438,11 @@ class DumpAnalyzer {
case '01':
return 'TNP3xxx (NXP)';
default:
- return '未知 (SAK=$sak)';
+ return l10n.messageUnknownSak((sak).toString());
}
}
- static String _getManufacturer(int byte0) {
+ static String _getManufacturer(int byte0, AppLocalizations l10n) {
switch (byte0) {
case 0x01:
return 'Motorola';
@@ -481,12 +491,13 @@ class DumpAnalyzer {
case 0x97:
return 'Qualcomm';
default:
- return '未知 (0x${byte0.toRadixString(16).padLeft(2, '0')})';
+ return l10n.messageUnknown0x(
+ (byte0.toRadixString(16).padLeft(2, '0')).toString());
}
}
// --- MAD 解析 ---
- static MadInfo? _parseMad(MifareCard card) {
+ static MadInfo? _parseMad(MifareCard card, AppLocalizations l10n) {
// MAD 位于扇区0的 block 1-2 (MADv1) 或 + 扇区16 block 0-2 (MADv2)
if (card.blocks.length < 4) return null;
final b1 = card.blocks[1];
@@ -523,8 +534,9 @@ class DumpAnalyzer {
final aid = (hi << 8) | lo;
if (aid != 0) hasNonZero = true;
- final desc = madAidDescriptions[aid] ??
- '应用 0x${aid.toRadixString(16).padLeft(4, '0')}';
+ final desc = madAidDescriptionsFor(l10n)[aid] ??
+ l10n.messageApplication0x(
+ (aid.toRadixString(16).padLeft(4, '0')).toString());
entries.add(MadEntry(sector: i + 1, aid: aid, description: desc));
}
@@ -549,7 +561,7 @@ class DumpAnalyzer {
}
// --- 密钥分析 ---
- static KeyAnalysis _analyzeKeys(MifareCard card) {
+ static KeyAnalysis _analyzeKeys(MifareCard card, AppLocalizations l10n) {
final keyAGroups = >{};
final keyBGroups = >{};
final defaults = [];
@@ -564,7 +576,7 @@ class DumpAnalyzer {
if (ka.isNotEmpty && ka != '000000000000') {
foundA++;
keyAGroups.putIfAbsent(ka, () => []).add(s);
- final defName = _findDefaultKeyName(ka);
+ final defName = _findDefaultKeyName(ka, l10n);
if (defName != null) {
defaults.add(DefaultKeyMatch(
keyHex: ka, keyType: 'A', sector: s, keyName: defName));
@@ -576,7 +588,7 @@ class DumpAnalyzer {
if (kb.isNotEmpty && kb != '000000000000') {
foundB++;
keyBGroups.putIfAbsent(kb, () => []).add(s);
- final defName = _findDefaultKeyName(kb);
+ final defName = _findDefaultKeyName(kb, l10n);
if (defName != null) {
defaults.add(DefaultKeyMatch(
keyHex: kb, keyType: 'B', sector: s, keyName: defName));
@@ -600,11 +612,11 @@ class DumpAnalyzer {
);
}
- static String? _findDefaultKeyName(String hex) {
+ static String? _findDefaultKeyName(String hex, AppLocalizations l10n) {
final upper = hex.toUpperCase();
- const names = {
- 'FFFFFFFFFFFF': 'NXP 出厂默认',
- '000000000000': '全零',
+ final names = {
+ 'FFFFFFFFFFFF': l10n.messageNxpFactoryDefault,
+ '000000000000': l10n.messageAllZeros,
'A0A1A2A3A4A5': 'MAD KeyA',
'B0B1B2B3B4B5': 'MAD KeyB',
'D3F7D3F7D3F7': 'NDEF',
diff --git a/lib/parsers/dump_parser.dart b/lib/parsers/dump_parser.dart
index 1ea2795..27bc955 100644
--- a/lib/parsers/dump_parser.dart
+++ b/lib/parsers/dump_parser.dart
@@ -12,6 +12,7 @@ import 'package:pm3gui/models/dump_analysis.dart';
import 'package:pm3gui/parsers/eml_parser.dart';
import 'package:pm3gui/parsers/bin_parser.dart';
import 'package:pm3gui/parsers/json_dump_parser.dart';
+import 'package:pm3gui/l10n/localized_messages.dart';
/// Result of parsing a dump file.
class DumpResult {
@@ -19,13 +20,20 @@ class DumpResult {
final String format; // 'eml', 'bin', 'json'
final String? error;
DumpAnalysis? _analysis;
+ String? _analysisLocale;
DumpResult({required this.card, required this.format, this.error});
bool get isSuccess => error == null;
/// 获取或生成深度分析结果 (懒加载)
- DumpAnalysis get analysis {
- _analysis ??= DumpAnalyzer.analyze(card);
+ DumpAnalysis get analysis => analysisFor(defaultLocalizations);
+
+ DumpAnalysis analysisFor(AppLocalizations localizations) {
+ if (_analysisLocale != localizations.localeName) {
+ _analysis = null;
+ _analysisLocale = localizations.localeName;
+ }
+ _analysis ??= DumpAnalyzer.analyze(card, localizations: localizations);
return _analysis!;
}
}
diff --git a/lib/parsers/key_parser.dart b/lib/parsers/key_parser.dart
index 35e4d74..05fdf2e 100644
--- a/lib/parsers/key_parser.dart
+++ b/lib/parsers/key_parser.dart
@@ -14,6 +14,7 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:pm3gui/models/mifare_card.dart';
+import 'package:pm3gui/l10n/localized_messages.dart';
// ---------------------------------------------------------------------------
// Binary key file (.bin) — sectorCount × 12 bytes
@@ -130,23 +131,26 @@ String exportKeysAsText(List keys) {
}
/// Read the indexed text format emitted by exportKeysAsText, without inventing missing sectors.
-List parseKeyText(String text) {
+List parseKeyText(String text, {AppLocalizations? localizations}) {
+ final l10n = localizations ?? defaultLocalizations;
final rows = {};
for (final raw in text.split('\n')) {
final line = raw.trim();
if (line.isEmpty || line.startsWith('#')) continue;
final match = RegExp(r'^(\d+)\s+([0-9a-fA-F]{12})\s+([0-9a-fA-F]{12})$')
.firstMatch(line);
- if (match == null) throw const FormatException('密钥文本格式错误');
+ if (match == null) throw FormatException(l10n.messageInvalidKeyTextFormat);
final sector = int.parse(match.group(1)!);
- if (rows.containsKey(sector)) throw FormatException('扇区 $sector 重复');
+ if (rows.containsKey(sector)) {
+ throw FormatException(l10n.messageDuplicateSector((sector).toString()));
+ }
rows[sector] = SectorKey(
keyA: match.group(2)!.toUpperCase(),
keyB: match.group(3)!.toUpperCase());
}
if (![5, 16, 32, 40].contains(rows.length) ||
List.generate(rows.length, (i) => i).any((i) => !rows.containsKey(i))) {
- throw const FormatException('密钥文本缺少扇区');
+ throw FormatException(l10n.messageKeyTextIsMissingSectors);
}
return List.generate(rows.length, (i) => rows[i]!);
}
diff --git a/lib/services/dump_converter.dart b/lib/services/dump_converter.dart
index aaa81fe..9768813 100644
--- a/lib/services/dump_converter.dart
+++ b/lib/services/dump_converter.dart
@@ -16,21 +16,31 @@ import 'package:pm3gui/parsers/bin_parser.dart';
import 'package:pm3gui/parsers/eml_parser.dart';
import 'package:pm3gui/parsers/json_dump_parser.dart';
import 'package:pm3gui/parsers/key_parser.dart';
+import 'package:pm3gui/l10n/localized_messages.dart';
/// Supported file formats for conversion.
enum DumpFormat {
- bin('bin', '二进制转储 (.bin)', true),
- eml('eml', '文本转储 (.eml)', true),
- json('json', 'PM3 JSON (.json)', true),
- keyBin('key.bin', '二进制密钥 (.key.bin)', false),
- dic('dic', '密钥字典 (.dic)', false),
- keyTxt('keys.txt', '密钥列表 (.keys.txt)', false);
+ bin('bin', true),
+ eml('eml', true),
+ json('json', true),
+ keyBin('key.bin', false),
+ dic('dic', false),
+ keyTxt('keys.txt', false);
final String ext;
- final String label;
+ String get label => localizedLabel(defaultLocalizations);
final bool isFullDump; // true = contains block data, false = keys only
- const DumpFormat(this.ext, this.label, this.isFullDump);
+ const DumpFormat(this.ext, this.isFullDump);
+
+ String localizedLabel(AppLocalizations l10n) => switch (this) {
+ bin => l10n.messageBinaryDumpBin,
+ eml => l10n.messageTextDumpEml,
+ json => 'PM3 JSON (.json)',
+ keyBin => l10n.messageBinaryKeysKeyBin,
+ dic => l10n.messageKeyDictionaryDic,
+ keyTxt => l10n.messageKeyListKeysTxt,
+ };
}
/// Result of a conversion operation.
@@ -61,12 +71,16 @@ Future convertDumpFile({
required String inputPath,
required String outputPath,
required DumpFormat targetFormat,
+ AppLocalizations? localizations,
}) async {
+ final l10n = localizations ?? defaultLocalizations;
try {
// ---- Parse input ----
final inputFile = File(inputPath);
if (!await inputFile.exists()) {
- return ConvertResult(success: false, error: '文件不存在: $inputPath');
+ return ConvertResult(
+ success: false,
+ error: l10n.messageFileNotFound((inputPath).toString()));
}
final ext = inputPath.split('.').last.toLowerCase();
@@ -108,47 +122,50 @@ Future convertDumpFile({
switch (targetFormat) {
case DumpFormat.bin:
if (card == null) {
- return ConvertResult(success: false, error: '密钥文件无法转换为完整dump(缺少数据块)');
+ return ConvertResult(
+ success: false, error: l10n.messageAKeyFileCannotBeConvertedTo);
}
await outFile.writeAsBytes(exportToBin(card));
case DumpFormat.eml:
if (card == null) {
- return ConvertResult(success: false, error: '密钥文件无法转换为EML(缺少数据块)');
+ return ConvertResult(
+ success: false, error: l10n.messageAKeyFileCannotBeConvertedTo2);
}
await outFile.writeAsString(exportToEml(card));
case DumpFormat.json:
if (card == null) {
- return ConvertResult(success: false, error: '密钥文件无法转换为JSON(缺少数据块)');
+ return ConvertResult(
+ success: false, error: l10n.messageAKeyFileCannotBeConvertedTo3);
}
await outFile.writeAsString(exportToJson(card));
case DumpFormat.keyBin:
final keys = card?.sectorKeys ?? keysOnly;
if (keys == null || keys.isEmpty) {
- return ConvertResult(success: false, error: '无密钥数据');
+ return ConvertResult(success: false, error: l10n.messageNoKeyData);
}
await outFile.writeAsBytes(exportKeysToBin(keys));
case DumpFormat.dic:
final keys = card?.sectorKeys ?? keysOnly;
if (keys == null || keys.isEmpty) {
- return ConvertResult(success: false, error: '无密钥数据');
+ return ConvertResult(success: false, error: l10n.messageNoKeyData);
}
await outFile.writeAsString(exportToDic(keys));
case DumpFormat.keyTxt:
final keys = card?.sectorKeys ?? keysOnly;
if (keys == null || keys.isEmpty) {
- return ConvertResult(success: false, error: '无密钥数据');
+ return ConvertResult(success: false, error: l10n.messageNoKeyData);
}
await outFile.writeAsString(exportKeysAsText(keys));
}
// Build key summary
final keys = card?.sectorKeys ?? keysOnly;
- final summary = keys != null ? _buildKeySummary(keys) : null;
+ final summary = keys != null ? _buildKeySummary(keys, l10n) : null;
return ConvertResult(
success: true,
@@ -156,16 +173,21 @@ Future convertDumpFile({
keySummary: summary,
);
} catch (e) {
- return ConvertResult(success: false, error: '转换失败: $e');
+ return ConvertResult(
+ success: false, error: l10n.messageConversionFailed((e).toString()));
}
}
/// Extract keys from any supported dump file.
///
/// Returns (sectorKeys, keySummaryText) or throws on error.
-Future<(List, String)> extractKeysFromFile(String path) async {
+Future<(List, String)> extractKeysFromFile(String path,
+ {AppLocalizations? localizations}) async {
+ final l10n = localizations ?? defaultLocalizations;
final file = File(path);
- if (!await file.exists()) throw Exception('文件不存在: $path');
+ if (!await file.exists()) {
+ throw Exception(l10n.messageFileNotFound((path).toString()));
+ }
final ext = path.split('.').last.toLowerCase();
@@ -175,7 +197,7 @@ Future<(List, String)> extractKeysFromFile(String path) async {
final data = Uint8List.fromList(bytes);
if (_isKeyBinSize(data.length)) {
final keys = parseKeyBinBytes(data);
- return (keys, _buildKeySummary(keys));
+ return (keys, _buildKeySummary(keys, l10n));
}
}
@@ -184,19 +206,20 @@ Future<(List, String)> extractKeysFromFile(String path) async {
final text = await file.readAsString();
final dictKeys = parseDicString(text);
final keys = _dictKeysToSectorKeys(dictKeys);
- return (keys, '${dictKeys.length} 个唯一密钥');
+ return (keys, l10n.messageUniqueKeys((dictKeys.length).toString()));
}
// Full dump — parse and extract
final result = await parseDumpFile(path);
if (!result.isSuccess) throw Exception(result.error);
final keys = result.card.sectorKeys;
- return (keys, _buildKeySummary(keys));
+ return (keys, _buildKeySummary(keys, l10n));
}
/// Write keys to a file in the requested format.
-Future saveKeysToFile(
- List keys, String path, DumpFormat fmt) async {
+Future saveKeysToFile(List keys, String path, DumpFormat fmt,
+ {AppLocalizations? localizations}) async {
+ final l10n = localizations ?? defaultLocalizations;
switch (fmt) {
case DumpFormat.keyBin:
await File(path).writeAsBytes(exportKeysToBin(keys));
@@ -205,7 +228,8 @@ Future saveKeysToFile(
case DumpFormat.keyTxt:
await File(path).writeAsString(exportKeysAsText(keys));
default:
- throw ArgumentError('不支持的密钥导出格式: ${fmt.label}');
+ throw ArgumentError(
+ l10n.messageUnsupportedKeyExportFormat(fmt.localizedLabel(l10n)));
}
}
@@ -249,12 +273,13 @@ List _dictKeysToSectorKeys(List dictKeys) {
return List.generate(16, (_) => SectorKey(keyA: kA, keyB: kB));
}
-String _buildKeySummary(List keys) {
+String _buildKeySummary(List keys, AppLocalizations l10n) {
final uniqueA = keys.map((k) => k.keyA.toUpperCase()).toSet();
final uniqueB = keys.map((k) => k.keyB.toUpperCase()).toSet();
final allKeys = {...uniqueA, ...uniqueB};
- return '${keys.length} 扇区, '
- '${uniqueA.length} 种 KeyA, '
- '${uniqueB.length} 种 KeyB, '
- '共 ${allKeys.length} 种唯一密钥';
+ return l10n.messageSectorsUniqueKeyAUniqueKeyB(
+ (keys.length).toString(),
+ (uniqueA.length).toString(),
+ (uniqueB.length).toString(),
+ (allKeys.length).toString());
}
diff --git a/lib/services/locale_settings_store.dart b/lib/services/locale_settings_store.dart
new file mode 100644
index 0000000..510aff8
--- /dev/null
+++ b/lib/services/locale_settings_store.dart
@@ -0,0 +1,54 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path_provider/path_provider.dart';
+import 'pm3_settings_store.dart';
+
+abstract interface class LocaleSettingsRepository {
+ Future load();
+ Future save(String? languageCode);
+}
+
+final class LocaleSettingsStore implements LocaleSettingsRepository {
+ LocaleSettingsStore({SettingsDirectoryProvider? directoryProvider})
+ : _directoryProvider =
+ directoryProvider ?? getApplicationSupportDirectory;
+
+ final SettingsDirectoryProvider _directoryProvider;
+ static var _sequence = 0;
+
+ Future _file() async {
+ final directory = await _directoryProvider();
+ return File('${directory.path}${Platform.pathSeparator}language.json');
+ }
+
+ @override
+ Future load() async {
+ final file = await _file();
+ if (!await file.exists()) return null;
+ final json = jsonDecode(await file.readAsString());
+ if (json is! Map ||
+ !json.containsKey('language') ||
+ (json['language'] != null && json['language'] is! String)) {
+ throw const FormatException('Invalid language preference file');
+ }
+ return json['language'] as String?;
+ }
+
+ @override
+ Future save(String? languageCode) async {
+ final file = await _file();
+ await file.parent.create(recursive: true);
+ final temporary = File('${file.path}.$pid.${_sequence++}.tmp');
+ try {
+ await temporary.create(exclusive: true);
+ await temporary.writeAsString(
+ jsonEncode({'language': languageCode}),
+ flush: true,
+ );
+ await temporary.rename(file.path);
+ } finally {
+ if (await temporary.exists()) await temporary.delete();
+ }
+ }
+}
diff --git a/lib/services/pm3_command_catalog.dart b/lib/services/pm3_command_catalog.dart
index e99ae60..b494b06 100644
--- a/lib/services/pm3_command_catalog.dart
+++ b/lib/services/pm3_command_catalog.dart
@@ -71,13 +71,17 @@ class Pm3CommandCatalog {
static const _zhAsset = 'docs/pm3_commands_help.zh.yaml';
static const _enAsset = 'docs/pm3_commands_help.yaml';
- static Future> load({bool preferZh = true}) async {
- final text = await _loadYamlText(preferZh: preferZh);
+ static Future> load(
+ {bool preferZh = true, AssetBundle? bundle}) async {
+ final assets = bundle ?? rootBundle;
+ final text = await _loadYamlText(preferZh: preferZh, bundle: assets);
final entries = _parseEntries(text);
- final snapshot = jsonDecode(await rootBundle.loadString(
- 'docs/pm3_commands_client.json')) as Map;
+ final snapshot =
+ jsonDecode(await assets.loadString('docs/pm3_commands_client.json'))
+ as Map;
final known = entries.map((entry) => entry.command).toSet();
- for (final item in (snapshot['commands'] as List).cast