rastrillo / native Public

Drain durable lanes with typed retry policy and localized sync status

Paul Campbell pushed by paul@keymail.dev 05bf4666060e271a83f56642047f6f5f913ac508 parent 111c15f
26 files changed, +728 −13
  • Sources/RastrilloApple/NativeSyncStrings.swift +27 −4
  • Sources/RastrilloApple/OutboxDrainer.swift +147 −8
  • Sources/RastrilloApple/Resources/ar.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/ar.lproj/Localizable.stringsdict +34 −0
  • Sources/RastrilloApple/Resources/bn.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/bn.lproj/Localizable.stringsdict +34 −0
  • Sources/RastrilloApple/Resources/en.lproj/Localizable.strings +11 −1
  • Sources/RastrilloApple/Resources/en.lproj/Localizable.stringsdict +34 −0
  • Sources/RastrilloApple/Resources/es.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/es.lproj/Localizable.stringsdict +38 −0
  • Sources/RastrilloApple/Resources/ga.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/ga.lproj/Localizable.stringsdict +34 −0
  • Sources/RastrilloApple/Resources/hi.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/hi.lproj/Localizable.stringsdict +34 −0
  • Sources/RastrilloApple/Resources/ja.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/ja.lproj/Localizable.stringsdict +34 −0
  • Sources/RastrilloApple/Resources/pt.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/pt.lproj/Localizable.stringsdict +38 −0
  • Sources/RastrilloApple/Resources/ru.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/ru.lproj/Localizable.stringsdict +40 −0
  • Sources/RastrilloApple/Resources/vi.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/vi.lproj/Localizable.stringsdict +34 −0
  • Sources/RastrilloApple/Resources/yue.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/yue.lproj/Localizable.stringsdict +34 −0
  • Sources/RastrilloApple/Resources/zh-Hans.lproj/Localizable.strings +11 −0
  • Sources/RastrilloApple/Resources/zh-Hans.lproj/Localizable.stringsdict +34 −0

CI failed — run details

diff --git a/Sources/RastrilloApple/NativeSyncStrings.swift b/Sources/RastrilloApple/NativeSyncStrings.swift
index 2d81af4..54e2855 100644
--- a/Sources/RastrilloApple/NativeSyncStrings.swift
+++ b/Sources/RastrilloApple/NativeSyncStrings.swift
@@ -31,10 +31,33 @@ public struct NativeSyncStrings: Sendable {
self.offlineDurable = offlineDurable; self.unsentLost = unsentLost
}
- public static var kit: NativeSyncStrings { kit(localization: "en") }
+ public static var kit: NativeSyncStrings { kit(localization: Bundle.module.preferredLocalizations.first ?? "en") }
public static func kit(localization: String) -> NativeSyncStrings {
- NativeSyncStrings(saving: "", retrying: "", offline: { _ in "" }, needsSignIn: { _ in "" }, signIn: "", failed: { _ in "" },
- offlineWaiting: "", offlineDurable: "", unsentLost: "")
+ let path = Bundle.module.path(forResource: localization, ofType: "lproj")
+ ?? Bundle.module.path(forResource: "en", ofType: "lproj")!
+ let bundle = Bundle(path: path)!
+ @Sendable func text(_ key: String) -> String { bundle.localizedString(forKey: key, value: nil, table: nil) }
+ @Sendable func plural(_ key: String, _ count: Int) -> String {
+ String(format: text(key), locale: Locale(identifier: localization), arguments: [count])
+ }
+ return NativeSyncStrings(saving: text("sync.saving"), retrying: text("sync.retrying"),
+ offline: { plural("sync.offline", $0) }, needsSignIn: { plural("sync.needsSignIn", $0) }, signIn: text("sync.signIn"),
+ failed: { reason in
+ switch reason {
+ case .cannotSaveOnDevice: return text("sync.failed.cannotSave")
+ case .tooManyUnsent: return text("sync.failed.tooManyUnsent")
+ case .held: return text("sync.failed.held")
+ }
+ }, offlineWaiting: text("offline.waiting"), offlineDurable: text("offline.durable"), unsentLost: text("sync.unsentLost"))
+ }
+ public func text(for status: SyncStatus) -> String? {
+ switch status {
+ case .saved: return nil
+ case .saving: return saving
+ case .retrying: return retrying
+ case .offline(let count): return offline(count)
+ case .needsSignIn(let count): return needsSignIn(count)
+ case .failed(_, let reason): return failed(reason)
+ }
}
- public func text(for status: SyncStatus) -> String? { nil }
}
diff --git a/Sources/RastrilloApple/OutboxDrainer.swift b/Sources/RastrilloApple/OutboxDrainer.swift
index 5350a5b..11c08ce 100644
--- a/Sources/RastrilloApple/OutboxDrainer.swift
+++ b/Sources/RastrilloApple/OutboxDrainer.swift
@@ -61,14 +61,153 @@ import RastrilloNative
public private(set) var phase: Phase = .idle
public private(set) var summary = OutboxSummary()
public private(set) var sessionEnded = false
+ private let transport: @MainActor () -> NativeTransport?
+ private let connectivity: NativeConnectivity
+ private let send: Send
+ private let maxBatchBytes: Int?
+ private var backoff: Backoff
+ private var draining = false
+ private var worker: Task<Void, Never>?
+ private var observation: Task<Void, Never>?
+ private var networkObservation: Task<Void, Never>?
+ private var timer: Task<Void, Never>?
+ private var wake: AsyncStream<Void>.Continuation?
+ private var retryDeadline: TimeInterval?
+ private var serverDeadline: TimeInterval?
+ private var now: TimeInterval { ProcessInfo.processInfo.systemUptime }
+
public init(outbox: Outbox, transport: @escaping @MainActor () -> NativeTransport?, connectivity: NativeConnectivity,
- maxBatchBytes: Int? = nil, backoff: Backoff = Backoff(), send: @escaping Send) { self.outbox = outbox }
- public func start() {}
- public func stop() {}
- public func kick() {}
- public func retryNow() {}
- public func sessionRestored() {}
- @discardableResult public func drain() async -> Outcome { .idle }
+ maxBatchBytes: Int? = nil, backoff: Backoff = Backoff(), send: @escaping Send) {
+ self.outbox = outbox; self.transport = transport; self.connectivity = connectivity
+ self.maxBatchBytes = maxBatchBytes; self.backoff = backoff; self.send = send
+ }
+ public func start() {
+ guard worker == nil else { return }
+ let (events, continuation) = AsyncStream<Void>.makeStream(bufferingPolicy: .bufferingNewest(1))
+ wake = continuation
+ observation = Task { [weak self, outbox] in
+ for await summary in await outbox.changes {
+ guard let self else { return }
+ if self.summary != summary { self.summary = summary; self.kick() }
+ }
+ }
+ let changes = connectivity.changes
+ networkObservation = Task { [weak self] in
+ for await _ in changes {
+ guard let self else { return }
+ self.backoff.reset(); self.retryDeadline = nil; self.kick()
+ }
+ }
+ worker = Task { [weak self] in
+ for await _ in events {
+ guard let self, !Task.isCancelled else { return }
+ if !self.connectivity.isPathSatisfied {
+ self.timer?.cancel(); self.timer = nil
+ self.phase = .waitingForNetwork; continue
+ }
+ let deadline = max(self.retryDeadline ?? 0, self.serverDeadline ?? 0)
+ if deadline > self.now { self.schedule(deadline - self.now); continue }
+ switch await self.drain() {
+ case .backingOff(let delay): self.retryDeadline = self.now + delay; self.schedule(delay)
+ case .rateLimited(let delay): self.schedule(delay)
+ default: break
+ }
+ }
+ }
+ kick()
+ }
+ private func schedule(_ delay: TimeInterval) {
+ timer?.cancel()
+ timer = Task { [weak self] in
+ do { try await Task.sleep(for: .seconds(delay)) } catch { return }
+ self?.kick()
+ }
+ }
+ public func stop() {
+ worker?.cancel(); observation?.cancel(); networkObservation?.cancel(); timer?.cancel(); wake?.finish()
+ worker = nil; observation = nil; networkObservation = nil; timer = nil; wake = nil
+ }
+ public func kick() { wake?.yield() }
+ public func retryNow() { retryDeadline = nil; backoff.reset(); kick() }
+ public func sessionRestored() { sessionEnded = false; retryNow() }
+ private func retry() -> Outcome { phase = .backingOff; return .backingOff(backoff.next()) }
+
+ @discardableResult public func drain() async -> Outcome {
+ guard !draining else { return .idle }
+ draining = true
+ defer { draining = false }
+ do { try await outbox.load() }
+ catch { summary = await outbox.summary; phase = .storeUnavailable; return .storeUnavailable }
+ summary = await outbox.summary
+ if sessionEnded { phase = .sessionEnded; return .sessionEnded }
+ if !connectivity.isPathSatisfied { phase = .waitingForNetwork; return .waitingForNetwork }
+ if let deadline = serverDeadline, deadline > now { phase = .backingOff; return .rateLimited(deadline - now) }
+ serverDeadline = nil
+ while !Task.isCancelled {
+ summary = await outbox.summary
+ var batch: [OutboxEntry] = []
+ do {
+ for lane in summary.lanes.keys.sorted() {
+ batch = try await outbox.beginSend(lane: lane, maxBytes: maxBatchBytes)
+ if !batch.isEmpty { break }
+ }
+ } catch { phase = .storeUnavailable; return .storeUnavailable }
+ guard let first = batch.first else { phase = .idle; return .idle }
+ let ids = batch.map(\.id)
+ guard let transport = transport() else {
+ await outbox.endSend(ids, failed: false); sessionEnded = true; phase = .sessionEnded; return .sessionEnded
+ }
+ phase = .sending
+ do {
+ let dispositions: [UUID: Disposition]
+ do { dispositions = try await send(first.lane, batch, transport) }
+ catch let error as NativeClientError {
+ let hold: Hold?
+ switch error {
+ case .forbidden, .notFound: hold = Hold(.accessLost)
+ case .conflict(let data): hold = Hold(.conflict(data))
+ case .tooLarge: hold = Hold(.tooLarge)
+ case .rejected(let status, _): hold = Hold(.rejected(String(status)))
+ default: hold = nil
+ }
+ guard let hold else { throw error }
+ dispositions = Dictionary(uniqueKeysWithValues: ids.map { ($0, .hold(hold)) })
+ }
+ var mustRetry = false; var progressed = false
+ for entry in batch {
+ switch dispositions[entry.id] {
+ case .sent: try await outbox.ack([entry.id]); progressed = true
+ case .hold(let hold): try await outbox.hold([entry.id], hold); progressed = true
+ case .retry: await outbox.endSend([entry.id]); mustRetry = true
+ case nil: break
+ }
+ }
+ await outbox.endSend(ids, failed: false)
+ summary = await outbox.summary
+ if mustRetry || !progressed { return retry() }
+ backoff.reset(); retryDeadline = nil
+ } catch {
+ await outbox.endSend(ids); summary = await outbox.summary
+ switch error {
+ case NativeClientError.unauthorized:
+ sessionEnded = true; phase = .sessionEnded; return .sessionEnded
+ case NativeClientError.offline where !connectivity.isPathSatisfied:
+ phase = .waitingForNetwork; return .waitingForNetwork
+ case NativeClientError.rateLimited(let after):
+ let delay = backoff.next(retryAfter: after); serverDeadline = now + delay
+ phase = .backingOff; return .rateLimited(delay)
+ default: return retry()
+ }
+ }
+ if !connectivity.isPathSatisfied { phase = .waitingForNetwork; return .waitingForNetwork }
+ }
+ phase = .idle; return .idle
+ }
public func status(lane: String? = nil, localFailure: SyncFailure? = nil,
- count: (OutboxSummary) -> Int = { $0.pendingLanes }) -> SyncStatus { .saved }
+ count: (OutboxSummary) -> Int = { $0.pendingLanes }) -> SyncStatus {
+ let state = lane.map { summary.only($0) } ?? summary
+ let offline = transport().map { connectivity.isOffline($0.origin) } ?? !connectivity.isPathSatisfied
+ return SyncStatus(summary: state, sending: state.inFlight > 0, sessionEnded: sessionEnded,
+ offline: offline, backingOff: phase == .backingOff, localFailure: localFailure, count: count)
+ }
}
diff --git a/Sources/RastrilloApple/Resources/ar.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/ar.lproj/Localizable.strings
new file mode 100644
index 0000000..3293509
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/ar.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "جارٍ الحفظ…";
+"sync.retrying" = "تعذّر الحفظ · جارٍ إعادة المحاولة…";
+"sync.signIn" = "تسجيل الدخول";
+"sync.failed.cannotSave" = "يتعذّر الحفظ على هذا الجهاز.";
+"sync.failed.tooManyUnsent" = "هناك تغييرات كثيرة غير مرسلة لا يمكن الاحتفاظ بها على هذا الجهاز. اتصل للحفظ.";
+"sync.failed.held" = "تعذّر حفظ بعض التغييرات.";
+"offline.waiting" = "أنت غير متصل. في انتظار الاتصال.";
+"offline.durable" = "أنت غير متصل. التغييرات محفوظة على هذا الجهاز وستُرسل عند الاتصال مجددًا.";
+"sync.unsentLost" = "تعذّرت قراءة التغييرات غير المرسلة على هذا الجهاز وتمت إزالتها.";
+"sync.offline" = "غير متصل · %d قيد الانتظار";
+"sync.needsSignIn" = "سجّل الدخول لإرسال %d قيد الانتظار";
diff --git a/Sources/RastrilloApple/Resources/ar.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/ar.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..7354e01
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/ar.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>سجّل الدخول لإرسال %d قيد الانتظار</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>غير متصل · %d قيد الانتظار</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/bn.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/bn.lproj/Localizable.strings
new file mode 100644
index 0000000..71594da
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/bn.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "সংরক্ষণ করা হচ্ছে…";
+"sync.retrying" = "সংরক্ষণে সমস্যা · আবার চেষ্টা করা হচ্ছে…";
+"sync.signIn" = "সাইন ইন করুন";
+"sync.failed.cannotSave" = "এই ডিভাইসে সংরক্ষণ করা যাচ্ছে না।";
+"sync.failed.tooManyUnsent" = "এই ডিভাইসে রাখার জন্য অনেক বেশি পরিবর্তন পাঠানো বাকি আছে। সংরক্ষণ করতে সংযোগ করুন।";
+"sync.failed.held" = "কিছু পরিবর্তন সংরক্ষণ করা যায়নি।";
+"offline.waiting" = "আপনি অফলাইনে আছেন। সংযোগের অপেক্ষায়।";
+"offline.durable" = "আপনি অফলাইনে আছেন। পরিবর্তনগুলো এই ডিভাইসে সংরক্ষিত আছে এবং আবার সংযোগ হলে পাঠানো হবে।";
+"sync.unsentLost" = "এই ডিভাইসে না পাঠানো পরিবর্তনগুলো পড়া যায়নি এবং সরিয়ে দেওয়া হয়েছে।";
+"sync.offline" = "অফলাইন · %dটি অপেক্ষমাণ";
+"sync.needsSignIn" = "অপেক্ষমাণ %dটি পাঠাতে সাইন ইন করুন";
diff --git a/Sources/RastrilloApple/Resources/bn.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/bn.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..07879af
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/bn.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>অপেক্ষমাণ %dটি পাঠাতে সাইন ইন করুন</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>অফলাইন · %dটি অপেক্ষমাণ</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/en.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/en.lproj/Localizable.strings
index c91be22..a039b03 100644
--- a/Sources/RastrilloApple/Resources/en.lproj/Localizable.strings
+++ b/Sources/RastrilloApple/Resources/en.lproj/Localizable.strings
@@ -1 +1,11 @@
-"stub" = "stub";
+"sync.saving" = "Saving…";
+"sync.retrying" = "Trouble saving · Retrying…";
+"sync.signIn" = "Sign in";
+"sync.failed.cannotSave" = "Can’t save on this device.";
+"sync.failed.tooManyUnsent" = "Too many unsent changes to keep on this device. Connect to save.";
+"sync.failed.held" = "Some changes could not be saved.";
+"offline.waiting" = "You’re offline. Waiting for a connection.";
+"offline.durable" = "You’re offline. Changes are saved on this device and will be sent when you reconnect.";
+"sync.unsentLost" = "Unsent changes on this device could not be read and were removed.";
+"sync.offline" = "Offline · %d pending";
+"sync.needsSignIn" = "Sign in to send %d pending";
diff --git a/Sources/RastrilloApple/Resources/en.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/en.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..9d3436d
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/en.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>Sign in to send %d pending</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>Offline · %d pending</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/es.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/es.lproj/Localizable.strings
new file mode 100644
index 0000000..8040158
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/es.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "Guardando…";
+"sync.retrying" = "Problemas al guardar · Reintentando…";
+"sync.signIn" = "Iniciar sesión";
+"sync.failed.cannotSave" = "No se puede guardar en este dispositivo.";
+"sync.failed.tooManyUnsent" = "Hay demasiados cambios sin enviar para guardarlos en este dispositivo. Conéctate para guardar.";
+"sync.failed.held" = "No se pudieron guardar algunos cambios.";
+"offline.waiting" = "Sin conexión. Esperando una conexión.";
+"offline.durable" = "Sin conexión. Los cambios se guardan en este dispositivo y se enviarán cuando vuelvas a conectarte.";
+"sync.unsentLost" = "No se pudieron leer los cambios sin enviar de este dispositivo y se eliminaron.";
+"sync.offline" = "Sin conexión · %d pendientes";
+"sync.needsSignIn" = "Inicia sesión para enviar %d pendientes";
diff --git a/Sources/RastrilloApple/Resources/es.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/es.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..6ecc453
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/es.lproj/Localizable.stringsdict
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>one</key>
+ <string>Inicia sesión para enviar %d pendiente</string>
+ <key>other</key>
+ <string>Inicia sesión para enviar %d pendientes</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>one</key>
+ <string>Sin conexión · %d pendiente</string>
+ <key>other</key>
+ <string>Sin conexión · %d pendientes</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/ga.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/ga.lproj/Localizable.strings
new file mode 100644
index 0000000..34423ac
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/ga.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "Á shábháil…";
+"sync.retrying" = "Fadhb leis an sábháil · Ag triail arís…";
+"sync.signIn" = "Sínigh isteach";
+"sync.failed.cannotSave" = "Ní féidir sábháil ar an ngléas seo.";
+"sync.failed.tooManyUnsent" = "Tá an iomarca athruithe gan seoladh le coinneáil ar an ngléas seo. Ceangail chun sábháil.";
+"sync.failed.held" = "Níorbh fhéidir roinnt athruithe a shábháil.";
+"offline.waiting" = "Tá tú as líne. Ag fanacht le nasc.";
+"offline.durable" = "Tá tú as líne. Tá na hathruithe sábháilte ar an ngléas seo agus seolfar iad nuair a athcheanglóidh tú.";
+"sync.unsentLost" = "Níorbh fhéidir na hathruithe gan seoladh ar an ngléas seo a léamh agus baineadh iad.";
+"sync.offline" = "As líne · %d ar feitheamh";
+"sync.needsSignIn" = "Sínigh isteach chun %d atá ar feitheamh a sheoladh";
diff --git a/Sources/RastrilloApple/Resources/ga.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/ga.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..98b3c0f
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/ga.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>Sínigh isteach chun %d atá ar feitheamh a sheoladh</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>As líne · %d ar feitheamh</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/hi.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/hi.lproj/Localizable.strings
new file mode 100644
index 0000000..482b18e
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/hi.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "सहेजा जा रहा है…";
+"sync.retrying" = "सहेजने में समस्या · फिर से कोशिश हो रही है…";
+"sync.signIn" = "साइन इन करें";
+"sync.failed.cannotSave" = "इस डिवाइस पर सहेजा नहीं जा सकता।";
+"sync.failed.tooManyUnsent" = "इस डिवाइस पर रखने के लिए बहुत अधिक बदलाव भेजे जाने बाकी हैं। सहेजने के लिए कनेक्ट करें।";
+"sync.failed.held" = "कुछ बदलाव सहेजे नहीं जा सके।";
+"offline.waiting" = "आप ऑफ़लाइन हैं। कनेक्शन की प्रतीक्षा है।";
+"offline.durable" = "आप ऑफ़लाइन हैं। बदलाव इस डिवाइस पर सहेजे गए हैं और दोबारा कनेक्ट होने पर भेजे जाएँगे।";
+"sync.unsentLost" = "इस डिवाइस पर न भेजे गए बदलाव पढ़े नहीं जा सके और हटा दिए गए।";
+"sync.offline" = "ऑफ़लाइन · %d लंबित";
+"sync.needsSignIn" = "%d लंबित बदलाव भेजने के लिए साइन इन करें";
diff --git a/Sources/RastrilloApple/Resources/hi.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/hi.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..5667c87
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/hi.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>%d लंबित बदलाव भेजने के लिए साइन इन करें</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>ऑफ़लाइन · %d लंबित</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/ja.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/ja.lproj/Localizable.strings
new file mode 100644
index 0000000..582c4f0
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/ja.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "保存中…";
+"sync.retrying" = "保存できません · 再試行中…";
+"sync.signIn" = "サインイン";
+"sync.failed.cannotSave" = "このデバイスに保存できません。";
+"sync.failed.tooManyUnsent" = "このデバイスに保存できる未送信の変更が多すぎます。接続して保存してください。";
+"sync.failed.held" = "一部の変更を保存できませんでした。";
+"offline.waiting" = "オフラインです。接続を待っています。";
+"offline.durable" = "オフラインです。変更はこのデバイスに保存されており、再接続すると送信されます。";
+"sync.unsentLost" = "このデバイスの未送信の変更を読み取れなかったため、削除しました。";
+"sync.offline" = "オフライン · %d 件送信待ち";
+"sync.needsSignIn" = "サインインして送信待ちの %d 件を送信";
diff --git a/Sources/RastrilloApple/Resources/ja.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/ja.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..b9b1d09
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/ja.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>サインインして送信待ちの %d 件を送信</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>オフライン · %d 件送信待ち</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/pt.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/pt.lproj/Localizable.strings
new file mode 100644
index 0000000..409ee71
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/pt.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "A guardar…";
+"sync.retrying" = "Problemas ao guardar · A tentar novamente…";
+"sync.signIn" = "Iniciar sessão";
+"sync.failed.cannotSave" = "Não é possível guardar neste dispositivo.";
+"sync.failed.tooManyUnsent" = "Há demasiadas alterações por enviar para guardar neste dispositivo. Ligue-se para guardar.";
+"sync.failed.held" = "Não foi possível guardar algumas alterações.";
+"offline.waiting" = "Sem ligação. A aguardar uma ligação.";
+"offline.durable" = "Sem ligação. As alterações estão guardadas neste dispositivo e serão enviadas quando voltar a ligar-se.";
+"sync.unsentLost" = "Não foi possível ler as alterações por enviar neste dispositivo e foram removidas.";
+"sync.offline" = "Sem ligação · %d pendentes";
+"sync.needsSignIn" = "Inicie sessão para enviar %d pendentes";
diff --git a/Sources/RastrilloApple/Resources/pt.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/pt.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..0adbbe8
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/pt.lproj/Localizable.stringsdict
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>one</key>
+ <string>Inicie sessão para enviar %d pendente</string>
+ <key>other</key>
+ <string>Inicie sessão para enviar %d pendentes</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>one</key>
+ <string>Sem ligação · %d pendente</string>
+ <key>other</key>
+ <string>Sem ligação · %d pendentes</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/ru.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/ru.lproj/Localizable.strings
new file mode 100644
index 0000000..6b4c740
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/ru.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "Сохранение…";
+"sync.retrying" = "Не удаётся сохранить · Повторная попытка…";
+"sync.signIn" = "Войти";
+"sync.failed.cannotSave" = "Не удаётся сохранить на этом устройстве.";
+"sync.failed.tooManyUnsent" = "Слишком много неотправленных изменений для хранения на этом устройстве. Подключитесь к сети для сохранения.";
+"sync.failed.held" = "Не удалось сохранить некоторые изменения.";
+"offline.waiting" = "Нет подключения. Ожидание сети.";
+"offline.durable" = "Нет подключения. Изменения сохранены на этом устройстве и будут отправлены при подключении к сети.";
+"sync.unsentLost" = "Не удалось прочитать неотправленные изменения на этом устройстве, и они были удалены.";
+"sync.offline" = "Не в сети · %d ожидают отправки";
+"sync.needsSignIn" = "Войдите, чтобы отправить ожидающие изменения: %d";
diff --git a/Sources/RastrilloApple/Resources/ru.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/ru.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..c204339
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/ru.lproj/Localizable.stringsdict
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>Войдите, чтобы отправить ожидающие изменения: %d</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>few</key>
+ <string>Не в сети · %d ожидают отправки</string>
+ <key>many</key>
+ <string>Не в сети · %d ожидают отправки</string>
+ <key>one</key>
+ <string>Не в сети · %d ожидает отправки</string>
+ <key>other</key>
+ <string>Не в сети · %d ожидают отправки</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/vi.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/vi.lproj/Localizable.strings
new file mode 100644
index 0000000..d96fa16
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/vi.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "Đang lưu…";
+"sync.retrying" = "Gặp sự cố khi lưu · Đang thử lại…";
+"sync.signIn" = "Đăng nhập";
+"sync.failed.cannotSave" = "Không thể lưu trên thiết bị này.";
+"sync.failed.tooManyUnsent" = "Có quá nhiều thay đổi chưa gửi để lưu trên thiết bị này. Hãy kết nối để lưu.";
+"sync.failed.held" = "Không thể lưu một số thay đổi.";
+"offline.waiting" = "Bạn đang ngoại tuyến. Đang chờ kết nối.";
+"offline.durable" = "Bạn đang ngoại tuyến. Các thay đổi đã được lưu trên thiết bị này và sẽ được gửi khi bạn kết nối lại.";
+"sync.unsentLost" = "Không thể đọc các thay đổi chưa gửi trên thiết bị này nên chúng đã bị xóa.";
+"sync.offline" = "Ngoại tuyến · %d đang chờ";
+"sync.needsSignIn" = "Đăng nhập để gửi %d mục đang chờ";
diff --git a/Sources/RastrilloApple/Resources/vi.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/vi.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..7f20433
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/vi.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>Đăng nhập để gửi %d mục đang chờ</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>Ngoại tuyến · %d đang chờ</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/yue.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/yue.lproj/Localizable.strings
new file mode 100644
index 0000000..8222134
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/yue.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "儲存中…";
+"sync.retrying" = "儲存遇到問題 · 重試中…";
+"sync.signIn" = "登入";
+"sync.failed.cannotSave" = "無法喺呢部裝置儲存。";
+"sync.failed.tooManyUnsent" = "未傳送嘅更改太多,無法保留喺呢部裝置。請連線儲存。";
+"sync.failed.held" = "部分更改無法儲存。";
+"offline.waiting" = "你已離線。等候連線中。";
+"offline.durable" = "你已離線。更改已儲存喺呢部裝置,重新連線後就會傳送。";
+"sync.unsentLost" = "無法讀取呢部裝置上未傳送嘅更改,已將佢哋移除。";
+"sync.offline" = "離線 · %d 項等候傳送";
+"sync.needsSignIn" = "登入以傳送 %d 項等候中嘅更改";
diff --git a/Sources/RastrilloApple/Resources/yue.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/yue.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..afc8745
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/yue.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>登入以傳送 %d 項等候中嘅更改</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>離線 · %d 項等候傳送</string>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/Sources/RastrilloApple/Resources/zh-Hans.lproj/Localizable.strings b/Sources/RastrilloApple/Resources/zh-Hans.lproj/Localizable.strings
new file mode 100644
index 0000000..0e15cba
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/zh-Hans.lproj/Localizable.strings
@@ -0,0 +1,11 @@
+"sync.saving" = "正在保存…";
+"sync.retrying" = "保存遇到问题 · 正在重试…";
+"sync.signIn" = "登录";
+"sync.failed.cannotSave" = "无法在此设备上保存。";
+"sync.failed.tooManyUnsent" = "未发送的更改过多,无法保存在此设备上。请联网保存。";
+"sync.failed.held" = "部分更改无法保存。";
+"offline.waiting" = "你已离线。正在等待连接。";
+"offline.durable" = "你已离线。更改已保存在此设备上,重新连接后将发送。";
+"sync.unsentLost" = "无法读取此设备上未发送的更改,已将其移除。";
+"sync.offline" = "离线 · %d 项待发送";
+"sync.needsSignIn" = "登录以发送 %d 项待处理的更改";
diff --git a/Sources/RastrilloApple/Resources/zh-Hans.lproj/Localizable.stringsdict b/Sources/RastrilloApple/Resources/zh-Hans.lproj/Localizable.stringsdict
new file mode 100644
index 0000000..5345c5f
--- /dev/null
+++ b/Sources/RastrilloApple/Resources/zh-Hans.lproj/Localizable.stringsdict
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>sync.needsSignIn</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>登录以发送 %d 项待处理的更改</string>
+ </dict>
+ </dict>
+ <key>sync.offline</key>
+ <dict>
+ <key>NSStringLocalizedFormatKey</key>
+ <string>%#@n@</string>
+ <key>n</key>
+ <dict>
+ <key>NSStringFormatSpecTypeKey</key>
+ <string>NSStringPluralRuleType</string>
+ <key>NSStringFormatValueTypeKey</key>
+ <string>d</string>
+ <key>other</key>
+ <string>离线 · %d 项待发送</string>
+ </dict>
+ </dict>
+</dict>
+</plist>