Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Cro::WebApp::I18N #42

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 62 additions & 15 deletions lib/Cro/WebApp/I18N.pm6
Original file line number Diff line number Diff line change
@@ -1,33 +1,80 @@
use Cro::HTTP::Router :plugin;
use Cro::HTTP::Router :DEFAULT, :plugin;
use POFile;

my $plugin-key = router-plugin-register('cro-webapp-i18n');
my $plugin-key = router-plugin-register('cro-webapp-i18n-files');
my $prefix-key = router-plugin-register('cro-webapp-i18n-prefix');

sub load-translation-file(Str $prefix, $file) is export {
router-plugin-add-config($plugin-key, $prefix => POFile.load($file));
my class TranslationFile {
has Str:D $.prefix is required;
has POFile:D $.file is required;
has Str @.languages;
}

#| Load a translation file and store it with a given prefix and a given (set of) language
sub load-translation-file(Str:D $prefix, $file, :language(:languages(@languages))) is export {
my $pofile = POFile.load($file);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably wants some defensive CATCH.

my $translation-file = TranslationFile.new(:@languages, :$prefix, file => $pofile);
router-plugin-add-config($plugin-key, $translation-file);
}

#| Configure the default prefix `_` should use.
#| This is useful for reducing duplication, especially in templates.
sub _-prefix(Str $prefix) is export {
router-plugin-add-config($prefix-key, $prefix);
}

#| Install a language selection handler.
#| That handler will receive a list of languages accepted by the client (from the Accept-Language header),
#| and should return a language that will be used to filter against the loaded translation files.
sub select-language(Callable $fn) is export {
# XXX We might register multiple `before-matched`, which is LTA
before-matched {
my @languages = get-languages(request.header('accept-language'));
request.annotations<language> = $fn(@languages);
}
}

#| Look up key and return its associated translation
sub _(Str $key, Str :$prefix is copy, Str :$default) is export {
without $prefix {
my @prefixes = router-plugin-get-innermost-configs($prefix-key)
or die "No prefix configured, did you mean to configure _-prefix or use the long form of _?";
$prefix = @prefixes[*-1];
or die "No prefix configured, did you forget to call `_-prefix` or pass the prefix to _?";
$prefix = @prefixes[*- 1];
}
my %config = router-plugin-get-configs($plugin-key);
with %config{$prefix} {
with $_{$key} {
.msgstr;
} orwith $default {
$default
} else {
die "No key $key in $prefix";
my $language = guess-language;
my %files = router-plugin-get-configs($plugin-key)
.grep(*.prefix eq $prefix)
.classify({ match-language(.languages, $language) });
for |(%files{"1"} // ()), |(%files{"2"} // ()), |(%files{"3"} // ()) {
with .file{$key} {
return .msgstr;
}
}
$default // die "No key $key in $prefix";
}

sub match-language(Str @languages, Str $accept --> Int) {
if +@languages && $accept.defined {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is taking the length here really necessary? Empty positionals are boolified as False.

return 1 if any(@languages) eq $accept;
return 2 if $accept ~~ /^@languages'-'/;
# XXX is this fuzzy matching really necessary
return 4
} else {
return 3
}
}

sub guess-language(--> Str) {
try { request.annotations<language> } // Str
}

# TODO move this to Request
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either do or remove the TODO?

sub get-languages($header) {
with $header {
# TODO q sort
# TODO move this to a request method
$header.split(',')>>.trim.map(*.split(';')[0].trim)
Comment on lines +74 to +76
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or at least Cro::HTTP::Request should have something like quality-header that does this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, are other headers parsed this way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe all the Accept* headers can use this.

} else {
die "No such translation file: $prefix";
()
}
}
53 changes: 34 additions & 19 deletions t/i18n.t
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,45 @@ my class I18NAwareForm is Cro::WebApp::Form {
has $.name is rw is i18n-label('name-field');
}

my $application = route {
load-translation-file('main', 't/resources/test.po');
is get-response(route {
load-translation-file('main', 't/resources/main.po');
_-prefix 'main';

get -> 'render' {
is 'b', _('a', :prefix('main'));
template 'i18n-_.crotmp';
}
}), "b\nb";

ok get-response(route {
load-translation-file('main', 't/resources/main.po');
_-prefix 'main';

get -> 'form' {
get -> 'render' {
template 'i18n-form.crotmp', { foo => I18NAwareForm.empty };
}
}
my $server = Cro::HTTP::Server.new(:$application, :host('localhost'), :port(TEST_PORT));
$server.start;
LEAVE try $server.stop;
my $client = Cro::HTTP::Client.new(base-uri => "http://localhost:{ TEST_PORT }", :cookie-jar);

my $render-response;
lives-ok { $render-response = await $client.get("/render") },
'Can use _ in a template';
ok $render-response.defined;
is await($render-response.body-text), "b\nb";

lives-ok { $render-response = await $client.get("/form") },
'Can render a form in a template';
ok $render-response.defined;
ok await($render-response.body-text) ~~ /'Your Name'/;
}) ~~ /'Your Name'/;

is get-response(route {
# XXX We currently fuzzy-match `en` and `en-XX`, should we really?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is their precedent for doing this elsewehre?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so

load-translation-file('main', 't/resources/main.po', :language<en en-GB en-US>);
load-translation-file('main', 't/resources/main-fr.po', :language<fr fr-FR fr-CH>);
_-prefix 'main';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this looks delightfully odd. I'm tempted to suggest translation-prefix, but then it's maybe less clear that it's about the _ function...perhaps it's fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely To Be Nitpicked™.

select-language -> @ { 'fr' }

get -> 'render' {
template 'i18n-_.crotmp';
}
}), "b mais en français\nb mais en français";

sub get-response($application) {
my $server = Cro::HTTP::Server.new(:$application, :host('localhost'), :port(TEST_PORT));
$server.start;
LEAVE try $server.stop;
my $client = Cro::HTTP::Client.new(base-uri => "http://localhost:{ TEST_PORT }", :cookie-jar);

my $render-response;
lives-ok { $render-response = await $client.get("/render") };
ok $render-response.defined;
return await $render-response.body-text;
}
2 changes: 2 additions & 0 deletions t/resources/main-fr.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
msgid "a"
msgstr "b mais en français"
File renamed without changes.