/// Locale-aware selection of Tesseract OCR language packs. /// /// Tane is international by design: the OCR must read seed packets in whatever /// language the user runs the app, not a fixed `eng`/`spa`. This maps the app's /// preferred locales to the traineddata packs we actually bundle and degrades /// gracefully — an unbundled locale simply falls back to the Latin baseline /// instead of failing. As more packs are bundled (incl. RTL/CJK scripts) they /// light up automatically with no code change here. library; import 'dart:convert'; const _trainedDataSuffix = '.traineddata'; /// ISO 639-1 (locale) → Tesseract language code (mostly ISO 639-2/T). Covers the /// scripts we plausibly bundle; extend as new packs are added. Kept /// intentionally broad — international first, not one region. const Map isoToTesseract = { // Latin-script European 'en': 'eng', 'es': 'spa', 'ca': 'cat', 'gl': 'glg', 'eu': 'eus', 'pt': 'por', 'fr': 'fra', 'de': 'deu', 'it': 'ita', 'nl': 'nld', 'pl': 'pol', 'cs': 'ces', 'ro': 'ron', 'sv': 'swe', 'da': 'dan', 'fi': 'fin', 'tr': 'tur', // Cyrillic / Greek 'ru': 'rus', 'uk': 'ukr', 'el': 'ell', // RTL scripts 'ar': 'ara', 'he': 'heb', 'fa': 'fas', 'ur': 'urd', // CJK & other non-Latin 'zh': 'chi_sim', 'ja': 'jpn', 'ko': 'kor', 'hi': 'hin', 'th': 'tha', }; /// Parses the bundled OCR config (`assets/tessdata_config.json`) into the set of /// available Tesseract language codes (basenames, e.g. `eng`, `spa`). The config /// is the single source of truth for what is shipped, so this stays in sync with /// the assets automatically. Set parseAvailableOcrLanguages(String configJson) { final data = jsonDecode(configJson) as Map; final files = (data['files'] as List?)?.cast() ?? const []; return files .map( (f) => f.endsWith(_trainedDataSuffix) ? f.substring(0, f.length - _trainedDataSuffix.length) : f, ) .where((code) => code.isNotEmpty) .toSet(); } /// Resolves the `+`-joined Tesseract language string for [preferredLocales] /// (ordered most-preferred first; each an ISO 639-1 code or a fuller tag like /// `es_ES`/`zh-Hans`), limited to the [available] bundled packs. /// /// Rules, in order: /// 1. Each preferred locale that maps to an available pack is included, in /// order, de-duplicated. /// 2. The Latin [baseline] (`eng`) is appended if bundled — Latin text is /// common on packets even in non-Latin locales, and it's a safe default. /// 3. If nothing matched and the baseline is unavailable, fall back to every /// available pack (sorted) so OCR still runs with whatever we shipped. /// /// Returns an empty string only when [available] is empty. String resolveOcrLanguages({ required List preferredLocales, required Set available, String baseline = 'eng', }) { final ordered = []; for (final locale in preferredLocales) { final code = tesseractCodeForLocale(locale); if (code != null && available.contains(code) && !ordered.contains(code)) { ordered.add(code); } } if (available.contains(baseline) && !ordered.contains(baseline)) { ordered.add(baseline); } if (ordered.isEmpty) { ordered.addAll(available.toList()..sort()); } return ordered.join('+'); } /// Maps a locale tag to its Tesseract code, or null if unmapped. Accepts full /// tags (`es_ES`, `zh-Hans-CN`) and uses the primary language subtag. String? tesseractCodeForLocale(String localeTag) { final primary = localeTag.split(RegExp('[_-]')).first.toLowerCase(); return isoToTesseract[primary]; }