5

I'm using a macro to simplify returning localised strings, like so:

#define GetLocalStr(key, ...) \
    [NSString stringWithFormat:[[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil], ##__VA_ARGS__]

Basically, if you have an entry in a localisation Strings file like "name" = "My name is %@";, calling

GetLocalStr( @"name", @"Foo" );

will return the NSString @"My name is Foo"

When I run it however, like:

NSString * str = GetLocalStr( @"name", @"Foo" );

I get the "format string is not a string literal" warning. Even following the advice of the other answers on SO about this warning and replacing it with:

NSString * str = [NSString stringWithFormat:@"%@", GetLocalStr( @"name", @"Foo" )];

I still get the warning, and besides, it kind of defeats the point of the macro making life easier.

How can I get rid of the warning short of wrapping all the GetLocalStr calls in #pragma suppressors?

Edit 27/08

After running through CRD's answer and doing some more tests, it seems like I made a bad assumption on the error. To clarify:

Localisation Strings file:

"TestNoArgs" = "Hello world";
"TestArgs" = "Hello world %@";

Code:

NSString * str1 = GetLocalStr( @"TestNoArgs" ); // gives warning
NSString * str2 = GetLocalStr( @"TestArgs", @"Foo" ); // doesn't give warning

The majority of my translations take no arguments, and those were the ones giving the warning, but I didn't make the connection until I read through CRD's answer.

I changed my single macro to two, like so:

#define GetLocalStrNoArgs(key) \
    [[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil]

#define GetLocalStrArgs(key, ...) \
    [NSString stringWithFormat:[[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil], ##__VA_ARGS__]

And if I call each one separately, there's no warnings.

I'd like GetLocalStr to expand to either GetLocalStrNoArgs or GetLocalStrArgs depending on if any arguments were passed or not, but so far I've been having no luck (macros are not my strong suit :D).

I'm using sizeof(#__VA_ARGS__) to determine if there's any arguments passed - it stringifys the arguments, and if the size is 1, it's empty (i.e. `\0'). Perhaps it's not the most ideal method, but it seems to work.

If I rewrite my GetLocalStr macro to:

#define GetLocalStr(key,...) (sizeof(#__VA_ARGS__) == 1) ? GetLocalStrNoArgs(key) : GetLocalStrArgs(key,##__VA_ARGS__)

I can use it, but I still get warnings everywhere it's used and there's no arguments passed, while something like

#define GetLocalStr( key,...)               \
    #if ( sizeof(#__VA_ARGS__) == 1 )       \
        GetLocalStrNoArgs(key)              \
    #else                                   \
        GetLocalStrArgs(key,##__VA_ARGS__)

won't compile. How can I get my GetLocalStr macro to expand properly?

divillysausages
  • 7,883
  • 3
  • 25
  • 39
  • `@"name" = @"My name is %@";` - literals are not assignable, it won't compile. `NSString * format = @"My name is %@"; NSString * str = GetLocalStr( format, @"Foo" ); NSLog(@"%@", str);` doesn't throw any warnings (I expected it only at `GetLocalStr` definition anyway), you might want to fix the quotes and provide more details about where the warning appears. – A-Live Aug 26 '13 at 17:12
  • 1
    You could create a NSString Category to do that for you. – Pétur Ingi Egilsson Aug 26 '13 at 19:29
  • @A-Live the `@"name" = @"My name is %@"` line is in a Strings localisation file, so technically it should just be "name" = "My name is %@" - sorry if that wasn't clear. The warning is flagged on the `NSString * str = GetLocalStr(...);` line – divillysausages Aug 27 '13 at 08:23
  • @JoshCaswell I'm aware of the other questions on this warning, but I couldn't find anything that helped me. If there's a specific answer that you're looking at, I'd appreciate it – divillysausages Aug 27 '13 at 08:24
  • Ok, I created `Localizable.strings` with a single line `"name" = "my name is %@";`, still no warning with your code. – A-Live Aug 27 '13 at 09:33
  • @A-Live Sorry, I was misunderstanding the problem (I've updated the question) - strings with arguments work fine, but strings with no arguments give the warning. Can you add a line `"foo" = "bar"` in your `Localizable.string` and tell me if there's still no warning? – divillysausages Aug 27 '13 at 10:35

2 Answers2

5

The Clang & GCC compilers check that format strings and the supplied arguments conform, they cannot do this if the format string is not a literal - hence the error message you see as you are obtaining the format string from the bundle.

To address this issue there is an attribute, format_arg(n) (docs), to mark functions which take a format string; alter it in some way without changing the actual format specifiers, e.g translate it; and then return it. Cocoa provides the convenient macro NS_FORMAT_ARG(n) for this attribute.

To fix your problem you need to do two things:

  1. Wrap up the call to NSBundle in a function with this attribute specified; and

  2. Change your "key" to include the format specifiers.

Second first, your strings file should contain:

"name %@" = "My name is %@"

so the key has the same format specifiers as the result (if you need to reorder the specifiers for a particular language you use positional format specifiers).

Now define a simple function to do the lookup, attributing it as a format translation function. Note we mark it as static inline, using the macro NS_INLINE as a hint to the compiler to both inline it into your macro expansion; the static allows you to include it in multiple files without symbol clashes:

NS_INLINE NSString *localize(NSString *string) NS_FORMAT_ARGUMENT(1);
NSString *localize(NSString *string)
{
   return [[NSBundle mainBundle] localizedStringForKey:string value:@"" table:nil];
}

And your macro becomes:

#define GetLocalStr(key, ...) [NSString stringWithFormat:localize(key), ##__VA_ARGS__]

Now when you:

GetLocalStr(@"name %@", @"Foo")

You will get both the localised format string and format checking.

Update

After Greg's comment I went back and checked - I had reproduced your error and so assumed it was down to a missing attribute. However as Greg points out localizedStringForKey:value:table: already has the attribute, so why the error? What I had absentmindedly done in reproducing your error was:

NSLog( GetLocalStr( @"name %@", @"Foo" ) );

and the compiler pointed at the macro definition and not that line - I should have spotted the compiler was misleading me.

So where does that leave you? Maybe you've done something similar? The key is that a format string must either be a literal or the result of a function/method attributed as a format translating function. And don't forget, you must also had the format specifier to your key as above.

Update 2

After your additional comments what you need to use is function, rather than a macro, along with the format attribute, for which Cocoa provides the convenient NS_FORMAT_FUNCTION(f,a) macro. This attribute informs the compiler that the function is a formatting one, the value of f is the number of the format string and a is the number of the first argument to the format. This gives the function declaration:

NSString *GetLocalStr(NSString *key, ...) NS_FORMAT_FUNCTION(1,2);

and the definition (assuming ARC):

NSString *GetLocalStr(NSString *key, ...)
{
   va_list args;
   va_start(args, key);
   NSString *format = [[NSBundle mainBundle] localizedStringForKey:key value:@"" table:nil];
   NSString *result = [[NSString alloc] initWithFormat:format arguments:args];
   va_end (args);
   return result;
}

(which is essentially the same as @A-Live's).

Uses of this will be checked appropriately, for example:

int x;
...
NSString *s1 = GetLocalStr(@"name = %d", x); // OK
NSString *s2 = GetLocalStr(@"name = %d");    // compile warning - More '%" conversions than data arguments
NSString *s3 = GetLocalStr(@"name", x);      // compile warning - Data argument not used by format string
NSString *s4 = GetLocalStr(@"name");         // OK
CRD
  • 52,522
  • 5
  • 70
  • 86
  • Which compiler and SDK are you using? -localizedStringForKey:value:table: should already be annotated with NS_FORMAT_ARGUMENT so the extra localize() function should not be necessary. – Greg Parker Aug 26 '13 at 21:34
  • @GregParker - Dang, I reproduced the OP's error, assumed it was through lack of the attribute, added in the function, and that fixed it. Turns out I'd made my own error, updated answer. Thanks. – CRD Aug 26 '13 at 22:02
  • @CRD - thanks for the detailed answer - it helped my clarify the problem more (I've updated the question). Calling `GetLocalStr( @"name", @"Foo" )` works fine, however `GetLocalStr( @"name" )` is what was giving the warning. I didn't see any difference with adding the format specifier to the key - it seemed to work fine without it, but maybe I'm misunderstanding something – divillysausages Aug 27 '13 at 10:39
  • On a side note - do you mind explaining what the `1` signifies in the `NS_FORMAT_ARGUMENT(1);` attribute? – divillysausages Aug 27 '13 at 10:40
  • The "1" indicates that the first argument is the format parameter. – Rob Napier Aug 27 '13 at 14:02
  • @divillysausages - updated answer for your new information, you need to use a function and a different attribute, see above. – CRD Aug 27 '13 at 19:24
  • hm, when my method declaration is like this: `+ (NSString *) localise:(NSString *)key, ... NS_FORMAT_FUNCTION(1,2);` I get the same warning, but when it's like this `+ (NSString *) localise:(NSString *)key, ...;` I don't. Am I missing something? (it's a static method in an NSString Category) – divillysausages Aug 29 '13 at 12:37
  • @divillysausages - for a static method you use `+ (NSString *) localise:(NSString *)key, ... NS_FORMAT_FUNCTION(1,2);` in your interface and `+ (NSString *) localise:(NSString *)key, ... {` *code body* `}` in your implementation - i.e. the attribute macro only goes on your declaration as for functions. – CRD Aug 29 '13 at 20:41
  • Yep, I mean, in my interface .h file, when I declare it like `+ (NSString *) localise:(NSString *)key, ... NS_FORMAT_FUNCTION(1,2);`, I get the same "format string..." warning. If I remove the `NS_FORMAT_FUNCTION(1,2)`, I don't, however – divillysausages Aug 30 '13 at 16:17
  • @divillysausages - the error suggests you are not passing a literal string as the first argument to `localise`. If this is not the case show your code (add it to your question and leave a comment here). – CRD Aug 30 '13 at 19:20
  • ok, more testing shows that, when I add the `NS_FORMAT_FUNCTION` to my `localise` method, if I call it like `[NSString localise@"some_key"]`, it's fine, but if I do `NSString * key = @"some_key"; [NSString localise:key];`, I get the "format string..." warning. It won't accept passing a variable, or a const declared in another file as `FOUNDATION_EXPORT`, which is what I was storing my keys as – divillysausages Sep 03 '13 at 10:35
  • 1
    @divillysausages - format checking depends on the compilers ability to "see" the format, the precise definition of which may depend on the compiler version. In general the compiler cannot determine the value of an `NSString *` variable, so you get a warning, but in some cases it can - the compiler can even sometimes see formats embedded in conditional expressions (`?:`). If you wish to define constants in a header file used in multiple places you can use `#define ` or `static NSString * const ;` and the definitions should be "seen" and format checking performed. – CRD Sep 03 '13 at 20:15
5

This variant produces no warnings (as there's always a va_list):

NSString* GetLocalStr1(NSString *formatKey, ...)  {
    va_list ap;
    va_start(ap, formatKey);
    NSString * format = [[NSBundle mainBundle] localizedStringForKey:formatKey value:@"" table:nil];
    NSString *result =  [[NSString alloc] initWithFormat:format arguments:ap];
    va_end (ap);
    return [result autorelease];
}

...

__unused NSString * str = GetLocalStr1( @"name", @"Foo" );
__unused NSString * str1 = GetLocalStr1( @"TestNoArgs" );
__unused NSString * str2 = GetLocalStr1( @"TestArgs", @"Foo" );

NSLog(@"%@", str);
NSLog(@"%@", str1);
NSLog(@"%@", str2);

Result:

my name is Foo

TestNoArgs

Hello world Foo

It doesn't answer the question exactly but might help you to avoid warnings until the solution is found.

Community
  • 1
  • 1
A-Live
  • 8,904
  • 2
  • 39
  • 74