Localization for Apple products always was a pain in the ass. Yep, every year Apple adds new and new options and features for localizations, but we always should handle a lot of issues on our own. Sometimes, when some issue is resolved, a few new ones appear.

The most critical issues are:

  • unique entries
  • untranslated entries
  • unused entries
  • usage of strings
  • typo in entries due to missed autocompletion
  • management of strings (by feature or by type)
  • different locales in different sources for strings
  • xib/storyboard localization (for UIKit) or Base.lproj
  • refactoring of strings
  • localization of Info.plist
  • sync localization between different platforms (for example iOS vs Android)

This is the list of the main problems, as for me. Off cause, there is might be even more of them, at least there are a few related to plurals and templated stings…

Looking for improvements, I found a way, that can solve the most critical problems from the list. And in this article, I would like to tell u about it.

The problems

If u work with an application that has a lot of features - u know, that there is might be a lot of things that should be translated. In my last project, we had about ~3k phrases that should be managed. At the same time, often we got some updates/change requests related to some of them. If u use plain .strings file - this may be a painful process full of errors - u should find, edit and finally check if everything is working as expected after an update.

Another painful process is to use strings in code - u either need to use a method from Bundle either Foundation macro NSLocalizedString (that under the hood use the same method from the bundle). In both cases, it’s easy to make a typo in the key name. It’s also hard to group the strings by key and so manage the separate flow. We may also separate each localization for a specific feature, but this is good only for a low qty of features.

I like to use a general name that doesn’t match to message itself (reason - text later or sooner will be changed, then the name of the key becomes outdated).

In android we have resources generator by ID, on iOS - no.

Thankfully to code generators, these 2 described problems can be solved easily. We can use swiftgen for this. And everything works just fine.

So, we can create a configuration for each .strings file and run swiftgen. Typos, management now a bit less painful. But still, we should manage all strings in the .strings files, we should check if there is no empty translation, no duplicates…

The good point is that this problem isn’t new and one more automized tool is available - bartyCrouch. This tool can also detect empty translations, duplicates and easily can be integrated within swiftgen.

Perfect tutorial from the author of bartyCrouch available here

We can combine swiftGen and bartyCrouch and solve issues with typos, dublicates, missing translations.

So, if we go to the list of localization problems - we can see, that the first top 5 are solved. Good.

.strings management

In the last project, I have used folders and separate .string files for each feature + swiftgen. Now, I see that we can do even better - each feature can be packed in a swift package. This is now possible thanks to the latest update - resources for the swift package.

Check WWDC20/10169 video - Swift packages: Resources and localization and Localizing Package Resources.

The good point here - is that we can now put localization in sp (swift package) and get a full feature as a separate unit!. U can now fully control and keep the logic of each part of u’r app, write separate tests and so reduce coupling and increase the quality of u’r code and cost of future changes.

That’s great, but we still have few points to solve before actual usage.

Localization

The tricky moment that I was facing when start dealing with this approach was related to localization. According to the WWDC video - we can just add a localized file into a package, expose it inside pkg and use it outside. All pretty simple, but when all u’r localizations in separate pkg and u try it on another language - nothing works.

here is a demo project - test it.

Yep - nothing works… The true reason here is that u’r app bundle doesn’t know about u’r localization in packages. We should somehow tell about it. Apple didn’t mention anything about this. I found 2 solutions:

CFBundleLocalizations

By adding CFBundleLocalizations in Info.plist with required localizations as an Array of strings. Even if in official doc says that it supports only a few localizations, we can add one there (using the same locale code as .lproj folders) and everything will works.

<key>CFBundleLocalizations</key>
<array>
	<string>en</string>
	<string>es</string>
	<string>uk-UA</string>
</array>

An application can notify the system that it supports additional localizations through its information property list (Info.plist) file. To specify localizations not included in your bundle’s .lproj directories, add the CFBundleLocalizations key to this file. The value for the key is an array of strings, each of which contains an ISO language designator as described in “Language and Locale Designations.”

this information from old outdated docs is not available right now.

But, thanks to the advice of @SDGGiesbrecht, such a solution is mostly a workaround. The proper one - CFBundleAllowMixedLocalizations

This may sound easy to use, but I spend a day figuring out this.

CFBundleAllowMixedLocalizations

If we are expecting libraries to use additional localizations beyond those supported by the main application bundle, setting CFBundleAllowMixedLocalizations to YES in the application’s Info.plist is the proper solution.

combining all parts

Finally, we have all parts ready to use. But here is still one unresolved issue - swiftGen and bartyCrouch does not support swift packages, so when we run scripts - it’s run on the whole project…

Also, every time create a package and add configuration files for swiftGen and bartyCrouch, add required localizations folders… DRY - do not repeat yourself!

Ok, let’s solve this one-by-one.

To follow DRY we can use xCode templates. I ended up creating a separate template, that contains all config files and specific files that are required for providing minimal functionality (I used TCA as an architecture for a project).

template


template_xCode



To know more about how to create an xCode template visit this great article

Now, if we try to create sp from this template, we can observe, that everything works fine, except .bartycrouch.toml file. The system thinks that all files, that is started from a dot - hidden one. For the xCode template, I didn’t find a way how to copy hidden files within all other files. Workaround for this problem - I decided to put this file in the root of the project, that during every build iterate over all pkg in the project, find the one that requires this file, and copy it into. To detect such a package I decided simply to use comment inside the Packadge.swift file. We can use a bash script to execute it in RunScripts for the project:

#! /bin/bash

#define colors
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

function green {
  printf "${GREEN}$@${NC}\n"
}

function yellow {
  printf "${YELLOW}$@${NC}\n"
}

# Search bartyCrouch config file
echo $(yellow Searching bartyCrouch config file...)
bartycrouchConfig=$(ls -a | grep bartycrouch)
echo ${bartycrouchConfig}

# search all dirs with Package.swift - this should be all pckg for proj
declare -a dirs
dirs=$(find "$PWD" -type f | grep Package.swift )
echo $(green Found next dirs with pkg:)
echo "${dirs}"

echo $(yellow Extacting path to pkg...)

# text that we are looking for inside pckg 
# to determine if we should copy .toml file there
# and copy
value=bartyCrouch 
for i in ${dirs[@]}
do    
 if grep -q ${value} "$i"; then
	echo $(yellow copy .bartycrouch.toml file into SP)
	cp $bartycrouchConfig ${i///Package.swift}
	echo $(green done)
 fi 
done
echo $(green .bartycrouch.toml copied into all SP)

To make pck acceptable for this script - add comment // bartyCrouch into Package.swift file at any place

Once we do this - configuration will be copied into all required pkg.

Next problem to solve - as was mentioned, swiftGen and bartyCrouch is not supports different sp, but only oot project. To make it works, we can add similar script in RunPhases to project, but this time, execute swiftgen and bartyCrouch.

For swiftGen my script is next:

#! /bin/bash

# Color
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color

function red {
    printf "${RED}$@${NC}\n"
}

function green {
    printf "${GREEN}$@${NC}\n"
}

function yellow {
    printf "${YELLOW}$@${NC}\n"
}

if which swiftgen > /dev/null; then

	
	declare -a dirs
	dirs=$(find "$PWD" -type f | grep Package.swift )
	echo $(green Found next dirs with pkg:)
	echo "${dirs}"
	
	echo $(yellow Extacting path to pkg...)
	
	declare -a pkgPath
	
	ITER=0
	value=localizableSP
	for i in ${dirs[@]}
	do    
    	ITER=$(expr $ITER + 1)
    	if grep -q ${value} "$i"; then
			echo $(green localizable pkg found!)
        	pkgPath[$ITER]="${i///Package.swift}"
			echo ${pkgPath[$ITER]}
		fi
	done
	
	echo "Result:"
	for j in ${pkgPath[@]}
	do
		echo $(yellow Switching to folder) $j
		cd $j
		
		echo $(yellow Running SwiftGen)
    swiftgen
		echo $(green done)
	done
	
	echo $(green SwiftGen finished localization update)
	
else
    echo $(red SwiftGen not installed)
    echo $(yellow download it from https://github.com/SwiftGen/SwiftGen)
fi

To make pck acceptable for this script - add comment // localizableSP into Package.swift file at any place.

For bartyCrouch - similar one, but another command used instead swiftgen:

# same stuff as above
...
	bartycrouch update -x
	bartycrouch lint -x  
...

Make sure that RunScript in the correct order

  • copy .toml file
  • run bartycrouch
  • run swiftgen
runScripts


Done. Now, when u would like to add localization, the flow will be like next:

demo



To make things, even more, better - create a code snippet for xCode and use it instantly when needed.

Conclusion

With this approach, we can solve the biggest part of issues related to localization. Off cause, we can add sync logic - to allow upload/download translation for localization, but this is a bit another story.


download source code

Resources