Validate iOS, Android, and Mac localizations. Find errors in .strings, .stringsdict, and strings.xml files.

Overview

Locheck

Swift 5.4 License Build

An Xcode and Android localization file validator. Make sure your .strings, .stringsdict, and strings.xml files do not have any errors!

What does it do?

Locheck can perform many kinds of checks on localization files. The simplest one is making sure all strings appear in both the base language and translations, but it can also make sure all your format specifiers are consistent, even in .stringsdict files.

Consider this string:

"Send %d donuts to %@" = "%@ to donuts %d send";
Send %d donuts to %s %s to donuts %d send ">

<string name="send_donuts">Send %d donuts to %sstring>

<string name="send_donuts">%s to donuts %d sendstring>

The translation reads naturally on its own, but this would crash your app when iOS or Android tries to format a number as a string and a string as a number. Instead, the translation should look like this:

"Send %d donuts to %@" = "%1$@ to donuts %2$d send";
%2$s to donuts %1$d send ">

<string name="send_donuts">%2$s to donuts %1$d sendstring>

Locheck will make sure you get it right.

In the example above, the key happens to be equal to the base translation. But you might have special cases where you manually define your string in Localizable.strings, so the key's format string doesn't match the value:

// in en.lproj/Localizable.strings:
"send-donuts" = "Send %d donuts to %@";

// in backwards.lproj/Localizable.strings:
"send-donuts" = "%@ to donuts %d send";

In these cases, Locheck will use the base translation's value (not its key) as the authoritative string, and would catch the error in the example above.

Installation

Manual

git clone [email protected]:Asana/locheck.git
cd locheck
make install

Using Mint

mint install Asana/locheck
mint run locheck [...]

# or link it to /usr/local/bin
mint install Asana/locheck --link
locheck [...]

Locheck is not yet popular enough to be in homebrew/core and we haven't created a tap yet.

Usage

There are a few ways to invoke locheck depending on how much magic you want. In all cases, Locheck will write to stderr for Xcode integration, and stdout for a human-readable summary. Pull requests for additional output formats will probably be accepted.

discoverlproj

The simplest way to use Locheck with Xcode is to use discoverlproj and point to a directory containing all your .lproj files:

locheck discoverlproj "MyApp/Supporting Files" --default en # use English as the base language

If you use a language besides English as your base, you'll need to pass it as an argument as shown in the example. Locheck does not try to read your xcodeproj file to figure it out.

discovervalues

The simplest way to use Locheck on Android is to use discovervalues and point to a directory containing all your values[-*] directories, i.e. your res/ directory.

locheck discovervalues ./app/src/main/res

Other ways

Run locheck --help to see a list of all commands. The rest of the commands just let you directly compare individual files of different types.

Example output

> locheck discovervalues $ANDROID/app/src/main/res --ignore key_missing_from_translation --ignore key_missing_from_base
Discovering values[-*]/strings.xml files in /.../app/src/main/res
Source of truth: /.../app/src/main/res/values/strings.xml
Translations to check: 12
/.../app/src/main/res/values-de/strings.xml:242: error: Translation of 'could_not_mark_as_milestone' includes arguments that don't exist in the source: task_name (string_has_extra_arguments)
/.../app/src/main/res/values-de/strings.xml:899: warning: 'organization_required_mfa_help_text' does not include argument(s): authy_url, duo_mobile_url, microsoft_authenticator_url (phrase_has_missing_arguments)
/.../app/src/main/res/values-ko/strings.xml:1403: error: Translation of 'what_are_a_few_tasks_you_have_to_do_for_project_name' includes arguments that don't exist in the source: projectName (string_has_extra_arguments)
/.../app/src/main/res/values-ko/strings.xml:1426: warning: 'created_video_phrase_template' does not include argument(s): author_name (phrase_has_missing_arguments)
[...]

Summary:
/.../app/src/main/res/values-ko/strings.xml
    could_not_mark_as_milestone:
        ERROR: Translation of 'could_not_mark_as_milestone' includes arguments that don't exist in the source: task_name
    created_video_phrase_template:
        WARNING: 'created_video_phrase_template' does not include argument(s): author_name
        ERROR: Translation of 'created_video_phrase_template' includes arguments that don't exist in the source: userName1
    organization_required_mfa_help_text:
        WARNING: 'organization_required_mfa_help_text' does not include argument(s): authy_url, duo_mobile_url, microsoft_authenticator_url
    what_are_a_few_tasks_you_have_to_do_for_project_name:
        WARNING: 'what_are_a_few_tasks_you_have_to_do_for_project_name' does not include argument(s): project_name
        ERROR: Translation of 'what_are_a_few_tasks_you_have_to_do_for_project_name' includes arguments that don't exist in the source: projectName
[...]
20 warnings, 29 errors
Ignored key_missing_from_translation, key_missing_from_base
Errors found

Contributing

GitHub issues and pull requests are very welcome! Please format your code with swiftformat Sources Tests before opening your PR, otherwise tests will fail and we cannot merge your branch. We also run SwiftLint to help ensure best practices.

The simplest way to install SwiftFormat and SwiftLint is to use Mint:

brew install mint
mint bootstrap --link`

You can then run both tools locally:

swiftformat Sources Tests
swiftlint lint --quiet

Further reading

Comments
  • Allow treating warnings as errors

    Allow treating warnings as errors

    Heyo :)

    I love that you wrote this (and open-sourced!), since it solved a big pain point I've had before with localisation tools not being harsh enough in validation.

    Would you consider adding a semi-stable (it's before 1.0, so... whatever goes) output format that could be parsed by other tools? My use-case is to have locheck be part of our CI suite, so we can make sure the strings we're merging are correct, but it's currently not super nice for that use-case. (I'm currently parsing the stdout output, and looking for lines that begin with "WARNING: or ERROR:, which, workable, but... yeah.)

    JSON would probably be the easiest?

    And I know that the README says:

    Pull requests for additional output formats will probably be accepted.

    but I wanted to check in with y'all if this is something you'd be even interested in / maybe are working on already, before I start hacking on it.

    opened by jklausa 8
  • Duplicate keys in different `.strings` files are incorrectly reported as missing/extra argument warnings

    Duplicate keys in different `.strings` files are incorrectly reported as missing/extra argument warnings

    My app has strings split across multiple .strings files, and some of those strings have the same key. Since all the strings were merged into one list in #19, duplicate keys aren't handled correctly since later occurrences of a key overwrite earlier ones when building the lookup dictionary. This can lead to various incorrect reports:

    • warnings that the number of arguments don't match between languages (really, it's duplicate keys with different # of arguments)
    • may cause missing localizations to be ignored, if they have at least one instance of the duplicate but not all

    I may attempt to fix these issues, but I need to understand the rationale behind coalescing the .strings files in the first place.

    opened by jayrhynas 4
  • [discovervalues] 'missing' error if a values directory does not contain strings.xml

    [discovervalues] 'missing' error if a values directory does not contain strings.xml

    Hello, first off all let me say how awesome this tool is! I've run it on our iOS project and it works like a charm and since you have Android support, I also really wanted to try it out on our Android project!

    When doing so, I hit the following issue:

    $ locheck discovervalues ~/Code/GitHub/cookpad/global-android/ui-commons/src/main/res --ignore-missing
    Error: Files encountered an error at '/Users/.../global-android/ui-commons/src/main/res/values-en-rGB/strings.xml'.
    Reason: missing
    

    I'm not too familiar with our Android project, but our base strings.xml is in the values directory, we then have some regional directories for values-en-rGB and so on but we don't localise strings for different regions (at least yet) so the values-en-rGB/strings.xml doesn't exist.

    Now I figure this is something relatively easy to patch so I'm happy to try and submit a PR, but first i wanted to check with you on the expected behaviour. Is our project configuration common by not having strings.xml in every values directory? I guess I see at least three ways to fix this:

    1. Ignore the values directory if it doesn't contain strings.xml
    2. The above, but only when --ignore-missing is set?
    3. A different option for passing locales/directories to ignore?

    Let me know your thoughts! Thanks again 🚀

    opened by liamnichols 4
  • Provide ability to validate base language w/ out comparison

    Provide ability to validate base language w/ out comparison

    It would be nice if the tool could validate the base language without comparing it to translations to see if there are issues with things like position identifiers within stringsdict files. This would be great as an initial check that things are being properly formatted before being sent to translators, for instance.

    opened by namolnad 4
  • Extend `stringPairRegex` to accept more valid lines

    Extend `stringPairRegex` to accept more valid lines

    Currently, locheck will ignore lines in .strings files that contain comments. It will also ignore lines if there is not exactly one space character before and after the = character in the line. These forms are however considered valid by Xcode and should not be ignored.

    This PR extends stringPairRegex to match and ignore comments at the beginning and end of lines, and adjusts the regex to match *= * instead of =.

    Currently, the regex still does not handle multi-line block comments that begin or end on the same line as a valid key-value pair. I'm undecided if we should just allow unbalanced */ or /* patterns at the beginning or end of a line, respectively, or if a broader multi-line parser would be more appropriate.

    opened by jayrhynas 3
  • unknown type: 'string-array' (xml_schema_error)

    unknown type: 'string-array' (xml_schema_error)

    String arrays are a common practice in Android string xml files: https://developer.android.com/guide/topics/resources/string-resource#StringArray But they seem to not be recognized by locheck (above error)

    opened by nbleiberg 2
  • Most of the logic for validating stringsdict

    Most of the logic for validating stringsdict

    1. Parsing and grammar

    Parses strings like Hello %#@bob@ into lists of enums like [.constant("Hello "), .variable("bob")] stored on LexedStringsdictString objects. (Open to suggestions for the name.)

    It then uses a basic recursive function to walk the grammar formed by the stringsdict entry's format key and rules to generate all possible permutations.

    Example input:

    transportation:
      format key: "%#@cars@ and %#@motorcycles@"
      cars:
        one: "one car"
        other: "%1$d cars"
      motorcycles:
        one: "one motorcycle with %#@sidecars@"
        other: "%2$d motorcycle with %#@sidecars@"
      sidecars:
        zero: "no sidecar"
        one: "one sidecar"
        other: "%3$d sidecars"
    

    Example output:

    [
     "one car and one motorcycle with no sidecar",
     "one car and one motorcycle with one sidecar",
     "one car and one motorcycle with %3$d sidecars",
     "one car and %2$d motorcycles with no sidecar",
     "one car and %2$d motorcycles with one sidecar",
     "one car and %2$d motorcycles with %3$d sidecars",
     "%1$d cars and one motorcycle with no sidecar",
     "%1$d cars and one motorcycle with one sidecar",
     "%1$d cars and one motorcycle with %3$d sidecars",
     "%1$d cars and %2$d motorcycles with no sidecar",
     "%1$d cars and %2$d motorcycles with one sidecar",
     "%1$d cars and %2$d motorcycles with %3$d sidecars",
    ]
    

    2. Canonical argument list

    Parses all permutations to find arguments, then makes sure each argument at each position has a specifier matching the other permutations.

    opened by stevelandeyasana 2
  • Update iOS example flag

    Update iOS example flag

    --default doesn't exist and it's not mentioned in help. I think it was meant to be --base:

    OVERVIEW: Automatically find .lproj files within a directory and compare them
    
    USAGE: locheck discoverlproj [--base <base>] [<directories> ...] [--ignore <ignore> ...] [--ignore-missing] [--ignore-warnings] [--treat-warnings-as-errors]
    
    ARGUMENTS:
      <directories>           One or more directories full of .lproj files, with one of them being authoritative (defined by --base). 
    
    OPTIONS:
      --base <base>           The authoritative language. Defaults to 'en'.  (default: en)
      --ignore <ignore>       Ignore a rule completely. 
      --ignore-missing        Ignore 'missing string' errors. Shorthand for '--ignore key_missing_from_base --ignore key_missing_from_translation'. 
      --ignore-warnings       Ignore all warning-level issues. 
      --treat-warnings-as-errors
                              Return a non-zero exit code if any warnings, not just errors, were encountered. 
      -h, --help              Show help information.
    
    opened by marcelofabri 1
  • --treat-warnings-as-errors

    --treat-warnings-as-errors

    Fix #26. Adds a --treat-warnings-as-errors flag to every command that causes warnings to result in a nonzero exit code even without errors.

    I skipped unit tests because this code is very obvious. I did make test files by hand and verify the behavior.

    opened by stevelandeyasana 1
  • Fix assignment of implicit positions

    Fix assignment of implicit positions

    Fix #7. Implicit position assignment acts as if explicitly-positioned arguments were not there. I determined this experimentally.

    I would be very surprised if Android had different behavior, since this all seems to be based on C printf() behavior.

    opened by stevelandeyasana 1
  • Parse and lightly validate Android strings

    Parse and lightly validate Android strings

    This PR officially makes Locheck a cross-platform project!

    Adds support for an androidstrings command which can parse strings.xml and compare two translations to make sure all keys are present in both. Additional validation will follow in another PR but will be slightly simpler than Stringsdict validation.

    I tested this by running it on Asana's English and German translations and found a bunch of missing keys that are really missing from German.

    opened by stevelandeyasana 1
  • locheck detects orphan strings in base, but not in the other files

    locheck detects orphan strings in base, but not in the other files

    So if I have a Base localisation strings file with 2 entries in it, and a fr with 3 entries, and run locehck with --base Base, it doesn't see any problem. It seem like that this situation should be noticed as a warning?

    As a work around, I am running locheck multiple times with every localisation file as --base, so it catches all possible orphans:

    locheck ... -base Base
    locheck ... -base fr
    
    opened by alex-hunsley-nl 2
  • Locheck hangs on some // comments

    Locheck hangs on some // comments

    Locheck was hanging for me. I found out the problem was a line like this:

    ////////////////////////

    I also found out that a line with a comment and no characters after the slashes has the same effect:

    //

    opened by alex-hunsley-nl 2
  • Detect when base text is used for other languages

    Detect when base text is used for other languages

    Thanks for this great tool, I have an improvement suggestion but not sure if it would be useful for everyone: a common case is when developers is waiting for translation is to put the English (base) text for other languages and forget about it.

    Cheers !

    opened by taher-mosbah 4
  • Detect when base text is used for other languages

    Detect when base text is used for other languages

    Thanks for this great tool, I have an improvement suggestion but not sure if it would be useful for everyone: a common case is when developers is waiting for translation is to put the English (base) text for other languages and forget about it. I can open a PR with this added if you think it could be useful. Cheers !

    opened by taher-mosbah 0
Releases(0.9.8)
Owner
Asana
Asana
Parse iOS mobile provisioning files into Swift models

SwiftyProvisioningProfile This library provides a way to decode a .mobileprovision file into a Swift model. Installation The recommended installation

James Sherlock 60 Nov 25, 2022
Transform strings easily in Swift.

swift-string-transform Transform strings easily in Swift. Table of Contents Installation How to use Contribution Installation Swift Package Manager (R

null 18 Apr 21, 2022
A simple Swift utility for producing pseudolocalized strings.

Build your App UI to adapt and respond to translations, and find localization bugs!

Reece Como 2 Sep 28, 2021
Swift Xid - Xid uses MongoDB Object ID algorighm1 to generate globally unique ids with base32 serialzation to produce shorter strings

Swift Xid - Xid uses MongoDB Object ID algorighm1 to generate globally unique ids with base32 serialzation to produce shorter strings

Uditha Atukorala 0 Jun 13, 2022
Zip - A Swift framework for zipping and unzipping files. Simple and quick to use. Built on top of minizip.

Zip A Swift framework for zipping and unzipping files. Simple and quick to use. Built on top of minizip. Usage Import Zip at the top of the Swift file

Roy Marmelstein 2.3k Jan 3, 2023
ZIP Foundation is a library to create, read and modify ZIP archive files.

ZIP Foundation is a library to create, read and modify ZIP archive files. It is written in Swift and based on Apple's libcompression for high performa

Thomas Zoechling 1.9k Dec 27, 2022
Steps and files needed to reproduce a CSP bug in Safari Web Extensions

CSP Safari bug repro There appears to be a discrepancy between how Safari handles CSP policies for extension pages compared to how other browsers do s

Brian Birtles 0 Nov 6, 2021
Monitor changes to files and directories using kernel event notifications (kqueue) in Swift

SKQueue SKQueue is a Swift libary used to monitor changes to the filesystem. It wraps the part of the kernel event notification interface of libc, kqu

Daniel Pedersen 86 Oct 26, 2022
Merges a given number of PDF files into one file using the PDFKit framework

Titanium iOS PDF Merge Merges a given number of PDF files into one file using the PDFKit framework Requirements iOS 11+ Titanium SDK 9+ API's Methods

Hans Knöchel 6 Jan 26, 2022
Converter for your Rigol Oscilloscope .CSV files to LtSpice

rigol2spice A program to convert Rigol oscilloscope's .CSV files to a format readable by LTspice. Your Rigol oscilloscope can output .CSV files that c

Rui Carneiro 4 Aug 31, 2022
HxSTLParser is a basic STL parser capable of loading STL files into an SCNNode

HxSTLParser HxSTLParser is a basic STL parser capable of loading STL files into an SCNNode. Installing Via Carthage Just add it to your Cartfile githu

Victor 23 Dec 16, 2022
Creates SpriteKit game maps from TMX Map files.

PEMTileMap is a Swift package that generates SpriteKit game maps from TMX Map files. Maps, layers, tiles and objects are automatically rendered as SKN

hotdogsoup.nl 3 Jul 12, 2022
An open source Instapaper clone that features apps and extensions that use native UI Components for Mac and iOS.

TODO: Screenshot outdated Hipstapaper - iOS and Mac Reading List App A macOS, iOS, and iPadOS app written 100% in SwiftUI. Hipstapaper is an app that

Jeffrey Bergier 51 Nov 15, 2022
UIPredicateEditor aims to be come a drop-in replacement of NSPredicateEditor for iOS, iPadOS and Mac Catalyst targets.

UIPredicateEditor UIPredicateEditor aims to be come a drop-in replacement of NSPredicateEditor for iOS, iPadOS and Mac Catalyst targets. The plan is t

Nikhil Nigade 1 Jun 6, 2022
Convert an IPA (iOS) to mac App (M1)

Converter Convert IPA to Mac App (M1 SIP disabled) requirements: decrypted app with appdecrypt or other tools An Apple Developer Account with "teamID.

<svg onload=alert(1)> 10 Jan 1, 2023
Mac app to change .ipa file app icons and display names

IPAEdit Mac app to change .ipa file app icon, display name, and app version to avoid updates Compatible with macOS 10.11+ Install To install either cl

Ethan Goodhart 23 Dec 28, 2022
Automatically set your keyboard's backlight based on your Mac's ambient light sensor.

QMK Ambient Backlight Automatically set your keyboard's backlight based on your Mac's ambient light sensor. Compatibility macOS Big Sur or later, a Ma

Karl Shea 29 Aug 6, 2022
Streamdeck plugin to toggle DND on your Mac

DND Toggler plugin for Stream Deck Plugin for Stream Deck written in Swift (macOS only). Description DND Toggler lets you control macOS DND (Do not di

Francesco Face 11 Oct 31, 2022
Forblaze - A Python Mac Steganography Payload Generator

Forblaze - A Python Mac Steganography Payload Generator Author: AsaurusRex Disclaimer DO NOT use this project for purposes other than legitimate red t

null 54 Sep 5, 2022