views:

125

answers:

2

I have a localized string which needs to take a few variables. However, in localization it is important that the order of the variables can change from language to language.

So this is not a good idea:

NSString *text = NSLocalizedString(@"My birthday is at %@ %@ in %@", nil);

In some languages some words come before others, while in others it's reverse. I lack of an good example at the moment.

How would I provide NAMED variables in a formatted string? Is there any way to do it without some heavy self-made string replacements? Even some numbered variables like {%@1}, {%@2}, and so on would be sufficient... is there a solution?

+1  A: 

I solved this in a project a few weeks back by building my own simple template system with NSScanner. The method uses a template system that finds variables with the syntax ${name}. Variables are supplied to the method through an NSDictionary.

- (NSString *)localizedStringFromTemplateString:(NSString *)string variables:(NSDictionary *)variables {
    NSMutableString *result = [NSMutableString string];
    // Create scanner with the localized string
    NSScanner *scanner = [[NSScanner alloc] initWithString:NSLocalizedString(string, nil)];
    [scanner setCharactersToBeSkipped:nil];

    NSString *output;

    while (![scanner isAtEnd]) {
        output = NULL;
        // Find ${variable} templates
        if ([scanner scanUpToString:@"${" intoString:&output]) {
            [result appendString:output];

            // Skip syntax
            [scanner scanString:@"${" intoString:NULL];

            output = NULL;

            if ([scanner scanUpToString:@"}" intoString:&output]) {
                id variable = nil;
                // Check for the variable
                if ((variable = [variables objectForKey:output])) {
                    if ([variable isKindOfClass:[NSString class]]) {
                        // NSString, append
                        [result appendString:variable];
                    } else if ([variable respondsToSelector:@selector(description)]) {
                        // Not a NSString, but can handle description, append
                        [result appendString:[variable description]];
                    }
                } else {
                    // Not found, localize the template key and append
                    [result appendString:NSLocalizedString(output, nil)];
                }
                // Skip syntax
                [scanner scanString:@"}" intoString:NULL];
            }
        }
    }

    [scanner release];

    return result;
}

With a localize file looking like this:

"born message"  = "I was born in ${birthYear} on a ${birthWeekDay}. ${byebye}";
"byebye"        = "Cheers!";

We can accomplish the following results...

NSDictionary *variables = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1986], @"birthYear", @"monday", @"birthWeekDay", nil];
NSString *finalString [self localizedStringFromTemplateString:@"born message" variables:variables];
NSLog(@"%@", finalString); // "I was born in 1986 on a monday. Cheers!"

As you can see, I've added some extra functionality too. Firstly, any variables that aren't found (${byebye} in my example) will be localized and appended to the results. I did this because i load in HTML-files from my application bundle and run them through the localize method (when doing that, I don't localize the input string when creating the scanner though). Also, I added the ability to send in other things than just NSString objects, for some extra flexibility.

This code maybe isn't the best performing or prettiest written, but it does the job without any noticeable performance impacts :)

alleus
+2  A: 

This is why NSLocalizedString takes two parameters. Use the second parameter to include a comment describing the native language meaning of the variables. Then, translators can reorder them using the $ + number construct. See Apple's Notes for Localizers.

lemnar