/
NSString+LocalizeSSY.h
358 lines (297 loc) · 13.2 KB
/
NSString+LocalizeSSY.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
#import <Cocoa/Cocoa.h>
/*
***** GENERAL DOCUMENTATION *****
ABSTRACT
This category is my replacement for Cocoa's +[NSString localizedStringWithFormat:] method
and also the NSLocalizedString() macro. It extends Cocoa's NSString class with five
additional methods.
DEFINITIONS
Substitution
When substring(s) are substituted into a translated string, for example as in:
"Put the %@ in the %@", "apples", "basket"
they are called "substitutions".
Reordering
In many cases, the order in which substitutions occur will be different in
in different languages. For example, consider the English string:
"Put the XXX in the YYY."
No reordering is needed if the translator wants this:
"Putten da XXX inta da YYY"
But in many cases "reordering" is needed because the translator may want something like this:
"Inta da YYY putten da XXX"
The problem of Reordering requires that the placeholders be somehow indexed.
MOTIVATIONS
1. Simpler Reorderable Strings
In Apple documentation Internationalization Programming Topics > Strings Files, Apple
states their solution to the problem of Reordering (described above):
"The translator can reorder the arguments in the translated string if that is necessary.
If a string contains multiple arguments, the translated string can use the
[n][$] modifier[] n$
modifier for each argument, where n indicates the position of the original argument."
Actually, it's not quite as bad as that says. In fact, after the modifier comes
immediately the format character (f, e, d, etc.) If the argument is an NSString,
the required placeholders are %0$@, %1$@, etc. These are ghastly. They become worse when
you actually use the modifiers, for example $%0$7.2f. I sure as hell can't type them
very fast without making lots of mistakes. And, since the above-referenced Apple
documentation is so confusing, I'd have to re-document it for our translators anyhow.
I don't like to explain things that are ghastly.
So, one motivation is to also support the "Classic Mac OS" placeholders %0, %1, %2,
which are much easier to use, even though they only allow strings to be substituted.
In practice, this is a small limitation because > 98% of substitutions in most apps
are strings anyhow, and the remaining 2% are mostly integers which can be handily
converted to strings using +[NSString stringWithInt:] provided herein. The very rare
case of floats, etc. can be preprocessed with [NSString localizedStringWithFormat:].
And my implementation of +[NSString localizeFormat:] still supports the ghastly Apple
syntax for those that really want to use it.
2. Get the Localization Comments out of the Code
I have no use for the 'comments' argument in NSLocalizedString because I prefer to carefully
re-use my strings based on comments which I enter directly into a common Localizable.strings
file. My comments are sometimes very long and therefore would make my code hard to read if
there were in every NSLocalizedString() call. I consider my comments to be a property of the
string key, not of any particular instantiated use, and that it is the responsiblity of the
person writing the code to check that the "documentation", i.e. comments, defining a particular
key before using it. There are two other alternatives: (1) Never re-use any translations.
(2) Have translators review the everyting whenever the app is updated. My localization
budget does not allow these other alternatives.
Of course, re-used translations can be poor if you don't define a broad enough scope
for your translators in your comments, and have some knowledge of international
linguistics to help you judge what can be generalized. But, again, my localization budget
cannot afford perfection.
Thus having dispensed with genstrings, I am tired of typing ", nil" in every call to
NSLocalizedString().
UNIT TEST CODE AND EXAMPLES
*** To run these tests, paste the following definitions into project's Localizable.strings:
"yessir" = "Oui" ;
"02" = "A=%0 C=%2" ;
"20" = "C=%2 A=%0" ;
"putStuff" = "Inta da %2 afteren %0 putten %3%% of da %1." ;
"destroy" = "We had to destroy the %0 in order to save the %0." ;
"show123" = "first: %@. second: %@. third: %@." ;
"unsignedLongz" = "Unsigned long numbers:\nlittleu: %u\n bigU: %U\nlittlex: %x\n bigX: %X\nlittleo: %o\n bigO: %O\npointer: %p" ;
"putApple" = "Inta da %2$@ afteren %0$@ putten %3$d%% of da %1$@." ;
"youHaveInteger" = "You have: %1$8i %0$@." ;
"youHaveFloat" = "You have: $%1$7.2f %0$@ in your %2$@." ;
"theFractions" = "Your fraction of the %2$@ is %1$0.3e and mine is %0$0.3e." ;
*** And paste this code into the Unit Test, or wherever it will execute:
#import "NSString+LocalizeSSY.h"
NSString* s ;
NSMutableString* result = [NSMutableString string] ;
// Basic, no substitutions
// Relevant entry in Localizable.strings:
// "yessir" = "Oui" ;
s = [NSString localize:@"yessir"] ;
[result appendFormat:@"%@\n",s] ;
// Using the localizeFormat: when there are no subs
// Wasteful, but works the same
s = [NSString localizeFormat:@"yessir"] ;
[result appendFormat:@"%@\n",s] ;
// Nil-terminate the argument list if you want to.
// Nil termination is not necessary.
s = [NSString localizeFormat:@"yessir", nil] ;
[result appendFormat:@"%@\n",s] ;
// Two substitutions with a third, not used:
s = [NSString localizeFormat:
@"02",
@"A",
@"B",
@"C"] ;
[result appendFormat:@"%@\n",s] ;
// Two substitutions with a third, not used, and
// the two that are used in reverse order.
s = [NSString localizeFormat:
@"20",
@"A",
@"B",
@"C"] ;
[result appendFormat:@"%@\n",s] ;
// Simple reorderable string substitutions: %0, %1, %2, ...
// also demonstrates a literal (escaped) percent %%
// and preprocessing an integer using +[NSString stringWithInt:]
// Relevant entry in Localizable.strings:
// "putStuff" = "Inta da %2 afteren %0 putten %3%% of da %1." ;
s = [NSString localizeFormat:@"putStuff",
@"you're done",
@"peanuts",
@"bucket",
[NSString stringWithInt:80]] ;
[result appendFormat:@"%@\n",s] ;
// Reorderable string arguments will be re-used if not enough
// are supplied in the argument list.
// Relevant entry in Localizable.strings:
// "destroy" = "We had to destroy the %0 in order to save the %0." ;
s = [NSString localizeFormat:@"destroy",
@"village"] ;
[result appendFormat:@"%@\n",s] ;
// Simpler nonreorderable string substitutions: %@, %@, %@, ...
// Relevant entry in Localizable.strings:
// "show123" = "first: %@. second: %@. third: %@." ;
s = [NSString localizeFormat:@"show123",
@"alpha",
@"beta",
@"gamma"] ;
[result appendFormat:@"%@\n",s] ;
// Error indicated if missing entry in Localizable.strings
s = [NSString localizeFormat:@"Oops some %@ forgot to enter a string for %@.",
@"This substring will not be used."] ;
[result appendFormat:@"%@\n",s] ;
// "unsignedLongz" = "Unsigned long numbers:\nlittleu: %u\n bigU: %U\nlittlex: %x\n bigX: %X\nlittleo: %o\n bigO: %O\npointer: %p" ;
s = [NSString localizeFormat:@"unsignedLongz",
0xFFFFFF00,
0xFFFFFF01,
0xFFFFFF02,
0xFFFFFF03,
0xFFFFFF04,
0xFFFFFF05,
0xdeadbeef] ;
[result appendFormat:@"%@\n",s] ;
// Remaining tests use Apple's ("ghastly") syntax (%n$x.yz) for reordering
// substitutions. It is messy but allows arbitrary format specifiers.
// Re-do the peanuts in the bucket example, with apples
// Relevant entry in Localizable.strings:
// "putApple" = "Inta da %2$@ afteren %0$@ putten %3$d%% of da %1$@." ;
s = [NSString localizeFormat:@"putApple",
@"you're done",
@"apples",
@"bucket",
80] ;
[result appendFormat:@"%@\n",s] ;
// Relevant entry in Localizable.strings:
// "youHaveInteger" = "You have: %1$8i %0$@." ;
s = [NSString localizeFormat:@"youHaveInteger",
@"toes",
10] ;
[result appendFormat:@"%@\n",s] ;
// Relevant entry in Localizable.strings:
// "youHaveFloat" = "You have: $%1$7.2f %0$@ in your %2$@." ;
s = [NSString localizeFormat:@"youHaveFloat",
@"dollars",
14.95123, // floats are promoted to double by ...
@"pocket"] ;
[result appendFormat:@"%@\n",s] ;
// Relevant entry in Localizable.strings:
// "theFractions" = "Your fraction of the %2$@ is %1$0.3e and mine is %0$0.3e." ;
s = [NSString localizeFormat:@"theFractions",
.56789,
1.2345e-5,
@"pie"] ;
[result appendFormat:@"%@\n",s] ;
NSString* expectedResult = @""
"Oui\n"
"Oui\n"
"Oui\n"
"A=A C=C\n"
"C=C A=A\n"
"Inta da bucket afteren you're done putten 80% of da peanuts.\n"
"We had to destroy the village in order to save the village.\n"
"first: alpha. second: beta. third: gamma.\n"
"OOPS SOME %@ FORGOT TO ENTER A STRING FOR %@. <NOT FOUND>\n"
"Unsigned long numbers:\n"
"littleu: 4294967040\n"
" bigU: 4294967041\n"
"littlex: ffffff02\n"
" bigX: FFFFFF03\n"
"littleo: 37777777404\n"
" bigO: 37777777405\n"
"pointer: 0xdeadbeef\n"
"Inta da bucket afteren you're done putten 80% of da apples.\n"
"You have: 10 toes.\n"
"You have: $ 14.95 dollars in your pocket.\n"
"Your fraction of the pie is 1.234e-05 and mine is 5.679e-01.\n"
;
NSInteger i ;
NSString* msg = nil ;
for (i=0; i<[expectedResult length]; i++) {
NSRange range = NSMakeRange(i, 1) ;
NSString* e = [expectedResult substringWithRange:range] ;
NSString* r = [result substringWithRange:range] ;
BOOL match = [e isEqualToString:r] ;
if (!match) {
msg = [NSString stringWithFormat:
@"\n-[NSString(SSYLocalize) localizeFormat:] failed test suite.\n"
"Error was at *the last character* of this substring:\n"
"*** EXPECTED RESULT:\n%@\n*** ACTUAL RESULT:\n%@\n",
[expectedResult substringToIndex:MIN(i+1, [expectedResult length])],
[result substringToIndex:MIN(i+1, [result length])]
] ;
break ;
}
}
if (msg) {
NSLog(@"%@", msg) ;
NSAssert (NO, msg) ;
}
else {
NSLog(@"-[NSString(SSYLocalize) localizeFormat:] passed test suite.") ;
}
*/
extern NSString* SSStringNotFoundAnnouncer ;
@interface NSString (LocalizeSSY)
/*
Returns a localized string built from formatString and placeholder substitutions
If formatString
- is nil, this method will return nil.
- is an empty string, this method will return the same empty string.
Otherwise, will search the "Localizable.strings" resources in the main bundle.
If formatString is not found, this method will return formatString capitalized
(UPPER CASE) and followed by " <NOT FOUND>", and will also log the error to
stderr.
If formatString is found, will process as explained below.
This method will parse formatString for percent ("%") characters which denote
placeholders. Placeholders will be parsed out (in this order):
literal % characters by escaping; i.e. %%.
nonreorderable NSString placeholders %@
reorderable NSString placeholders %0, %1, %2, ... , %9.
reorderable arbitrary placeholders n$[modifier][formatChar]
Examples: %0$@, %1$@, $%0$7.2f
For formatChar, all of the single-character Format Specifiers listed in:
String Programming Guide For Cocoa > String Format Specifiers
> Format Specifiers > Table 1
are supported. Specifically, these are:
@, c, C, d, D, i, e, f, F, g, G, s, S, p, u, U, x, X, o, O
The same reorderable placeholder may be used more than once, for example,
"We had to destroy the %0 in order to save the %0."
There is no need to nil-terminate the argument list.
DISCUSSION
As is typical of methods based on va_arg, it's easy to cause a crash. Here,
the number of placeholders with unique index numbers in in the first argument
(the format string) must be <= number of remaining arguments (placeholder
substitutions). (The alternative design is to require that argument lists
be nil-terminated, but that often results in crashes too.) For example, the
following formatString will cause a crash if it is not followed by at least
two arguments:
"We had to destroy the %@ in order to save the %@."
You can still use the simple reorderable NSString placeholders %0, %1, %2,...
to process numbers by preprocessing the arguments.
If you want to substitute in an integer, preprocess it with
+[NSString stringWithInt:].
If you want to substitute in a float, preprocess it with
+[NSString localizedStringWithFormat:],
which will give a string "locale-ized" to the user's default locale.
For example, the decimal point may be a comma in some locales.
*/
+ (NSString*)localizeFormat:(NSString*)formatString, ... ;
/*
Same as localizeFormat:, except that if formatString is not found in any relevant
"Localizable.strings", will return formatString and will not log anything
to stderr.
*/
+ (NSString*)weaklyLocalizeFormat:(NSString*)formatString, ... ;
/*
Convenient and cheaper-running method for when there are no substitutions.
keyString should be a key in Localizable.strings.
keyString may include literal percent characters (%) without escaping.
*/
+ (NSString*)localize:(NSString*)keyString ;
/*
Same as localize:, except that if keyString is not found in any relevant
"Localizable.strings", will return formatString and will not log anything
to stderr.
*/
+ (NSString*)localizeWeakly:(NSString*)keyString ;
/*
Returns the nominal two-letter code of the language which is loaded and currently
running in the application. Actually, it simply returns the value of the entry
in Localizable.strings for the key "000_language", but if this entry is missing
it returns "en" (for English).
*/
+ (NSString*)languageCodeLoaded ;
@end