tane/apps/app_seeds/lib/services/recovery_sheet_service.dart
vjrj baa8867287 feat(i18n): full RTL/CJK support — fonts, Japanese locale, directional fixes
Bundle Noto Sans Arabic + Noto Sans JP (SIL OFL 1.1) as per-glyph fallbacks
for both the UI text theme and every generated PDF, so RTL (Arabic) and CJK
render on all platforms — including desktop, where the system font may lack
them — instead of tofu. A shared pdf_fonts.dart centralizes the PDF theme and
the three label/catalog/recovery services reuse it.

Add Japanese (ja) as the CJK reference locale — fittingly, the app's own name
is Japanese (種, tane, 'seed'). Core strings translated; the rest fall back to
English via slang. Wired into supportedLocales (automatic) and the settings
picker.

Fix three physical Alignment.centerLeft -> AlignmentDirectional.centerStart in
variety_detail_screen, and make month-abbreviation and avatar-initial
truncation grapheme-safe (.characters) so CJK text is never cut mid-character.

Tests: CJK/Arabic PDF glyph render, ja locale resolution + English fallback.
The chat-bubble mirroring flagged by the audit was verified correct (already
respects Directionality) and left unchanged.
2026-07-14 11:11:19 +02:00

75 lines
2.3 KiB
Dart

import 'dart:typed_data';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'file_service.dart';
import 'pdf_fonts.dart';
/// Renders the printable recovery sheet — a QR plus the typed code and short
/// instructions ("keep two paper copies, like your best seed"). All strings
/// arrive already localized.
class RecoverySheetService {
RecoverySheetService({required FileService files}) : _files = files;
final FileService _files;
Future<Uint8List> buildPdf({
required String title,
required String intro,
required String code,
}) async {
final fonts = await loadPdfFonts();
final doc = pw.Document(theme: fonts.theme);
doc.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4,
build: (context) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(title, style: pw.TextStyle(font: fonts.bold, fontSize: 20)),
pw.SizedBox(height: 12),
pw.Text(intro, style: const pw.TextStyle(fontSize: 11)),
pw.SizedBox(height: 24),
pw.Center(
child: pw.BarcodeWidget(
barcode: pw.Barcode.qrCode(),
data: code,
width: 220,
height: 220,
// The code is printed below with the Unicode-capable face;
// the widget's built-in caption would fall back to Courier.
drawText: false,
),
),
pw.SizedBox(height: 24),
pw.Center(
child: pw.Text(
code,
textAlign: pw.TextAlign.center,
style: pw.TextStyle(font: fonts.bold, fontSize: 12),
),
),
],
),
),
);
return doc.save();
}
/// Builds the sheet and asks the user where to keep it. Returns true when
/// saved, false when they cancelled.
Future<bool> saveSheet({
required String title,
required String intro,
required String code,
required String suggestedName,
}) async {
final bytes = await buildPdf(title: title, intro: intro, code: code);
final path = await _files.saveFile(
suggestedName: suggestedName,
bytes: bytes,
);
return path != null;
}
}