-
Notifications
You must be signed in to change notification settings - Fork 11
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
use Cro::HTTP::Router :DEFAULT, :plugin; | ||
use POFile; | ||
|
||
my $plugin-key = router-plugin-register('cro-webapp-i18n-files'); | ||
my $prefix-key = router-plugin-register('cro-webapp-i18n-prefix'); | ||
|
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 forget to call `_-prefix` or pass the prefix to _?"; | ||
$prefix = @prefixes[*- 1]; | ||
} | ||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or at least There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, are other headers parsed this way? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe all the |
||
} else { | ||
() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
use Cro::WebApp::I18N; | ||
use Cro::HTTP::Router; | ||
use Cro::HTTP::Client; | ||
use Cro::HTTP::Server; | ||
use Cro::WebApp::Form; | ||
use Cro::WebApp::Template; | ||
use Test; | ||
|
||
my constant TEST_PORT = 30210; | ||
|
||
template-location $*PROGRAM.parent.add('test-data'); | ||
|
||
my class I18NAwareForm is Cro::WebApp::Form { | ||
has $.name is rw is i18n-label('name-field'); | ||
} | ||
|
||
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 -> 'render' { | ||
template 'i18n-form.crotmp', { foo => I18NAwareForm.empty }; | ||
} | ||
}) ~~ /'Your Name'/; | ||
|
||
is get-response(route { | ||
# XXX We currently fuzzy-match `en` and `en-XX`, should we really? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is their precedent for doing this elsewehre? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, this looks delightfully odd. I'm tempted to suggest There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
msgid "a" | ||
msgstr "b mais en français" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
msgid "a" | ||
msgstr "b" | ||
|
||
msgid "name-field" | ||
msgstr "Your Name" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
<&_('a')> | ||
<&_('a', :prefix('main'))> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<&form(.foo)> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this shoulda been done as a trait on the form class for consistency with it being an attribute trait on the fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered it as well, I can change it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍