146

I'm (like all others) using NSLocalizedStringto localize my app.

Unfortunately, there are several "drawbacks" (not necessarily the fault of NSLocalizedString itself), including

  • No autocompletition for strings in Xcode. This makes working not only error-prone but also tiresome.
  • You might end up redefining a string simply because you didn't know an equivalent string already existed (i.e. "Please enter password" vs. "Enter password first")
  • Similarily to the autocompletion-issue, you need to "remember"/copypaste the comment strings, or else genstring will end up with multiple comments for one string
  • If you want to use genstring after you've already localized some strings, you have to be careful to not lose your old localizations.
  • Same strings are scattered througout your whole project. For example, you used NSLocalizedString(@"Abort", @"Cancel action") everywhere, and then Code Review asks you to rename the string to NSLocalizedString(@"Cancel", @"Cancel action") to make the code more consistent.

What I do (and after some searches on SO I figured many people do this) is to have a seperate strings.h file where I #define all the localize-code. For example

// In strings.h
#define NSLS_COMMON_CANCEL NSLocalizedString(@"Cancel", nil)
// Somewhere else
NSLog(@"%@", NSLS_COMMON_CANCEL);

This essentially provides code-completion, a single place to change variable names (so no need for genstring anymore), and an unique keyword to auto-refactor. However, this comes at the cost of ending up with a whole bunch of #define statements that are not inherently structured (i.e. like LocString.Common.Cancel or something like that).

So, while this works somewhat fine, I was wondering how you guys do it in your projects. Are there other approaches to simplify the use of NSLocalizedString? Is there maybe even a framework that encapsulates it?

JiaYow
  • 5,207
  • 3
  • 32
  • 36
  • I just do it almost the same like you. But I am using the NSLocalizedStringWithDefaultValue makro to create different strings files for different localization issues (like controllers, models, etc.) and to create an initial default value. – anka Apr 16 '12 at 22:23
  • It seems like xcode6's Export to localization doesn't catch the strings that are defined as macros in a header file. Can anyone confirm or tell me what I might be missing? Thanks...! – Juddster Sep 30 '14 at 12:58
  • @Juddster, can confirm, even with the new fund Editor->Export for Localization it does not get picked up in the header file – Red Nov 10 '15 at 09:04

9 Answers9

101

NSLocalizedString has a few limitations, but it is so central to Cocoa that it's unreasonable to write custom code to handle localization, meaning you will have to use it. That said, a little tooling can help, here is how I proceed:

Updating the strings file

genstrings overwrites your string files, discarding all your previous translations. I wrote update_strings.py to parse the old strings file, run genstrings and fill in the blanks so that you don't have to manually restore your existing translations. The script tries to match the existing string files as closely as possible to avoid having too big a diff when updating them.

Naming your strings

If you use NSLocalizedString as advertised:

NSLocalizedString(@"Cancel or continue?", @"Cancel notice message when a download takes too long to proceed");

You may end up defining the same string in another part of your code, which may conflict as the same english term may have different meaning in different contexts (OK and Cancel come to mind). That is why I always use a meaningless all-caps string with a module-specific prefix, and a very precise description:

NSLocalizedString(@"DOWNLOAD_CANCEL_OR_CONTINUE", @"Cancel notice window title when a download takes too long to proceed");

Using the same string in different places

If you use the same string multiple times, you can either use a macro as you did, or cache it as an instance variable in your view controller or your data source. This way you won't have to repeat the description which may get stale and get inconsistent among instances of the same localization, which is always confusing. As instance variables are symbols, you will be able to use auto-completion on these most common translations, and use "manual" strings for the specific ones, which would only occur once anyway.

I hope you'll be more productive with Cocoa localization with these tips!

ndfred
  • 3,842
  • 2
  • 23
  • 14
  • Thank you for your answer, I will definitely take a look at your python-file. I agree with your naming conventions. I've talked with some other iOS devs recently, and they recommended the usage of static strings instead of macros, which makes sense. I've upvoted your answer, but will wait a bit before I accept it, because the solution is still a bit clumsy. Maybe something better comes along. Thanks again! – JiaYow Apr 23 '12 at 17:33
  • You're very welcome. Localization is a tedious process, having the right tools and workflow makes a world of difference. – ndfred Apr 24 '12 at 07:57
  • 19
    I've never understood why gettext-style localization functions use one of the translations as the key. What happens if your original text changes? Your key changes and all your localized files are using the old text for their key. It has never made sense to me. I've always used keys like "home_button_text" so they are unique and never change. I have also written a bash script to parse all my Localizable.strings files and generate a class file with static methods which will load the appropriate string. This gives me code completion. One day I might open source this. – Mike Weller May 22 '12 at 12:13
  • Why static strings instead of macros? – Elliot Jun 20 '13 at 17:12
  • What would macros give you? They would translate to strings anyway, and declaring them means more code. – ndfred Jun 28 '13 at 01:07
  • 2
    I think you mean `genstrings` not `gestring`. – hiroshi Sep 28 '13 at 02:27
  • @MikeWeller, any progress with it? – Iulian Onofrei Jan 27 '15 at 12:25
  • @Mike Weller: One advantage to using the localized text from the source language as the key is that there's no risk of changing the source text and forgetting to update the translations. The downside being that you lose the link between the old translations and the new key, so you orphan the now (typically only slightly) out of date translations (and it would be less work to update these than write new ones from scratch). However this is fairly easily solvable by tooling - look for orphaned translations with keys that are a short Levenshtein distance from keys that are missing translations. – Martin Gjaldbaek Apr 23 '15 at 07:35
  • 1
    @ndfred compile time checks that you haven't typed the string wrong is the biggest win. It's marginally more code to add anyway. Also in the case of refactoring, static analysis, having a symbol is going to make things waaay easier. – Allen Zeng Nov 12 '15 at 23:14
  • `NSLocalizedString(@"DOWNLOAD_CANCEL_OR_CONTINUE", @"Cancel notice window title when a download takes too long to proceed"); ` -- the key is **DOWNLOAD_CANCEL_OR_CONTINUE** but actual **Value** is unknown. @ndfred – nodebase Sep 20 '16 at 22:51
  • @MikeWeller Agree. That's why it is better to use `NSLocalizedString` with the `value:` parameter, e.g. `NSLocalizedString("btn_yes", value: "Yes", comment: "Yes button")`. This way it is possible to separate the translation ID and the default text. This is how it is used https://stackoverflow.com/a/45991563/1245231 – petrsyn Aug 31 '17 at 23:57
31

As for autocompletition for strings in Xcode, you could try https://github.com/questbeat/Lin.

hiroshi
  • 6,871
  • 3
  • 46
  • 59
25

Agree with ndfred, but I would like to add this:

Second parameter can be use as ... default value!!

(NSLocalizedStringWithDefaultValue does not work properly with genstring, that's why I proposed this solution)

Here is my Custom implementation that use NSLocalizedString that use comment as default value:

1 . In your pre compiled header (.pch file) , redefine the 'NSLocalizedString' macro:

// cutom NSLocalizedString that use macro comment as default value
#import "LocalizationHandlerUtil.h"

#undef NSLocalizedString
#define NSLocalizedString(key,_comment) [[LocalizationHandlerUtil singleton] localizedString:key  comment:_comment]

2. create a class to implement the localization handler

#import "LocalizationHandlerUtil.h"

@implementation LocalizationHandlerUtil

static LocalizationHandlerUtil * singleton = nil;

+ (LocalizationHandlerUtil *)singleton
{
    return singleton;
}

__attribute__((constructor))
static void staticInit_singleton()
{
    singleton = [[LocalizationHandlerUtil alloc] init];
}

- (NSString *)localizedString:(NSString *)key comment:(NSString *)comment
{
    // default localized string loading
    NSString * localizedString = [[NSBundle mainBundle] localizedStringForKey:key value:key table:nil];

    // if (value == key) and comment is not nil -> returns comment
    if([localizedString isEqualToString:key] && comment !=nil)
        return comment;

    return localizedString;
}

@end

3. Use it!

Make sure you add a Run script in your App Build Phases so you Localizable.strings file will be updated at each build, i.e., new localized string will be added in your Localized.strings file:

My build phase Script is a shell script:

Shell: /bin/sh
Shell script content: find . -name \*.m | xargs genstrings -o MyClassesFolder

So when you add this new line in your code:

self.title = NSLocalizedString(@"view_settings_title", @"Settings");

Then perform a build, your ./Localizable.scripts file will contain this new line:

/* Settings */
"view_settings_title" = "view_settings_title";

And since key == value for 'view_settings_title', the custom LocalizedStringHandler will returns the comment, i.e. 'Settings"

Voilà :-)

Pascal
  • 15,257
  • 2
  • 52
  • 65
  • Getting ARC errors, No known instance method for selector 'localizedString:comment::( – Mangesh Feb 04 '14 at 15:30
  • I suppose it's because LocalizationHandlerUtil.h is missing. I can't find the code back... Just try to create the header file LocalizationHandlerUtil.h and it should be OK – Pascal Feb 04 '14 at 16:19
  • I have created the files. I think it is due to folder path issue. – Mangesh Feb 04 '14 at 16:22
4

In Swift I'm using the following, e.g. for button "Yes" in this case:

NSLocalizedString("btn_yes", value: "Yes", comment: "Yes button")

Note usage of the value: for the default text value. The first parameter serves as the translation ID. The advantage of using the value: parameter is that the default text can be changed later but the translation ID remains the same. The Localizable.strings file will contain "btn_yes" = "Yes";

If the value: parameter was not used then the first parameter would be used for both: for the translation ID and also for the default text value. The Localizable.strings file would contain "Yes" = "Yes";. This kind of managing localization files seems to be strange. Especially if the translated text is long then the ID is long as well. Whenever any character of the default text value is changed, then the translation ID gets changed as well. This leads to issues when external translation systems are used. Changing of the translation ID is understood as adding new translation text, which may not be always desired.

petrsyn
  • 5,054
  • 3
  • 45
  • 48
2

I wrote a script to help maintaining Localizable.strings in multiple languages. While it doesn't help in autocompletion it helps to merge .strings files using command:

merge_strings.rb ja.lproj/Localizable.strings en.lproj/Localizable.strings

For more info see: https://github.com/hiroshi/merge_strings

Some of you find it useful I hope.

nacho4d
  • 43,720
  • 45
  • 157
  • 240
hiroshi
  • 6,871
  • 3
  • 46
  • 59
2

If anyone looking for a Swift solution. You may want to check out my solution I put together here: SwiftyLocalization

With few steps to setup, you will have a very flexible localization in Google Spreadsheet (comment, custom color, highlight, font, multiple sheets, and more).

In short, steps are: Google Spreadsheet --> CSV files --> Localizable.strings

Moreover, it also generates Localizables.swift, a struct that acts like interfaces to a key retrieval & decoding for you (You have to manually specify a way to decode String from key though).

Why is this great?

  1. You no longer need have a key as a plain string all over the places.
  2. Wrong keys are detected at compile time.
  3. Xcode can do autocomplete.

While there're tools that can autocomplete your localizable key. Reference to a real variable will ensure that it's always a valid key, else it won't compile.

// It's defined as computed static var, so it's up-to-date every time you call. 
// You can also have your custom retrieval method there.

button.setTitle(Localizables.login.button_title_login, forState: .Normal)

The project uses Google App Script to convert Sheets --> CSV , and Python script to convert CSV files --> Localizable.strings You can have a quick look at this example sheet to know what's possible.

aunnnn
  • 1,882
  • 2
  • 17
  • 23
1

with iOS 7 & Xcode 5, you should avoid using the 'Localization.strings' method, and use the new 'base localisation' method. There are some tutorials around if you google for 'base localization'

Apple doc : Base localization

Ronny Webers
  • 5,244
  • 4
  • 28
  • 24
  • yes Steve that is correct. Also, you still need the .strings file method for any dynamically generated string. But only for these, Apple's preferred method is base localisation. – Ronny Webers Feb 19 '14 at 16:06
  • Link to the new method? – Hyperbole Jul 29 '14 at 15:24
  • 1
    Imo the base Localization method is worthless. You still have to keep other location files for dynamic strings, and it keeps your strings spread through a lot of files. Strings inside Nibs/Storyboards can be localized automatically to keys in Localizable.strings with some Libs such as https://github.com/AliSoftware/OHAutoNIBi18n – Rafael Nobre Feb 17 '16 at 20:37
0
#define PBLocalizedString(key, val) \

[[NSBundle mainBundle] localizedStringForKey:(key) value:(val) table:nil]
Baby Groot
  • 4,637
  • 39
  • 52
  • 71
0

Myself, I'm often carried away with coding, forgetting to put the entries into .strings files. Thus I have helper scripts to find what do I owe to put back into .strings files and translate.

As I use my own macro over NSLocalizedString, please review and update the script before using as I assumed for simplicity that nil is used as a second param to NSLocalizedString. The part you'd want to change is

NSLocalizedString\(@(".*?")\s*,\s*nil\) 

Just replace it with something that matches your macro and NSLocalizedString usage.

Here comes the script, you only need Part 3 indeed. The rest is to see easier where it all comes from:

// Part 1. Get keys from one of the Localizable.strings
perl -ne 'print "$1\n" if /^\s*(".+")\s*=/' myapp/fr.lproj/Localizable.strings

// Part 2. Get keys from the source code
grep -n -h -Eo -r  'NSLocalizedString\(@(".*?")\s*,\s*nil\)' ./ | perl -ne 'print "$1\n" if /NSLocalizedString\(@(".+")\s*,\s*nil\)/'

// Part 3. Get Part 1 and 2 together.

comm -2 -3 <(grep -n -h -Eo -r  'NSLocalizedString\(@(".*?")\s*,\s*nil\)' ./ | perl -ne 'print "$1\n" if /NSLocalizedString\(@(".+")\s*,\s*nil\)/' | sort | uniq) <(perl -ne 'print "$1\n" if /^\s*(".+")\s*=/' myapp/fr.lproj/Localizable.strings | sort) | uniq >> fr-localization-delta.txt

The output file contains keys that were found in the code, but not in the Localizable.strings file. Here is a sample:

"MPH"
"Map Direction"
"Max duration of a detailed recording, hours"
"Moving ..."
"My Track"
"New Trip"

Certainly can be polished more, but thought I'd share.

Stanislav Dvoychenko
  • 1,303
  • 1
  • 15
  • 15