Files
vnidrop/apple/VniDrop/UI/Components/Components.swift
cdricms ef42875ddb feat(apple): generate type-safe L10n accessors and migrate all key literals
Replaces every stringly-typed localization key in the Apple app with
compile-time-checked accessors generated from localization/strings.json.
A mistyped key is now a build error instead of a silent fallback to the
raw key at runtime. The runtime path is unchanged: plain keys are
String.LocalizationValue constants resolved with String(localized:) and
Apple's String Catalog still does the lookup; keys with arguments become
typed, named functions applying args through String(format:).

Generator: new renderSwiftAccessors emits apple/VniDrop/Generated/L10n.swift,
wired into generate. Renamed generic arg1/arg2 tokens on four keys to
semantic names (receiver, transferName, deviceId) and updated their context
notes; positional output is unchanged so .xcstrings (bar the 4 comments)
and the Android XML regenerate identical.

Migration: every key-carrying value flipped to String.LocalizationValue
end to end, resolved only at the leaf. Zero key literals and zero
LocalizedStringKey remain in app or test code. macOS build passes; iOS
test run pending.
2026-07-23 17:12:43 +02:00

97 lines
2.2 KiB
Swift

import SwiftUI
// MARK: - StatusPill
enum PillTone { case neutral, success, warning, destructive, brand }
struct StatusPill: View {
let label: String
var tone: PillTone = .neutral
private var color: Color {
switch tone {
case .neutral: return .secondary
case .success, .brand: return VniDropColors.brandPurple
case .warning: return .orange
case .destructive: return .red
}
}
var body: some View {
HStack(spacing: 5) {
Circle().fill(color).frame(width: 6, height: 6)
Text(label).font(.caption).fontWeight(.medium).foregroundStyle(color).lineLimit(1)
}
.padding(.horizontal, 9).padding(.vertical, 4)
.background(color.opacity(0.14), in: Capsule())
}
}
// MARK: - ProgressRow
struct ProgressRow: View {
let labelKey: String.LocalizationValue
let progress: Double?
var detail: String? = nil
/// Pre-resolved label; when set it overrides `labelKey`.
var labelText: String? = nil
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
label.font(.subheadline).lineLimit(1)
Spacer()
if let progress {
Text("\(Int(progress * 100))%").font(.caption).foregroundStyle(.secondary)
}
}
if let detail {
Text(detail).font(.caption).foregroundStyle(.secondary).lineLimit(1)
}
if let progress {
ProgressView(value: progress)
} else {
ProgressView().progressViewStyle(.linear)
}
}
.frame(maxWidth: .infinity)
}
@ViewBuilder
private var label: some View {
if let labelText {
Text(labelText)
} else {
Text(String(localized: labelKey))
}
}
}
// MARK: - Field
/// Labeled text field using native styling. Renders cleanly both inside a `Form`
/// row and standalone (e.g. inside a sheet).
struct Field: View {
let label: String
@Binding var value: String
var minLines: Int = 1
var enabled: Bool = true
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(label).font(.subheadline).foregroundStyle(.secondary)
Group {
if minLines > 1 {
TextField(label, text: $value, axis: .vertical)
.lineLimit(minLines, reservesSpace: true)
} else {
TextField(label, text: $value)
}
}
.textFieldStyle(.roundedBorder)
.disabled(!enabled)
}
}
}