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
Original file line number Diff line number Diff line change
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:

Hello Arthur, do you know Barbara ?
Hello Arthur, do you know Barbara?
Bonjour Arthur, est-ce que tu connais 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:

```objc
id data = @{
@"localize": [GRMustacheHelper helperWithBlock:^(GRMustacheSection *section) {
return NSLocalizedString(section.innerTemplateString, nil);
}]
};
NSString *templateString = @"{{#localize}}Hello{{/localize}}";
// Bonjour, Hola, Hello
NSString *rendering = [GRMustacheTemplate renderObject:data
fromString:templateString
error:NULL];
```
```objc
id data = @{
@"localize": [GRMustacheHelper helperWithBlock:^(GRMustacheSection *section) {
return NSLocalizedString(section.innerTemplateString, nil);
}]
};

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

// Bonjour, Hola, Hello
NSString *rendering = [GRMustacheTemplate renderObject:data
fromString:templateString
error:NULL];
```
`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:
```objc
id data = @{
@"greeting": @"Hello",
@"localize": [GRMustacheHelper helperWithBlock:^(GRMustacheSection *section) {
return NSLocalizedString([section render], nil);
}]
};
```objc
id data = @{
@"greeting": @"Hello",
@"localize": [GRMustacheHelper helperWithBlock:^(GRMustacheSection *section) {
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
NSString *rendering = [GRMustacheTemplate renderObject:data
fromString:templateString
error:NULL];
```
3. We'll localize the localizable format string with `NSLocalizedString`, that will give us the *localized format string*:

- `@"Hello %@, do you know %@?"`
- `@"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)
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
#import "GRAppDelegate.h"
#import "GRMustache.h"

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

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


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

@implementation LocalizatingHelper

- (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].
*
* 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 %@?",
* which is a string that is suitable for localization.
*
* We'll assume that the localized version of this string is another similar
* string that is suitable for [NSString stringWithFormat:].
*
* We still need the values to fill the format: "Arthur", and "Barbara".
* 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.values array.
* self.formatArguments array, here initialized as an empty array.
*/

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


/**
Expand All @@ -126,25 +124,34 @@ - (NSString *)renderSection:(GRMustacheSection *)section
* Render!
*
* [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;
switch (self.values.count) {
switch (self.formatArguments.count) {
case 0:
rendering = localizedFormat;
break;

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

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;

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;
}

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

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

Expand All @@ -162,10 +169,11 @@ - (void)template:(GRMustacheTemplate *)template willInterpretReturnValueOfInvoca
/**
* 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.