Home

Awesome

<p align="center"> <img src="https://raw.githubusercontent.com/FlineDev/BartyCrouch/main/Logo.png" width=600> </p> <p align="center"> <a href="https://github.com/FlineDev/BartyCrouch/actions?query=workflow%3ACI+branch%3Amain"> <img src="https://github.com/FlineDev/BartyCrouch/workflows/CI/badge.svg?branch=main" alt="CI Status"> </a> <a href="https://www.codacy.com/gh/FlineDev/BartyCrouch"> <img src="https://api.codacy.com/project/badge/Grade/7b34ad9193c2438aa32aa29a0490451f"/> </a> <a href="https://www.codacy.com/gh/FlineDev/BartyCrouch"> <img src="https://api.codacy.com/project/badge/Coverage/7b34ad9193c2438aa32aa29a0490451f"/> </a> <a href="https://github.com/FlineDev/BartyCrouch/releases"> <img src="https://img.shields.io/badge/Version-4.14.0-blue.svg" alt="Version: 4.14.0"> </a> <img src="https://img.shields.io/badge/Swift-5.7-FFAC45.svg" alt="Swift: 5.7"> <a href="https://github.com/FlineDev/BartyCrouch/blob/main/LICENSE.md"> <img src="https://img.shields.io/badge/License-MIT-lightgrey.svg" alt="License: MIT"> </a> <br /> <a href="https://paypal.me/Dschee/5EUR"> <img src="https://img.shields.io/badge/PayPal-Donate-orange.svg" alt="PayPal: Donate"> </a> <a href="https://github.com/sponsors/Jeehut"> <img src="https://img.shields.io/badge/GitHub-Become a sponsor-orange.svg" alt="GitHub: Become a sponsor"> </a> <a href="https://patreon.com/Jeehut"> <img src="https://img.shields.io/badge/Patreon-Become a patron-orange.svg" alt="Patreon: Become a patron"> </a> </p> <p align="center"> <a href="#installation">Installation</a> • <a href="#configuration">Configuration</a> • <a href="#usage">Usage</a> • <a href="#build-script">Build Script</a> • <a href="#donation">Donation</a> • <a href="#migration-guides">Migration Guides</a> • <a href="https://github.com/FlineDev/BartyCrouch/issues">Issues</a> • <a href="#contributing">Contributing</a> • <a href="#license">License</a> </p>

:sparkles: Important Notice :sparkles:

Apple introduced String Catalogs in Xcode 15 which implements many aspects of BartyCrouch like the incremental auto-extraction, or warning against empty translations. It's also fully backward-compatible with all iOS versions. Migrating is as simple as right-clicking a .strings file and choosing "Migrate to String Catalog...". I wrote a detailed FAQ about String Catalogs if you want to learn more. It's really awesome, everybody should migrate to it!

The only feature it's missing is machine translation, but I wrote an app to fill the gap and it supports even more translation services than BartyCrouch. Use TranslateKit in the future by simply drag & dropping the String Catalog file and letting it handle the translation, it's really easy.

Note that TranslateKit is being actively worked on. In comparison, BartyCrouch is kept up-to-date only by volunteers in the community.

BartyCrouch

BartyCrouch incrementally updates your Strings files from your Code and from Interface Builder files. "Incrementally" means that BartyCrouch will by default keep both your already translated values and even your altered comments. Additionally you can also use BartyCrouch for machine translating from one language to 60+ other languages. Using BartyCrouch is as easy as running a few simple commands from the command line what can even be automated using a build script within your project.

Checkout this blog post to learn how you can effectively use BartyCrouch in your projects.

Requirements

Getting Started

Installation

<details> <summary>Via <a href="https://brew.sh/">Homebrew</a></summary>

To install Bartycrouch the first time, simply run the command:

brew install bartycrouch

To update to the newest version of BartyCrouch when you have an old version already installed run:

brew upgrade bartycrouch
</details> <details> <summary>Via <a href="https://github.com/yonaskolb/Mint">Mint</a></summary>

To install or update to the latest version of BartyCrouch simply run this command:

mint install FlineDev/BartyCrouch
</details>

Configuration

To configure BartyCrouch for your project, first create a configuration file within your projects root directory. BartyCrouch can do this for you:

bartycrouch init

Now you should have a file named .bartycrouch.toml with the following contents:

[update]
tasks = ["interfaces", "code", "transform", "normalize"]

[update.interfaces]
paths = ["."]
subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"]
defaultToBase = false
ignoreEmptyStrings = false
unstripped = false
ignoreKeys = ["#bartycrouch-ignore!", "#bc-ignore!", "#i!"]

[update.code]
codePaths = ["."]
subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"]
localizablePaths = ["."]
defaultToKeys = false
additive = true
unstripped = false
ignoreKeys = ["#bartycrouch-ignore!", "#bc-ignore!", "#i!"]

[update.transform]
codePaths = ["."]
subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"]
localizablePaths = ["."]
transformer = "foundation"
supportedLanguageEnumPath = "."
typeName = "BartyCrouch"
translateMethodName = "translate"

[update.normalize]
paths = ["."]
subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"]
sourceLocale = "en"
harmonizeWithSource = true
sortByKeys = true

[lint]
paths = ["."]
subpathsToIgnore = [".git", "carthage", "pods", "build", ".build", "docs"]
duplicateKeys = true
emptyValues = true

This is the default configuration of BartyCrouch and should work for most projects as is. In order to use BartyCrouch to its extent, it is recommended though to consider making the following changes:

  1. To speed it up significantly, provide more specific paths for any key containing path if possible (especially in the update.transform section, e.g. ["App/Sources"] for codePaths or ["App/Supporting Files"] for supportedLanguageEnumPaths).
  2. Remove the code task if your project is Swift-only and you use the new transform update task.
  3. If you are using SwiftGen with the structured-swift4 template, you will probably want to use the transform task and change its transformer option to swiftgenStructured.
  4. If you decided to use the transform task, create a new file in your project (e.g. under SupportingFiles) named BartyCrouch.swift and copy the following code:
//  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/FlineDev/BartyCrouch

import Foundation

enum BartyCrouch {
    enum SupportedLanguage: String {
        // TODO: remove unsupported languages from the following cases list & add any missing languages
        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!"
    }
}
  1. If you don't develop in English as the first localized language, you should update the sourceLocale of the normalize task.
  2. If you want to use the machine translation feature of BartyCrouch, add translate to the tasks list at the top and copy the following section into the configuration file with secret replaced by your Microsoft Translator Text API Subscription Key:
[update.translate]
paths = "."
translator = "microsoftTranslator"
secret = "<#Subscription Key#>"
sourceLocale = "en"

Usage

Before using BartyCrouch please make sure you have committed your code. Also, we highly recommend using the build script method described below.


bartycrouch accepts one of the following sub commands:

Also the following command line options can be provided:

update subcommand

The update subcommand can be run with one or multiple of the following tasks:

In order to configure which tasks are executed, edit this section in the config file:

[update]
tasks = ["interfaces", "code", "transform", "normalize"]
<details><summary>Options for <code>interfaces</code></summary> </details> <details><summary>Options for <code>code</code></summary> </details> <details><summary>Options for <code>transform</code></summary> </details> <details><summary>Options for <code>translate</code></summary> </details> <details><summary>Options for <code>normalize</code></summary> </details>

lint subcommand

The lint subcommand was designed to analyze a project for typical translation issues. The current checks include:

Note that the lint command can be used both on CI and within Xcode via the build script method:

Localization Workflow via transform

When the transform update task is configured (see recommended step 4 in the Configuration section above) and you are using the build script method, you can use the following simplified process for writing localized code during development:

  1. Instead of NSLocalizedString calls you can use BartyCrouch.translate and specify a key, translations (if any) and optionally a comment. For example:
self.title = BartyCrouch.translate(key: "onboarding.first-page.header-title",  translations: [.english: "Welcome!"])
  1. Once you build your app, BartyCrouch will automatically add the new translation key to all your Localizable.strings files and add the provided translations as values for the provided languages.
  2. Additionally, during the same build BartyCrouch will automatically replace the above call to BartyCrouch.translate with the proper translation call, depending on your transformer option setting.

The resulting code depends on your transformer option setting:

When set to foundation, the above code will transform to:

self.title = NSLocalizedString("onboarding.first-page.header-title", comment: "")

When set to swiftgenStructured it will transform to:

self.title = L10n.Onboarding.FirstPage.headerTitle

Advantages of transform over the code task:

Disadvantages of transform over the code task:

NOTE: As of version 4.x of BartyCrouch formatted localized Strings are not supported by this automatic feature.

Localizing strings of LocalizableStringResource type (AppIntents, ...)

Historically, Apple platforms used CFCopyLocalizedString, and NSLocalizedString macros and their variants, to mark strings used in code to be localized, and to load their localized versions during runtime from Localizable.strings file.

Since introduction of the AppIntents framework, the localized strings in code can also be typed as LocalizedStringResource, and are no longer marked explicitly.

Let's examine this snippet of AppIntents code:

struct ExportAllTransactionsIntent: AppIntent {
    static var title: LocalizedStringResource = "Export all transactions"

    static var description =
        IntentDescription("Exports your transaction history as CSV data.")
}

In the example above, both the "Export all transactions", and "Exports your transaction history as CSV data." are actually StaticString instances that will be converted during compilation into LocalizedStringResource instances, and will lookup their respective localizations during runtime from Localized.strings file the same way as when using NSLocalizedString in the past. The only exception being that such strings are not marked explicitly, and require swift compiler to parse and extract such strings for localization. This is what Xcode does from version 13 when using Product -> Export Localizations... option.

In order to continue translating these strings with bartycrouch it is required to mark them explicitely with LocalizedStringResource(_: String, comment: String) call, and specify customFunction="LocalizedStringResource" in code task options.

The example AppIntents code that can be localized with bartycrouch will look like this:

struct ExportAllTransactionsIntent: AppIntent {
    static var title = LocalizedStringResource("Export all transactions", comment: "")

    static var description =
        IntentDescription(LocalizedStringResource("Exports your transaction history as CSV data.", comment: ""))
}

Note that you must use the full form of LocalizedStringResource(_: StaticString, comment: StaticString) for the bartycrouch, or more specifically for the extractLocStrings (see xcrun extractLocStrings) to properly parse the strings.

Build Script

In order to truly profit from BartyCrouch's ability to update & lint your .strings files you can make it a natural part of your development workflow within Xcode. In order to do this select your target, choose the Build Phases tab and click the + button on the top left corner of that pane. Select New Run Script Phase and copy the following into the text box below the Shell: /bin/sh of your new run script phase:

export PATH="$PATH:/opt/homebrew/bin"

if which bartycrouch > /dev/null; then
    bartycrouch update -x
    bartycrouch lint -x
else
    echo "warning: BartyCrouch not installed, download it from https://github.com/FlineDev/BartyCrouch"
fi
<img src="Images/Build-Script-Example.png">

Next, make sure the BartyCrouch script runs before the steps Compiling Sources (and SwiftGen if used) by moving it per drag & drop, for example right after Target Dependencies.

Now BartyCrouch will be run on each build and you won't need to call it manually ever (again). Additionally, all your co-workers who don't have BartyCrouch installed will see a warning with a hint on how to install it.

Note: Please make sure you commit your code using source control regularly when using the build script method.


Exclude specific Views / NSLocalizedStrings from Localization

Sometimes you may want to ignore some specific views containing localizable texts e.g. because their values are going to be set programmatically.

For these cases you can simply include #bartycrouch-ignore! or the shorthand #bc-ignore! into your value within your base localized Storyboard/XIB file. Alternatively you can add #bc-ignore! into the field "Comment For Localizer" box in the utilities pane.

This will tell BartyCrouch to ignore this specific view when updating your .strings files.

Here's an example of how a base localized view in a XIB file with partly ignored strings might look like:

<img src="Images/Exclusion-Example.png">

Here's an example with the alternative comment variant:

<div style="float:left;"> <img src="Images/IB-Comment-Exclusion-Example1.png" width="255px" height="437px"> <img src="Images/IB-Comment-Exclusion-Example2.png" width="254px" height="140px"> </div>

You can also use #bc-ignore! in your NSLocalizedString macros comment part to ignore them so they are not added to your Localizable.strings. This might be helpful when you are using a .stringsdict file to handle pluralization (see docs).

For example you can do something like this:

func updateTimeLabel(minutes: Int) {
  String.localizedStringWithFormat(NSLocalizedString("%d minute(s) ago", comment: "pluralized and localized minutes #bc-ignore!"), minutes)
}

The %d minute(s) ago key will be taken from Localizable.stringsdict file, not from Localizable.strings, that's why it should be ignored by BartyCrouch.

Donation

BartyCrouch was brought to you by Cihat Gündüz in his free time. If you want to thank me and support the development of this project, please make a small donation on PayPal. In case you also like my other open source contributions and articles, please consider motivating me by becoming a sponsor on GitHub or a patron on Patreon.

Thank you very much for any donation, it really helps out a lot! 💯

Migration Guides

See the file MIGRATION_GUIDES.md.

Contributing

Contributions are welcome. Feel free to open an issue on GitHub with your ideas or implement an idea yourself and post a pull request. If you want to contribute code, please try to follow the same syntax and semantic in your commit messages (see rationale here). Also, please make sure to add an entry to the CHANGELOG.md file which explains your change.

In order for the tests to run build issues, you need to run – also add an API key in the new file to run the translations tests, too:

cp Tests/BartyCrouchTranslatorTests/Secrets/secrets.json.sample Tests/BartyCrouchTranslatorTests/Secrets/secrets.json

After Release Checklist:

  1. Run make portable_zip to generate .build/release/portable_bartycrouch.zip
  2. Create new release with text from new CHANGELOG.md section & attach portable_bartycrouch.zip as binary
  3. Run pod trunk push to make a new release known to CocoaPods
  4. Update tag and revision in Formula/bartycrouch.rb, commit & push change
  5. Run brew bump-formula-pr bartycrouch --tag=<tag> --revision=<revision>

License

This library is released under the MIT License. See LICENSE for details.