Lokalisiere in Swift wie ein Profi

22. Februar 2019

Dieser Artikel ist eine Übersetzung des englischen Originals, ebenfalls von Cihat Gündüz.

Der Status Quo in Xcode

Als Entwickler wissen wir genau, dass Kontextwechsel ineffizient sind. Dies gilt jedoch nicht nur für CPUs, sondern auch für das Coden selbst. Oftmals ist es essentiell, dass wir uns während des Schreibens ganz auf den Code konzentrieren können. Deshalb versuchen Entwicklertools, uns bei den vielen kleinen Aufgaben zu unterstützen, die uns vom Schreiben des eigentlichen Codes ablenken könnten.

Xcode ist eine sehr gute Entwicklungsumgebung: Es hilft uns als App-Entwicklern sowohl bei grundlegenden Entwicklungsaufgaben (wie Codevervollständigung, Syntaxhervorhebung, Refactoring) als auch bei komplexeren Aufgaben. Hier wäre die Definition geräteunabhängiger Benutzeroberflächen (Interface Builder) ebenso zu nennen wie die verschiedenen Arten von Debugging- und Testwerkzeugen (View Hierarchy Debugger, UI / Performance Testing).

Fehlende Funktion Nr. 1: Lokalisierungen synchron halten

Aber bei einigen Aufgaben, die eindeutig nicht Teil der Programmierung sind, mangelt es Xcode an Komfort. Ein Beispiel dafür ist das Synchronisieren von Lokalisierungen:

  • Code-Dateien (z.B. NSLocalizedString) und Localizable.strings-Dateien
  • Storyboards / XIBs und ihre Strings-Dateien
  • Die verschiedenen Sprachvarianten einer Strings-Datei

Apple stellt tatsächlich Tools zur Verfügung, um Strings-Dateien zu aktualisieren, nämlich ibtool und die xcrun-Skripte genstrings und dessen Nachfolger extractLocStrings. Man kann sie in der Praxis in XCode erleben, wenn man zu einer bestehenden Strings-Datei eine Sprache hinzufügt: Xcode verwendet automatisch vorhandene Keys in der Ausgangssprache, um die Strings-Datei für neue Sprachen zu erstellen. Ihr bemerkt auch, dass Xcode beim ersten Lokalisieren einer Storyboard- oder XIB-Datei automatisch alle lokalisierbaren Views (wie UILabel) zu den entsprechenden Strings-Dateien hinzufügt. Aber genau hier endet die Lokalisierungsunterstützung von Xcode: Wenn man z. B. einem Storyboard ein neues Label hinzufügt, ergänzt Xcode dieses nicht automatisch in den Strings-Dateien.

Fehlende Funktion Nr. 2: Ressourcenzugriff

Ein weiteres Beispiel für fehlenden Komfort bei Xcode ist der Ressourcenzugriff. Genauer gesagt: Image Assets, Color Assets, Storyboards, XIBs und Localization Strings werden typischerweise alle in Code geladen, indem ein Stringliteral verwendet wird, wie im folgenden Beispiel:

title = NSLocalizedString("onboarding.page-one.title", Kommentar: "")

Diese dynamische String-Referenzmethode ist einfach zu nutzen, aber es fehlen zwei wichtige Funktionen:

  • Es gibt keine Compiler-Prüfungen, um sicherzustellen, dass eine Ressource (weiterhin) verfügbar ist.
  • Es gibt keine Autovervollständigung, was zu vielen Rechtschreibfehlern und genauen Namensüberprüfungen führt.

Im Vergleich dazu hat Google das Laden von Ressourcen viel eleganter gelöst. In Android Studio kann man auf [ähnliche Ressourcen] inklusive aller oben genannten Funktionen zugreifen:

title = R.string.onboarding.page_one.title

Xcode enttäuscht in dieser Hinsicht und hat immer noch keine praktikable Lösung.

Xcode mit Build-Skripten erweitern

Glücklicherweise müssen wir uns nicht auf Apple verlassen, um diese Probleme zu beheben. Die Entwickler-Community von Swift hat ihre eigenen Wege gefunden, um die Funktionen von Xcode zu erweitern und den Workflow zu verbessern. Eine solche Möglichkeit ist, Befehlszeilen-Tools bereitzustellen und sie bei jedem Build Ihrer App automatisch auszuführen. Dieser Ansatz erfordert zwar einige anfängliche Einstellungen und Konfigurationen, stellt sich aber als ziemlich einfach heraus, wenn er einmal verstanden wurde, und ist sehr leistungsfähig in Bezug auf die Möglichkeiten. Detaillierte Anweisungen zur Konfiguration eines Build-Skripts finden sich hier.

Die Werkzeuge, mit denen wir die beiden fehlenden Funktionen in unseren Xcode-Workflow integrieren werden, sind BartyCrouch und SwiftGen. Installiert sie auf eurem System einfach mithilfe von Homebrew:

brew install bartycrouch swiftgen

BartyCrouch

Erinnern wir uns, dass Xcode Tools zur Unterstützung der Lokalisierung enthält, diese aber nur in einigen seltenen Situationen einsetzt: Nun, BartyCrouch behebt das. Es durchsucht das Projekt automatisch nach Lokalisierungen und aktualisiert alle Strings-Dateien schrittweise. Das funktioniert in alle Richtungen:

  • Neue NSLocalizedString-Einträge werden zu den Localizable.strings-Locales hinzugefügt.
  • Neue lokalisierbare Ansichten in Storyboards / XIBs werden zu den Strings-Dateien hinzugefügt.
  • Gelöschte lokalisierbare Ansichten in Storyboards / XIBs werden aus ihren Strings-Dateien entfernt.
  • Alle Sprachvarianten einer Strings-Datei werden mit den Keys einer Ausgangssprache synchronisiert.

Aber das ist nicht alles, was BartyCrouch für uns tut. Es sortiert auch die Keys innerhalb der Strings-Dateien alphabetisch, um Keys mit dem gleichen Präfix automatisch zusammenzufassen und Merge-Konflikte zu minimieren. Außerdem bietet BartyCrouch die Möglichkeit, bestimmte Ansichten in Storyboards / XIBs von der Lokalisierung auszuschließen. Das ist nützlich, wenn man Labels hat, deren Werte programmgesteuert im Code gesetzt werden und daher nicht übersetzt werden sollten. Fügt einfach #bc-ignore! in das Feld Comment for Localizer im Interface Builder ein, wie hier beschrieben. Um BartyCrouch zu verwenden, müsst ihr dann nur noch dieses Build-Skript zu eurem Projekt hinzufügen:

if which bartycrouch > /dev/null; then
    bartycrouch update -x
else
    echo "warning: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch"
fi

Stellt sicher, dass das Build-Skript vor Compile Sources ausgeführt wird, indem ihr die Reihenfolge per Drag & Drop ändert. Aber das ist noch nicht alles, was BartyCrouch für uns tun kann:

Das Umschalten auf Strings-Dateien vollständig eliminieren

Der klassische Lokalisierungs-Workflow des Entwicklers in Xcode sieht so aus:

  • NSLocalizedString mit dem entsprechenden Lokalisierungs-Key aufrufen.
  • Auf die Datei Localizable.strings umschalten.
  • Einen neuen Key hinzufügen und eine erste Übersetzung für die Dev-Sprache bereitstellen.
  • Schritt 3 für alle unterstützten Sprachvarianten wiederholen, dabei Übersetzungen in andere Sprachen leer lassen.

Das oben genannte Build-Skript genügt bereits, damit BartyCrouch es ermöglicht, neue Keys zu Strings-Dateien ohne weitere Konfiguration hinzuzufügen. Allerdings ist es dann immer noch notwendig, auf die Strings-Datei(en) umzuschalten, um einige erste Übersetzungen zu liefern. Aber mit ein wenig Konfiguration lässt sich sogar dieser Schritt eliminieren:

  • Eine neue Swift-Datei zu dem Projekt hinzufügen (z.B. BartyCrouch.swift)
  • Den Inhalt durch den folgenden Code ersetzen (TODO bei Bedarf korrigieren):
//
//  This file is required in order for the `transform` task of the translation helper tool BartyCrouch to work.
//  See here for more details: https://github.com/Flinesoft/BartyCrouch
//

import Foundation

enum BartyCrouch {
    enum SupportedLanguage: String {
        // TODO: nicht unterstützte Sprachen aus der folgenden Liste entfernen und fehlende Sprachen hinzufügen
        case arabic = "ar"
        case chineseSimplified = "zh-Hans"
        case chineseTraditional = "zh-Hant"
        case english = "en"
        case french = "fr"
        case german = "de"
        case hindi = "hi"
        case italian = "it"
        case japanese = "ja"
        case korean = "ko"
        case malay = "ms"
        case portuguese = "pt-BR"
        case russian = "ru"
        case spanish = "es"
        case turkish = "tr"
    }

    static func translate(key: String, translations: [SupportedLanguage: String], comment: String? = nil) -> String {
        let typeName = String(describing: BartyCrouch.self)
        let methodName = #function

        print(
            "Warning: [BartyCrouch]",
            "Untransformed \(typeName).\(methodName) method call found with key '\(key)' and base translations '\(translations)'.",
            "Please ensure that BartyCrouch is installed and configured correctly."
        )

        // fall back in case something goes wrong with BartyCrouch transformation
        return "BC: TRANSFORMATION FAILED!"
    }
}

Das war alles!

Anstelle von NSLocalizedString ist es von nun an sinnvoll, BartyCrouch.translate zu verwenden: Beachtet, dass ihr im Dictionary eine oder mehrere Erstübersetzungen bereitstellen könnt. Beim nächsten Build fügt BartyCrouch sie euren Strings-Dateien hinzu, so dass ihr überhaupt nicht mehr zu ihnen wechseln müsst. Zudem ersetzt es auch automatisch den Aufruf BartyCrouch.translate durch den Aufruf von NSLocalizedString. Der resultierende Code bleibt also weiterhin das originale Foundation-Makro, wie wir es kennen. Ein optionaler Kommentarparameter kann ebenfalls angegeben werden, ist aber nicht erforderlich wie in NSLocalizedString.

Häufige Probleme in Strings-Dateien finden

Als verantwortungsbewusste Entwickler verwendet ihr bereits SwiftLint (oder einen anderen Code-Linter), um den Codestil einheitlich zu halten und einige häufige Fehler im Code zu vermeiden. Swift bietet viele Warnungen und Compiler-Checks selbst, so dass man in Sachen Code in guten Händen ist. Aber wusstet ihr, dass es auch bei Übersetzungen Probleme geben kann? Die häufigsten Probleme sind:

  • Mehrdeutigkeit der Übersetzung:
    • Mehrere Einträge desselben Keys innerhalb einer einzigen Strings-Datei
  • Fehlende Übersetzungen:
    • Leere Übersetzungen für Keys in ungetesteten Sprachvarianten Ihres Projekts

BartyCrouch hat einen integrierten Linter für Strings-Dateien, der zeilenspezifische Warnungen direkt in Xcode für diese beiden Probleme bereitstellt. Alles, was ihr tun müsst, ist, den Subcommand lint direkt nach dem Subcommand update im Build-Skript auszuführen:

if which bartycrouch > /dev/null; then
    bartycrouch update -x
#   bartycrouch lint -x
else
    echo "warning: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch"
fi

Wenn ihr nun eure App baut, seht ihr so etwas, wenn Probleme auftreten:

BartyCrouch-Fehlermeldung

BartyCrouch konfigurieren

Alle oben genannten Funktionen sind standardmäßig aktiviert, so dass ihr BartyCrouch überhaupt nicht konfigurieren müsst, um sie zu verwenden (denkt einfach an die Option -x im Build-Skript, um sicherzustellen, dass Warnungen in Xcode angezeigt werden).

Ihr könnt BartyCrouch mithilfe einer Konfigurationsdatei vollständig anpassen - ich empfehle auf jeden Fall eine solche zu verwenden, um die Leistung von BartyCrouch zu optimieren. Ihr werdet sie definitiv benötigen, wenn eure Entwicklungssprache nicht Englisch ist, da dies die Standardsprache ist. Das README hat einen guten Schritt-für-Schritt Konfigurationsabschnitt, so dass ich hier nicht weiter darauf eingehen werde. Schaut es euch einfach an.

SwiftGen

Erinnert ihr euch, wie Android Studio besser mit dem Ressourcenzugriff umgeht als Xcode, indem es sowohl Compile-Time-Checks als auch Autovervollständigung bietet? Nun, SwiftGen behebt das. Es durchsucht euer Projekt automatisch nach einer beliebigen Art von Ressourcen und erzeugt eine Swift-Datei mit einer Enum mit allen euren Ressourcennamen. Auf diese Weise erhaltet ihr sowohl Compile-Time Checks als auch Autovervollständigung beim Zugriff auf sie. Im Rahmen dieses Artikels erzeugen wir nur eine Enum für Übersetzungs-Strings, aber ihr könnt im README weitere Optionen finden, um z.B. referenzierte Bilder, Farben und Storyboards zu verwenden.

Um SwiftGen zu konfigurieren, müssen wir zunächst eine leere Strings.swift-Datei in unserem Projekt erstellen. Der Inhalt dieser Datei wird automatisch durch SwiftGen ersetzt, so dass es sinnvoll sein könnte, sie getrennt von anderem Code zu platzieren, z.B. in Resources/Generated. Als nächstes erstellt ihr eine Datei namens swiftgen.yml im Stammverzeichnis Ihres Projekts und fügt die folgenden Inhalte hinzu:

strings:
  inputs: path/to/your/Localizable.strings
  outputs:
    - templateName: structured-swift4
      output: path/to/your/Generated/Strings.swift

Die beiden Einträge von path/to/your ersetzt ihr durch die korrekten Pfade der entsprechenden Dateien und fügt dann dieses Build-Skript zu eurem Projekt hinzu:

if which swiftgen > /dev/null; then
    swiftgen
else
    echo "warning: SwiftGen not installed, download it from https://github.com/SwiftGen/SwiftGen"
fi

Als nächstes stellt ihr sicher, dass das SwiftGen-Build-Skript direkt nach BartyCrouch ausgeführt wird. Nachdem ihr euer Projekt zum ersten Mal gebaut habt, könnt ihr von nun an Aufrufe von NSLocalizedString durch Aufrufe der generierten Enum L10n (kurz für Localization, aber ohne die 10 Zeichen zwischen dem Anfangs-L und dem End-n) ersetzen. Beachtet, dass SwiftGen die Lokalisierungs-Keys automatisch nach . teilt und - oder _ für camelCasing verwendet. Keys wie ONBOARDING.PAGE_ONE.TITLE oder onboarding.page-one.title sind somit beide über einen Aufruf von L10n.Onboarding.PageOne.title verwendbar.

Kombinieren von BartyCrouch & SwiftGen.

Jetzt habt ihr Xcode um die beiden fehlenden Funktionen ergänzt, aber sie funktionieren noch nicht gut zusammen. Wenn ihr den Aufruf BartyCrouch.translate verwendet, um neue Lokalisierungen zu erstellen, wandelt BartyCrouch diesen beim nächsten Build in einen NSLocalizedString-Aufruf um, so dass man ihn noch durch einen L10n-Aufruf ersetzen muss. Aber BartyCrouch unterstützt uns auch hier: Ändert einfach den transformator der Konfigurationsoption in der Datei .bartycrouch.toml im Abschnitt [update.transform] von foundation auf swiftgenStructured. Dadurch wird sichergestellt, dass BartyCrouch das BartyCrouch.translate direkt in einen Aufruf von SwiftGens L10n statt in NSLocalizedString von Foundation umwandelt.

Pro-Lokalisierungs-Workflow

Nach all den vorherigen Schritten sieht der neue Lokalisierungs-Workflow für Entwickler so aus:

  • Aufrufen von BartyCrouch.translate mit Lokalisierungs-Keys und einer oder mehreren Übersetzungen (beliebig oft wiederholbar)
  • Projekt bauen - nachdem ihr Schritt 1 so oft wie nötig durchgeführt habt

Da der zweite Schritt sowieso irgendwann einmal fällig wird, ist es jetzt praktisch nur noch ein einstufiger Prozess.

Fazit

Durch die Konfiguration von BartyCrouch und SwiftGen für euer Projekt könnt ihr euren Lokalisierungs-Workflow in Xcode optimieren, wodurch er für Entwickler sowohl einfacher als auch sicherer wird, da Übersetzungsprobleme gleichzeitig vermieden werden. Das spart Zeit und Nerven.