From 19b1408c5538708c9f09219df7cbe293896f8e1b Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Wed, 8 Jul 2026 15:59:54 +0200 Subject: [PATCH] 1.4.2 (build 48): OCR de screenshots en Share Extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El extension acepta ahora imágenes además de texto: al compartir un screenshot del banco/broker, Vision reconoce el texto EN EL DISPOSITIVO (la imagen nunca sale del teléfono), extrae los importes candidatos y rellena el más prominente visualmente (en las UIs bancarias el balance es el número más grande). El resto de candidatos se ofrecen como chips de un toque. Percentajes y años se filtran. - ExtImageAmountScanner.swift: VNRecognizeTextRequest + ranking por altura de bounding box + dedup por valor - ShareViewController: carga UIImage/URL/Data del provider, estado de escaneo, sección de candidatos - Info.plist: NSExtensionActivationSupportsImageWithMaxCount=1 - Strings nuevas en los 7 idiomas (ext_scanning_image, ext_detected_amounts, ext_ocr_no_amounts) - PredictionEngine: troceado ternario que agotaba el type-checker en Debug - Bump a 1.4.2 build 48 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WoScpmHdVj1aUf4rAp6hbe --- PortfolioJournal.xcodeproj/project.pbxproj | 40 +++--- .../Services/PredictionEngine.swift | 11 +- .../ExtImageAmountScanner.swift | 130 ++++++++++++++++++ PortfolioJournalQuickUpdate/Info.plist | 2 + .../ShareViewController.swift | 127 ++++++++++++++--- .../de.lproj/Localizable.strings | 3 + .../en.lproj/Localizable.strings | 3 + .../es-ES.lproj/Localizable.strings | 3 + .../fr.lproj/Localizable.strings | 3 + .../it.lproj/Localizable.strings | 3 + .../ja.lproj/Localizable.strings | 3 + .../pt-BR.lproj/Localizable.strings | 3 + 12 files changed, 290 insertions(+), 41 deletions(-) create mode 100644 PortfolioJournalQuickUpdate/ExtImageAmountScanner.swift diff --git a/PortfolioJournal.xcodeproj/project.pbxproj b/PortfolioJournal.xcodeproj/project.pbxproj index ce6583a..9b707f0 100644 --- a/PortfolioJournal.xcodeproj/project.pbxproj +++ b/PortfolioJournal.xcodeproj/project.pbxproj @@ -541,7 +541,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournal/PortfolioJournalDebug.entitlements; ENABLE_USER_SCRIPT_SANDBOXING = NO; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -559,7 +559,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -582,7 +582,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; PROVISIONING_PROFILE_SPECIFIER = "porfoliojournal"; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_ASSET_PATHS = PortfolioJournal/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; ENABLE_PREVIEWS = YES; @@ -600,7 +600,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -739,7 +739,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = PortfolioJournalWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -752,7 +752,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal.PortfolioJournalWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -774,7 +774,7 @@ CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; PROVISIONING_PROFILE_SPECIFIER = "Portfolio Journalwidget"; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_ASSET_PATHS = PortfolioJournalWidget/Assets.xcassets; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; @@ -787,7 +787,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal.PortfolioJournalWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -805,7 +805,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -814,7 +814,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournalTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -829,7 +829,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -838,7 +838,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournalTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -852,7 +852,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -861,7 +861,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournalUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -875,7 +875,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.6; @@ -884,7 +884,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournalUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -899,7 +899,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = 2825Q76T7H; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = PortfolioJournalQuickUpdate/Info.plist; @@ -911,7 +911,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal.QuickUpdate; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -927,7 +927,7 @@ CODE_SIGN_ENTITLEMENTS = PortfolioJournalQuickUpdateExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = 2825Q76T7H; PROVISIONING_PROFILE_SPECIFIER = "PortfolioJournal QuickUpdate AppStore"; GENERATE_INFOPLIST_FILE = NO; @@ -940,7 +940,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.4.1; + MARKETING_VERSION = 1.4.2; PRODUCT_BUNDLE_IDENTIFIER = com.alexandrevazquez.PortfolioJournal.QuickUpdate; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/PortfolioJournal/Services/PredictionEngine.swift b/PortfolioJournal/Services/PredictionEngine.swift index c66efb7..7fde4ba 100644 --- a/PortfolioJournal/Services/PredictionEngine.swift +++ b/PortfolioJournal/Services/PredictionEngine.swift @@ -115,10 +115,13 @@ class PredictionEngine { var smoothed = values[0] for i in 1..= 2 - ? (values.suffix(3).reduce(0, +) / Double(min(3, values.count)) - - values.prefix(3).reduce(0, +) / Double(min(3, values.count))) / Double(values.count) - : 0 + var trend = 0.0 + if values.count >= 2 { + let window = Double(min(3, values.count)) + let recentAvg: Double = values.suffix(3).reduce(0, +) / window + let earlyAvg: Double = values.prefix(3).reduce(0, +) / window + trend = (recentAvg - earlyAvg) / Double(values.count) + } let stdDev = calculateStdDev(values: values) return (1...monthsAhead).compactMap { month in diff --git a/PortfolioJournalQuickUpdate/ExtImageAmountScanner.swift b/PortfolioJournalQuickUpdate/ExtImageAmountScanner.swift new file mode 100644 index 0000000..d28cb42 --- /dev/null +++ b/PortfolioJournalQuickUpdate/ExtImageAmountScanner.swift @@ -0,0 +1,130 @@ +import Foundation +import UIKit +import Vision + +// MARK: - Screenshot amount scanner (on-device OCR) +// +// When the user shares an image (a bank/broker screenshot), Vision runs text +// recognition entirely on-device — the image never leaves the phone. Every +// number that parses as a positive amount becomes a candidate, ranked by +// visual prominence: in banking UIs the account balance is almost always the +// biggest number on screen, so the tallest match is prefilled and the rest are +// offered as one-tap alternatives. + +struct ExtAmountCandidate: Identifiable, Equatable { + let id: UUID + let display: String + let value: Decimal + let prominence: CGFloat + + init(display: String, value: Decimal, prominence: CGFloat) { + self.id = UUID() + self.display = display + self.value = value + self.prominence = prominence + } + + static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id } +} + +enum ExtImageAmountScanner { + static let maxCandidates = 6 + + static func scan(_ image: UIImage, completion: @escaping ([ExtAmountCandidate]) -> Void) { + guard let cgImage = image.cgImage else { + completion([]) + return + } + + let request = VNRecognizeTextRequest { request, _ in + let observations = (request.results as? [VNRecognizedTextObservation]) ?? [] + completion(candidates(from: observations)) + } + request.recognitionLevel = .accurate + // Amounts are digits and separators; language correction only "fixes" them into words. + request.usesLanguageCorrection = false + + DispatchQueue.global(qos: .userInitiated).async { + let handler = VNImageRequestHandler(cgImage: cgImage, orientation: cgOrientation(from: image.imageOrientation)) + do { + try handler.perform([request]) + } catch { + completion([]) + } + } + } + + // MARK: Candidate extraction + + private static let numberPattern: NSRegularExpression = { + // Grouped amounts (12.345,67 / 1,234.56 / 12 345,67), plain decimals (1234,5) or integers. + // Non-raw string so \u{00A0} becomes a literal NBSP inside the character class. + let pattern = "\\d{1,3}(?:[.,\u{00A0} ]\\d{3})+(?:[.,]\\d{1,2})?|\\d+[.,]\\d{1,2}|\\d+" + return try! NSRegularExpression(pattern: pattern) + }() + + private static func candidates(from observations: [VNRecognizedTextObservation]) -> [ExtAmountCandidate] { + var found: [ExtAmountCandidate] = [] + + for observation in observations { + guard let recognized = observation.topCandidates(1).first else { continue } + let line = recognized.string + let range = NSRange(line.startIndex..., in: line) + + for match in numberPattern.matches(in: line, range: range) { + guard let matchRange = Range(match.range, in: line) else { continue } + let display = String(line[matchRange]) + + guard !isPercentage(display, in: line, at: matchRange), + !looksLikeYear(display), + let value = ExtAmountParser.parse(display) else { continue } + + found.append(ExtAmountCandidate( + display: display.replacingOccurrences(of: "\u{00A0}", with: " "), + value: value, + prominence: observation.boundingBox.height + )) + } + } + + // Dedup by value, keeping the most prominent occurrence. + var byValue: [Decimal: ExtAmountCandidate] = [:] + for candidate in found { + if let existing = byValue[candidate.value], existing.prominence >= candidate.prominence { continue } + byValue[candidate.value] = candidate + } + + return byValue.values + .sorted { $0.prominence > $1.prominence } + .prefix(maxCandidates) + .map { $0 } + } + + /// "1,25 %" or "%1.25" — performance figures, not balances. + private static func isPercentage(_ display: String, in line: String, at range: Range) -> Bool { + let after = line[range.upperBound...].drop(while: { $0 == " " || $0 == "\u{00A0}" }) + if after.first == "%" { return true } + let before = line[.. Bool { + guard display.count == 4, display.allSatisfy(\.isNumber), let value = Int(display) else { return false } + return (1990...2099).contains(value) + } + + private static func cgOrientation(from orientation: UIImage.Orientation) -> CGImagePropertyOrientation { + switch orientation { + case .up: return .up + case .down: return .down + case .left: return .left + case .right: return .right + case .upMirrored: return .upMirrored + case .downMirrored: return .downMirrored + case .leftMirrored: return .leftMirrored + case .rightMirrored: return .rightMirrored + @unknown default: return .up + } + } +} diff --git a/PortfolioJournalQuickUpdate/Info.plist b/PortfolioJournalQuickUpdate/Info.plist index 45ac010..6e8c040 100644 --- a/PortfolioJournalQuickUpdate/Info.plist +++ b/PortfolioJournalQuickUpdate/Info.plist @@ -28,6 +28,8 @@ NSExtensionActivationSupportsText + NSExtensionActivationSupportsImageWithMaxCount + 1 NSExtensionPointIdentifier diff --git a/PortfolioJournalQuickUpdate/ShareViewController.swift b/PortfolioJournalQuickUpdate/ShareViewController.swift index 2258dfd..e320a80 100644 --- a/PortfolioJournalQuickUpdate/ShareViewController.swift +++ b/PortfolioJournalQuickUpdate/ShareViewController.swift @@ -5,10 +5,11 @@ import UniformTypeIdentifiers // MARK: - Quick Update share extension // // Appears in the share sheet when the user shares/selects text in ANY app (their -// bank, broker…). Shows a compact form floating over the host app: pick a source -// (the next one pending this month is preselected), amount prefilled from the -// shared text, save — and the user never leaves the bank app. The main app turns -// these captures into real snapshots on its next activation. +// bank, broker…) or shares a screenshot where the balance is visible. Shows a +// compact form floating over the host app: pick a source (the next one pending +// this month is preselected), amount prefilled from the shared text — or OCR'd +// on-device from the image — save, and the user never leaves the bank app. The +// main app turns these captures into real snapshots on its next activation. @objc(ShareViewController) final class ShareViewController: UIViewController { @@ -16,33 +17,53 @@ final class ShareViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear - extractSharedText { [weak self] text in + extractSharedContent { [weak self] text, image in DispatchQueue.main.async { - self?.presentForm(sharedText: text) + self?.presentForm(sharedText: text, sharedImage: image) } } } - private func extractSharedText(completion: @escaping (String?) -> Void) { + private func extractSharedContent(completion: @escaping (String?, UIImage?) -> Void) { let providers = (extensionContext?.inputItems as? [NSExtensionItem])? .compactMap { $0.attachments } .flatMap { $0 } ?? [] - guard let provider = providers.first(where: { + if let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) - }) else { - completion(nil) + }) { + provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in + completion(item as? String, nil) + } return } - provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in - completion(item as? String) + if let provider = providers.first(where: { + $0.hasItemConformingToTypeIdentifier(UTType.image.identifier) + }) { + provider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { item, _ in + completion(nil, Self.image(from: item)) + } + return + } + + completion(nil, nil) + } + + /// Share sheets deliver images as UIImage, file URL, or raw Data depending on the host app. + private static func image(from item: NSSecureCoding?) -> UIImage? { + switch item { + case let image as UIImage: return image + case let url as URL: return (try? Data(contentsOf: url)).flatMap(UIImage.init(data:)) + case let data as Data: return UIImage(data: data) + default: return nil } } - private func presentForm(sharedText: String?) { + private func presentForm(sharedText: String?, sharedImage: UIImage?) { let form = QuickUpdateShareView( sharedText: sharedText, + sharedImage: sharedImage, onDone: { [weak self] in self?.extensionContext?.completeRequest(returningItems: nil) }, @@ -68,6 +89,7 @@ final class ShareViewController: UIViewController { struct QuickUpdateShareView: View { let sharedText: String? + let sharedImage: UIImage? let onDone: () -> Void let onCancel: () -> Void @@ -75,6 +97,9 @@ struct QuickUpdateShareView: View { @State private var selectedSourceId: UUID? @State private var amountText: String = "" @State private var saved = false + @State private var scanning = false + @State private var candidates: [ExtAmountCandidate] = [] + @State private var scanFoundNothing = false @FocusState private var amountFocused: Bool private var parsedAmount: Decimal? { @@ -117,10 +142,59 @@ struct QuickUpdateShareView: View { private var form: some View { Form { Section(String(localized: "ext_amount_section")) { - TextField(String(localized: "ext_amount_placeholder"), text: $amountText) - .keyboardType(.decimalPad) - .font(.title3.weight(.semibold)) - .focused($amountFocused) + if scanning { + HStack(spacing: 10) { + ProgressView() + Text(String(localized: "ext_scanning_image")) + .font(.subheadline) + .foregroundColor(.secondary) + } + } else { + TextField(String(localized: "ext_amount_placeholder"), text: $amountText) + .keyboardType(.decimalPad) + .font(.title3.weight(.semibold)) + .focused($amountFocused) + if scanFoundNothing { + Text(String(localized: "ext_ocr_no_amounts")) + .font(.caption) + .foregroundColor(.secondary) + } + } + } + + if candidates.count > 1 { + Section(String(localized: "ext_detected_amounts")) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(candidates) { candidate in + Button { + amountText = "\(candidate.value)" + } label: { + Text(candidate.display) + .font(.subheadline.weight(.medium)) + .padding(.horizontal, 12) + .padding(.vertical, 7) + .background( + Capsule().fill( + amountText == "\(candidate.value)" + ? Color.blue.opacity(0.18) + : Color(.tertiarySystemFill) + ) + ) + .overlay( + Capsule().strokeBorder( + amountText == "\(candidate.value)" ? Color.blue : .clear, + lineWidth: 1 + ) + ) + .foregroundColor(.primary) + } + .buttonStyle(.plain) + } + } + .padding(.vertical, 2) + } + } } Section(String(localized: "ext_source_section")) { @@ -181,11 +255,30 @@ struct QuickUpdateShareView: View { if let sharedText, let parsed = ExtAmountParser.parse(sharedText) { amountText = "\(parsed)" + } else if let sharedImage { + scanImage(sharedImage) } else { amountFocused = true } } + private func scanImage(_ image: UIImage) { + scanning = true + ExtImageAmountScanner.scan(image) { found in + DispatchQueue.main.async { + scanning = false + candidates = found + if let best = found.first { + // Most visually prominent number — in bank UIs, the balance. + amountText = "\(best.value)" + } else { + scanFoundNothing = true + amountFocused = true + } + } + } + } + private func save() { guard let amount = parsedAmount, let sourceId = selectedSourceId else { return } ExtSharedStore.appendPending(ExtPendingUpdate( diff --git a/PortfolioJournalQuickUpdate/de.lproj/Localizable.strings b/PortfolioJournalQuickUpdate/de.lproj/Localizable.strings index ec4c4b4..ff02f0d 100644 --- a/PortfolioJournalQuickUpdate/de.lproj/Localizable.strings +++ b/PortfolioJournalQuickUpdate/de.lproj/Localizable.strings @@ -9,3 +9,6 @@ "ext_saved_body" = "Erscheint in Portfolio Journal beim nächsten Öffnen."; "ext_no_sources_title" = "Noch keine Quellen"; "ext_no_sources_body" = "Öffne Portfolio Journal einmal, damit deine Quellen hier erscheinen."; +"ext_scanning_image" = "Bild wird gelesen…"; +"ext_detected_amounts" = "Erkannte Beträge"; +"ext_ocr_no_amounts" = "Keine Beträge erkannt — gib den Wert manuell ein."; diff --git a/PortfolioJournalQuickUpdate/en.lproj/Localizable.strings b/PortfolioJournalQuickUpdate/en.lproj/Localizable.strings index e964110..3bd2ded 100644 --- a/PortfolioJournalQuickUpdate/en.lproj/Localizable.strings +++ b/PortfolioJournalQuickUpdate/en.lproj/Localizable.strings @@ -9,3 +9,6 @@ "ext_saved_body" = "It will appear in Portfolio Journal the next time you open it."; "ext_no_sources_title" = "No sources yet"; "ext_no_sources_body" = "Open Portfolio Journal once so your sources appear here."; +"ext_scanning_image" = "Reading image…"; +"ext_detected_amounts" = "Detected amounts"; +"ext_ocr_no_amounts" = "No amounts recognized — type the value manually."; diff --git a/PortfolioJournalQuickUpdate/es-ES.lproj/Localizable.strings b/PortfolioJournalQuickUpdate/es-ES.lproj/Localizable.strings index f546b47..5107d8a 100644 --- a/PortfolioJournalQuickUpdate/es-ES.lproj/Localizable.strings +++ b/PortfolioJournalQuickUpdate/es-ES.lproj/Localizable.strings @@ -9,3 +9,6 @@ "ext_saved_body" = "Aparecerá en Portfolio Journal la próxima vez que la abras."; "ext_no_sources_title" = "Aún no hay fuentes"; "ext_no_sources_body" = "Abre Portfolio Journal una vez para que tus fuentes aparezcan aquí."; +"ext_scanning_image" = "Leyendo imagen…"; +"ext_detected_amounts" = "Importes detectados"; +"ext_ocr_no_amounts" = "No se han reconocido importes — escribe el valor a mano."; diff --git a/PortfolioJournalQuickUpdate/fr.lproj/Localizable.strings b/PortfolioJournalQuickUpdate/fr.lproj/Localizable.strings index 04270d9..77a572d 100644 --- a/PortfolioJournalQuickUpdate/fr.lproj/Localizable.strings +++ b/PortfolioJournalQuickUpdate/fr.lproj/Localizable.strings @@ -9,3 +9,6 @@ "ext_saved_body" = "Apparaîtra dans Portfolio Journal à la prochaine ouverture."; "ext_no_sources_title" = "Aucune source"; "ext_no_sources_body" = "Ouvrez Portfolio Journal une fois pour que vos sources apparaissent ici."; +"ext_scanning_image" = "Lecture de l’image…"; +"ext_detected_amounts" = "Montants détectés"; +"ext_ocr_no_amounts" = "Aucun montant reconnu — saisissez la valeur manuellement."; diff --git a/PortfolioJournalQuickUpdate/it.lproj/Localizable.strings b/PortfolioJournalQuickUpdate/it.lproj/Localizable.strings index b5c0953..20fa569 100644 --- a/PortfolioJournalQuickUpdate/it.lproj/Localizable.strings +++ b/PortfolioJournalQuickUpdate/it.lproj/Localizable.strings @@ -9,3 +9,6 @@ "ext_saved_body" = "Apparirà in Portfolio Journal alla prossima apertura."; "ext_no_sources_title" = "Nessuna fonte"; "ext_no_sources_body" = "Apri Portfolio Journal una volta perché le tue fonti appaiano qui."; +"ext_scanning_image" = "Lettura dell’immagine…"; +"ext_detected_amounts" = "Importi rilevati"; +"ext_ocr_no_amounts" = "Nessun importo riconosciuto — inserisci il valore manualmente."; diff --git a/PortfolioJournalQuickUpdate/ja.lproj/Localizable.strings b/PortfolioJournalQuickUpdate/ja.lproj/Localizable.strings index dd89ec1..a09397c 100644 --- a/PortfolioJournalQuickUpdate/ja.lproj/Localizable.strings +++ b/PortfolioJournalQuickUpdate/ja.lproj/Localizable.strings @@ -9,3 +9,6 @@ "ext_saved_body" = "次回Portfolio Journalを開いたときに反映されます。"; "ext_no_sources_title" = "投資先がありません"; "ext_no_sources_body" = "一度Portfolio Journalを開くと、ここに投資先が表示されます。"; +"ext_scanning_image" = "画像を読み取り中…"; +"ext_detected_amounts" = "検出された金額"; +"ext_ocr_no_amounts" = "金額を認識できませんでした。手動で入力してください。"; diff --git a/PortfolioJournalQuickUpdate/pt-BR.lproj/Localizable.strings b/PortfolioJournalQuickUpdate/pt-BR.lproj/Localizable.strings index 5a318d6..6cf7889 100644 --- a/PortfolioJournalQuickUpdate/pt-BR.lproj/Localizable.strings +++ b/PortfolioJournalQuickUpdate/pt-BR.lproj/Localizable.strings @@ -9,3 +9,6 @@ "ext_saved_body" = "Aparecerá no Portfolio Journal na próxima vez que você abrir."; "ext_no_sources_title" = "Nenhuma fonte"; "ext_no_sources_body" = "Abra o Portfolio Journal uma vez para que suas fontes apareçam aqui."; +"ext_scanning_image" = "Lendo imagem…"; +"ext_detected_amounts" = "Valores detectados"; +"ext_ocr_no_amounts" = "Nenhum valor reconhecido — digite o valor manualmente.";