Skip to content

Commit

Permalink
localization.md WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
groue committed Aug 20, 2012
1 parent 2dff5f4 commit 48e0dc7
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 48 deletions.
243 changes: 214 additions & 29 deletions Guides/sample_code/localization.md
Expand Up @@ -16,7 +16,7 @@ Mustache and GRMustache have no built-in localization feature. It is thus a matt


Into the various renderings below, depending on the current locale: Into the various renderings below, depending on the current locale:


Hello Arthur, do you know Barbara ? Hello Arthur, do you know Barbara?
Bonjour Arthur, est-ce que tu connais Barbara ? Bonjour Arthur, est-ce que tu connais Barbara ?
Hola Arthur, sabes Barbara? Hola Arthur, sabes Barbara?


Expand Down Expand Up @@ -53,20 +53,20 @@ We'll execute our localizing code by attaching to the `localize` section an obje


The shortest way to build a helper is the `[GRMustacheHelper helperWithBlock:]` method. Its block is given a `GRMustacheSection` object whose `innerTemplateString` property perfectly suits our needs: The shortest way to build a helper is the `[GRMustacheHelper helperWithBlock:]` method. Its block is given a `GRMustacheSection` object whose `innerTemplateString` property perfectly suits our needs:


```objc ```objc
id data = @{ id data = @{
@"localize": [GRMustacheHelper helperWithBlock:^(GRMustacheSection *section) { @"localize": [GRMustacheHelper helperWithBlock:^(GRMustacheSection *section) {
return NSLocalizedString(section.innerTemplateString, nil); return NSLocalizedString(section.innerTemplateString, nil);
}] }]
}; };

NSString *templateString = @"{{#localize}}Hello{{/localize}}"; NSString *templateString = @"{{#localize}}Hello{{/localize}}";

// Bonjour, Hola, Hello // Bonjour, Hola, Hello
NSString *rendering = [GRMustacheTemplate renderObject:data NSString *rendering = [GRMustacheTemplate renderObject:data
fromString:templateString fromString:templateString
error:NULL]; error:NULL];
``` ```
`GRMustacheHelper` and `innerTemplateString` are documented in the [helpers.md](../helpers.md) guide. `GRMustacheHelper` and `innerTemplateString` are documented in the [helpers.md](../helpers.md) guide.
Expand Down Expand Up @@ -94,23 +94,208 @@ However, this time, we are not localizing a raw portion of the template. Instead
Fortunately, `GRMustacheSection` objects are able to provide helpers with the rendering of their inner content, `"Hello"` in our case, with their `render` method: Fortunately, `GRMustacheSection` objects are able to provide helpers with the rendering of their inner content, `"Hello"` in our case, with their `render` method:
```objc ```objc
id data = @{ id data = @{
@"greeting": @"Hello", @"greeting": @"Hello",
@"localize": [GRMustacheHelper helperWithBlock:^(GRMustacheSection *section) { @"localize": [GRMustacheHelper helperWithBlock:^(GRMustacheSection *section) {
return NSLocalizedString([section render], nil); return NSLocalizedString([section render], nil);
}] }]
}; };
NSString *templateString = @"{{#localize}}{{greeting}}{{/localize}}";
// Bonjour, Hola, Hello
NSString *rendering = [GRMustacheTemplate renderObject:data
fromString:templateString
error:NULL];
```

You can see this as a "double-pass" rendering: the section is rendered once, in order to turn `{{greeting}}` into `Hello`, and the localization of this string is eventually inserted in the final rendering.

`GRMustacheHelper` and `[GRMustacheSection render]` are documented in the [helpers.md](../helpers.md) guide.


Localizing a template section with arguments
--------------------------------------------

Template:

{{#localize}}
Hello {{name1}}, do you know {{name2}}?
{{/localize}}

Data:

{
name1: "Arthur",
name2: "Barbara"
}

Rendering:

Hello Arthur, do you know Barbara?
Bonjour Arthur, est-ce que tu connais Barbara ?
Hola Arthur, sabes Barbara?

Before diving in the sample code, let's first describe out strategy:

1. We'll build the following string, the *localizable format string*:

`@"Hello %@, do you know %@?"`

2. We'll gather the *format arguments*:


NSString *templateString = @"{{#localize}}{{greeting}}{{/localize}}"; - `@"Arthur"`
- `@"Barbara"`


// Bonjour, Hola, Hello 3. We'll localize the localizable format string with `NSLocalizedString`, that will give us the *localized format string*:
NSString *rendering = [GRMustacheTemplate renderObject:data
fromString:templateString - `@"Hello %@, do you know %@?"`
error:NULL]; - `@"Bonjour %@, est-ce que tu connais %@ ?"`
``` - `@"Hola %@, sabes %@?"`


`GRMustacheHelper` and `[GRMustacheSection render]` are documented in the [helpers.md](../helpers.md) guide. 4. We'll finally use `[NSString stringWithFormat:]`, with the localized format string, and format arguments:

- `@"Hello Arthur, do you know Barbara?"`
- `@"Bonjour Arthur, est-ce que tu connais Barbara ?"`
- `@"Hola Arthur, sabes Barbara?"`

The tricky part is building the *localizable format string* and extracting the *format arguments*. We could most certainly "manually" parse the inner template string of the section, `Hello {{name1}}, do you know {{name2}}?`. However, we'll take a more robust and reusable path.

The [GRMustacheDelegate](../delegate.md) protocol is a nifty tool: it lets you know what GRMustache is about to render, and replace it with whatever value you want.

This looks like a nice way to build our format arguments and the localizable format string in a single strike: instead of letting GRMustache render `Arthur` and `Barbara`, we'll put those values away, and tell the library to render `%@` instead.

We'll thus now attach to the `localize` section an object that conforms to *both* the `GRMustacheHelper` and `GRMustacheTemplateDelegate` protocols. As in the previous example, we'll perform a "double-pass" rendering: the first rendering will use the delegate side, build the localizable format string, and fill the format arguments. The second rendering will simply mix the format and the arguments.

Now the `[GRMustacheHelper helperWithBlock:]` is not enough. Let's write a full class:

```objc
@interface LocalizatingHelper : NSObject<GRMustacheHelper, GRMustacheTemplateDelegate>
@property (nonatomic, strong) NSMutableArray *formatArguments;
@end

@implementation LocalizatingHelper

/**
* GRMustacheHelper method
*/


- (NSString *)renderSection:(GRMustacheSection *)section
{
/**
* Let's perform a first rendering of the section, invoking
* [section render].
*
* This method returns the rendering of the section
* ("Hello {{name1}}! Do you know {{name2}}?" in our specific example).
*
* Normally, it would return "Hello Arthur! Do you know Barbara?", which
* we could not localize.
*
* But we are also a GRMustacheTemplateDelegate, and as such, GRMustache
* will tell us when it is about to render a value.
*
* In the template:willInterpretReturnValueOfInvocation:as: delegate method,
* we'll tell GRMustache to render "%@" instead of the actual values
* "Arthur" and "Barbara".
*
* The rendering of the section will thus be "Hello %@! Do you know %@?",
* which is a string that is suitable for localization.
*
* We still need the format arguments to fill the format: "Arthur", and
* "Barbara".
*
* They also be gathered in the delegate method, that will fill the
* self.formatArguments array, here initialized as an empty array.
*/

self.formatArguments = [NSMutableArray array];
NSString *localizableFormat = [section render]; // triggers delegate callbacks


/**
* Now localize the format.
*/

NSString *localizedFormat = NSLocalizedString(localizableFormat, nil);


/**
* Render!
*
* [NSString stringWithFormat:] unfortunately does not accept an array of
* formatArguments to fill the format. Let's support up to 3 arguments:
*/

NSString *rendering = nil;
switch (self.formatArguments.count) {
case 0:
rendering = localizedFormat;
break;

case 1:
rendering = [NSString stringWithFormat:localizedFormat, [self.formatArguments objectAtIndex:0]];
break;

case 2:
rendering = [NSString stringWithFormat:localizedFormat, [self.formatArguments objectAtIndex:0], [self.formatArguments objectAtIndex:1]];
break;
}


/**
* Cleanup and return the rendering
*/

self.formatArguments = nil;
return rendering;
}


/**
* GRMustacheTemplateDelegate method
*/

- (void)template:(GRMustacheTemplate *)template willInterpretReturnValueOfInvocation:(GRMustacheInvocation *)invocation as:(GRMustacheInterpretation)interpretation
{
/**
* invocation.returnValue is "Arthur" or "Barbara".
*
* Fill self.formatArguments so that we have arguments for
* [NSString stringWithFormat:].
*/

[self.formatArguments addObject:invocation.returnValue];


/**
* Render "%@" instead of the value.
*/

invocation.returnValue = @"%@";
}

@end
```
With such a helper, the rendering is easy:
```objc
id data = @{
@"name1": @"Arthur",
@"name2": @"Barbara",
@"localize": [[LocalizatingHelper alloc] init]
};
NSString *templateString = @"{{#localize}}Hello {{name1}}! Do you know {{name2}}?{{/localize}}";
// Hello Arthur, do you know Barbara?
// Bonjour Arthur, est-ce que tu connais Barbara ?
// Hola Arthur, sabes Barbara?
NSString *rendering = [GRMustacheTemplate renderObject:data
fromString:templateString
error:NULL];
```


[up](../../../../tree/master/Guides/sample_code), [next](../forking.md) [up](../../../../tree/master/Guides/sample_code), [next](../forking.md)
Expand Up @@ -23,7 +23,7 @@
#import "GRAppDelegate.h" #import "GRAppDelegate.h"
#import "GRMustache.h" #import "GRMustache.h"


@interface LocalizatingHelper : NSObject<GRMustacheHelper> @interface LocalizatingHelper : NSObject<GRMustacheHelper, GRMustacheTemplateDelegate>
@end @end


@implementation GRAppDelegate @implementation GRAppDelegate
Expand Down Expand Up @@ -74,16 +74,16 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
@end @end




@interface LocalizatingHelper()<GRMustacheTemplateDelegate> @interface LocalizatingHelper()
@property (nonatomic, strong) NSMutableArray *values; @property (nonatomic, strong) NSMutableArray *formatArguments;
@end @end


@implementation LocalizatingHelper @implementation LocalizatingHelper


- (NSString *)renderSection:(GRMustacheSection *)section - (NSString *)renderSection:(GRMustacheSection *)section
{ {
/** /**
* Let's perform a rendering of the section first, invoking * Let's perform a first rendering of the section, invoking
* [section render]. * [section render].
* *
* This method returns the rendering of the section * This method returns the rendering of the section
Expand All @@ -102,17 +102,15 @@ - (NSString *)renderSection:(GRMustacheSection *)section
* The rendering of the section will thus be "Hello %@! Do you know %@?", * The rendering of the section will thus be "Hello %@! Do you know %@?",
* which is a string that is suitable for localization. * which is a string that is suitable for localization.
* *
* We'll assume that the localized version of this string is another similar * We still need the format arguments to fill the format: "Arthur", and
* string that is suitable for [NSString stringWithFormat:]. * "Barbara".
*
* We still need the values to fill the format: "Arthur", and "Barbara".
* *
* They also be gathered in the delegate method, that will fill the * They also be gathered in the delegate method, that will fill the
* self.values array. * self.formatArguments array, here initialized as an empty array.
*/ */


self.values = [NSMutableArray array]; self.formatArguments = [NSMutableArray array];
NSString *localizableFormat = [section render]; NSString *localizableFormat = [section render]; // triggers delegate callbacks




/** /**
Expand All @@ -126,25 +124,34 @@ - (NSString *)renderSection:(GRMustacheSection *)section
* Render! * Render!
* *
* [NSString stringWithFormat:] unfortunately does not accept an array of * [NSString stringWithFormat:] unfortunately does not accept an array of
* values to fill the format. Let's support 0,1,2 and 3 arguments: * formatArguments to fill the format. Let's support up to 3 arguments:
*/ */


NSString *rendering = nil; NSString *rendering = nil;
switch (self.values.count) { switch (self.formatArguments.count) {
case 0: case 0:
rendering = localizedFormat; rendering = localizedFormat;
break; break;


case 1: case 1:
rendering = [NSString stringWithFormat:localizedFormat, [self.values objectAtIndex:0]]; rendering = [NSString stringWithFormat:
localizedFormat,
[self.formatArguments objectAtIndex:0]];
break; break;


case 2: case 2:
rendering = [NSString stringWithFormat:localizedFormat, [self.values objectAtIndex:0], [self.values objectAtIndex:1]]; rendering = [NSString stringWithFormat:
localizedFormat,
[self.formatArguments objectAtIndex:0],
[self.formatArguments objectAtIndex:1]];
break; break;


case 3: case 3:
rendering = [NSString stringWithFormat:localizedFormat, [self.values objectAtIndex:0], [self.values objectAtIndex:1], [self.values objectAtIndex:2]]; rendering = [NSString stringWithFormat:
localizedFormat,
[self.formatArguments objectAtIndex:0],
[self.formatArguments objectAtIndex:1],
[self.formatArguments objectAtIndex:2]];
break; break;
} }


Expand All @@ -153,7 +160,7 @@ - (NSString *)renderSection:(GRMustacheSection *)section
* Cleanup and return the rendering * Cleanup and return the rendering
*/ */


self.values = nil; self.formatArguments = nil;
return rendering; return rendering;
} }


Expand All @@ -162,10 +169,11 @@ - (void)template:(GRMustacheTemplate *)template willInterpretReturnValueOfInvoca
/** /**
* invocation.returnValue is "Arthur" or "Barbara". * invocation.returnValue is "Arthur" or "Barbara".
* *
* Fill self.values so that we have arguments for [NSString stringWithFormat:]. * Fill self.formatArguments so that we have arguments for
* [NSString stringWithFormat:].
*/ */


[self.values addObject:invocation.returnValue]; [self.formatArguments addObject:invocation.returnValue];




/** /**
Expand Down

0 comments on commit 48e0dc7

Please sign in to comment.