Greetings, dear hackers!
Today I want to share an interesting experience in solving the localization problem. In iOS, localization is arranged quite conveniently from the point of view of one target, or several targets in which the keys in localizable.strings are not very repeated. But everything becomes more complicated when you have a dozen targets, in which more than half of the keys are repeated, but at the same time have different meanings, and there is also a set of keys that are unique for a specific target.
For those who have not yet encountered this, I will explain the problem in more detail with an example.
Let's say we have a large project in which 90% of the general code and 3 targets : MyApp1 , MyApp2 , MyApp3 , which have a number of specific screens, as well as each has its own name and texts. In fact, a target is an independent application. Each of them must be translated into 10 languages. However, we do NOT want to add localization keys like app1_localizable_key1 , app2_localizable_key1 , etc. We want everything to be beautiful in the code and localization in one line
NSLocalizedString(@"localizable_key1", nil)
Without any if and ifdef , so that when adding a new target, we do not have to look for places with NSLocalizedString throughout the code of a huge project and register new keys there. We also want some of the keys to be tied to specific target screens, i.e. there were keys app2_screen1_key , app3_screen2_key .
Now you can do the following using standard Xcode tools:
- Copy the common part localizable.strings into each target, and we will get 3 copies of these files.
- Add target-specific keys to the corresponding localizable.strings.
What problems do we get:
- It is quite expensive to add a new public key to a project. The number of seats equals the number of targets multiplied by the number of languages. In our example, this is 30 places.
- There is a chance of an error when we added a line to 1-2 current targets with which we are actively working, and a year later they decided to resurrect one or more targets. You will have to manually synchronize localizations with each other, or write a script for this. And if some sloppiness was shown when adding or merging branches, and general keys are mixed with specific ones, then there will be a real quest.
- The volume of localization files. They are all constantly growing, this makes it difficult to work with them and increases the chances of conflict when merging branches.
What I would like:
- To keep all public keys in a separate file.
- For each target, there was a file in which only the keys specific to it were stored, as well as general keys with values for this target.
For our example, having a common file localizable.strings with strings
"shared_localizable_key1" = "MyApp title"
"shared_localizable_key2" = "MyApp description"
"shared_localizable_key3" = "Shared text1"
"shared_localizable_key4" = "Shared text2"
I would like to have a localizable_app2.strings file with the keys
"shared_localizable_key1" = "MyApp2 another title"
"shared_localizable_key2" = "MyApp2 another description"
"app2_screen1_key" = "Profile screen title"
Those. organize the principle of inheritance in localization files .
Unfortunately, Xcode is not tailored for this, so I had to reinvent my own "bicycle", which did not want to go for a long time due to the fact that Xcode here and there put sticks in the wheels.
We have a project with 18 targets and 12 languages. And this is not a joke, the project is really big and so many targets are needed there. Every time we need to add a new public key for translation, we are dealing with 216 localization files. This is time consuming. And adding a new target leads to the fact that you need to copy 12 more localizable.strings into it . In general, at some point we realized that it was impossible to live like this anymore and we had to look for a solution.
I will not talk for a long time about all the methods that I managed to test in the process, I will go straight to the working solution.
So, first we needed to find all the shared keys. This can be done using a script, I will not go into details, this is a rather trivial task.
Further, when we received a common (base) localization file, or rather 12 physical files, as well as a set of files for each target, we go to Xcode, add all the files there. At the same time, we do not attach files to any target, i.e. the right pane under Target Membership should be unchecked.
We will put these marks only for the file, which will be the result of the work of the script for assembling files.
Then the same "bike" begins:
- Localization, build_localization.py.
- Localizable. localizable.strings.
- Localizable .
We just need it to correctly add a link to the files in the project, and so that Xcode will recognize them correctly. Otherwise, it will not use them to find keys. For example, if you create a Localizable folder with the correct localizable.strings files inside, and add it to the project as a folder references , then no matter what Xcode will not understand that we gave it localization keys. Therefore, take the Localizable folder , drag it as a group ( create group ) and uncheck the copy items if needed checkbox to get it like the picture below.
Delete the Localizable folderand add it to the exceptions for the gita. Because we don't need the result of the script in the gita, it will change for each target and clog up the commits.
Now we need to add the script to the build phase. To do this, in Build Phases, click New Run Script Phase and write our script with parameters.
python3 ${SRCROOT}/Localization/build_localization.py -b “${SRCROOT}/BaseLocalization" -s "${SRCROOT}/Target1Localization" -d "${SRCROOT}/Localization/Localizable"
b is the folder with the base localization, s is the localization of the current target, d is the result folder.
Move the new phase up, it must not be lower than the Copy Bundle Resources phase . Those. first, the script generates files, and only then they are taken into the bundle.
Now it is important to tell Xcode that during the execution of the script, the files are changed, otherwise during the build it will throw an error that it could not find the files. Moreover, the error will only be on a clean assembly, and it will not be immediately clear what the problem is. In the build phase, add all localization files to the output files
This needs to be done for each target. The easiest way to do this is by opening the project with a text editor, because Xcode will not be able to copy / paste the phase between the targets. Accordingly, the script parameter -s for each target will be different.
Now, with each build, the script will take the base localization file, roll over changes from the target file (add, overwrite keys) and generate localization in the Localizable folder , which iOS will use to find keys.
In general, we got what was planned when implementing the inheritance mechanism:
- Shared keys reside in one file and do not interfere with others. The time for the process of entering new keys has been reduced by 18! time.
- Keys related to a specific target are in the corresponding file.
- The file size has dropped significantly. We got rid of the clutter of repeating lines.
- The process of adding a new language to a project is also greatly simplified.
- When creating a new target, you don't need to copy the localization with a bunch of unnecessary lines. Create a new file called localizable.strings and add only what is needed for this target.
- If you decide to revive the old target, then you don't need to do anything with the lines at all, everything will be pulled from the base file.
- The script does not litter the git, the result of the work remains locally and can be removed painlessly.
→ The finished script can be taken here
I do not pretend to be a perfect script, pull requests are welcome.