i18n: working i18n based on gettext

This commit is contained in:
Vincent Breitmoser 2019-09-27 16:21:10 +02:00
parent 331ef566ef
commit 0fced131c5
No known key found for this signature in database
GPG Key ID: 7BD18320DEADFA11
25 changed files with 382 additions and 230 deletions

View File

@ -1,26 +1,24 @@
<!doctype html>
<html lang=en>
<html lang="en">
<head>
<meta charset=utf-8>
<title>Manage your key on {{domain}}</title>
<title>{{ text "Manage your key on {{domain}}" rerender }}</title>
</head>
<body>
<p>
Hi,
{{ text "Hi," }}
<p>
this is an automated message from <a rel="nofollow" href="{{base_uri}}" style="text-decoration:none; color: #333"><tt>{{domain}}</tt></a>. If you didn't
request this message, please ignore it.
{{ text "this is an automated message from <a rel=\"nofollow\" href=\"{{ base_uri }}\" style=\"text-decoration:none; color: #333\"><tt>{{ domain }}</tt></a>." rerender }}
{{ text "If you didn't request this message, please ignore it." }}
<p>
OpenPGP key: <tt>{{primary_fp}}</tt>
{{ text "OpenPGP key: <tt>{{ primary_fp }}</tt>" rerender }}
<p>
To manage and delete listed addresses on this key, please follow
the link below:
{{ text "To manage and delete listed addresses on this key, please follow the link below:" }}
<p>
<a href="{{uri}}">{{uri}}</a>
<p>
You can find more info at <a href="{{base_uri}}/about">{{domain}}/about</a>.
{{ text "You can find more info at <a href="{{ base_uri }}/about">{{ domain }}/about</a>." rerender }}
<p>
Greetings from the <a rel="nofollow" href="{{base_uri}}" style="text-decoration:none; color: #333"><tt>keys.openpgp.org</tt></a> team
{{ text "Greetings from the <a rel=\"nofollow\" href=\"{{ base_uri }}\" style=\"text-decoration:none; color: #333\"><tt>keys.openpgp.org</tt></a> team" rerender }}
</body>
</html>

View File

@ -1,16 +1,16 @@
Hi,
{{ text "Hi," }}
this is an automated message from {{domain}}. If you didn't
request this message, please ignore it.
{{ text "this is an automated message from {{domain}}. If you didn't" rerender }}
{{ text "request this message, please ignore it." }}
OpenPGP key: {{primary_fp}}
{{ text "OpenPGP key: {{ primary_fp }}" rerender }}
To manage and delete listed addresses on this key, please follow
the link below:
{{ text "To manage and delete listed addresses on this key, please follow" }}
{{ text "the link below:" }}
{{uri}}
{{ uri }}
You can find more info at {{base_uri}}/about
{{ text "You can find more info at {{ base_uri }}/about" }}
Greetings from the keys.openpgp.org team
{{ text "Greetings from the keys.openpgp.org team" }}

View File

@ -1 +1 @@
Bad request: {{error}}
Bad request: {{ page/error }}

View File

@ -1,7 +1,7 @@
{{#> layout }}
<h2>Error</h2>
<h2>{{ text "Error" }}</h2>
<p>Looks like something went wrong :(</p>
<p>{{ text "Looks like something went wrong :(" }}</p>
<p><strong>Error:</strong> {{internal_error}}</p>
<p>{{ text "<strong>Error:</strong> {{ internal_error }}" }}</p>
{{/layout}}

View File

@ -1,4 +1,5 @@
{{#> layout }}
{{#with page}}
<div class="about usage">
<center><h2><a href="/about">About</a> | <a href="/about/news">News</a> | Usage | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Stats</a> | <a href="/about/privacy">Privacy</a></h2></center>
@ -93,7 +94,7 @@
You can try this shortcut for uploading your key, which outputs
a direct link to the verification page:
<blockquote>
gpg --export your_address@example.net | curl -T - {{base_uri}}
gpg --export your_address@example.net | curl -T - {{ ../base_uri }}
</blockquote>
</li>
<li>
@ -156,4 +157,5 @@
</p>
</div>
{{/with}}
{{/layout}}

View File

@ -1,20 +1,20 @@
{{#> layout }}
<div class="ui">
<p>
We found an entry for <span class="email">{{ query }}</span>:
</p>
{{#with page}}
<div class="ui">
<p>
{{ text "We found an entry for <span class=\"email\">{{ query }}</span>:" rerender }}
</p>
<p>
<a href="{{base_uri}}/vks/v1/by-fingerprint/{{ fpr }}">{{base_uri}}/vks/v1/by-fingerprint/{{ fpr }}</a>
</p>
<p>
<a href="{{ ../base_uri }}/vks/v1/by-fingerprint/{{ fpr }}">{{ ../base_uri }}/vks/v1/by-fingerprint/{{ fpr }}</a>
</p>
<p>
<strong>Hint:</strong> It's more convenient to use
<span class="brand">keys.openpgp.org</span> from your OpenPGP software.<br />
Take a look at our <a href="/about/usage">usage guide</a> for details.
</p>
<div class="debug_link" style="color: transparent;">
<a href="{{base_uri}}/debug?q={{ fpr }}">debug info</a>
<p>
{{text "<strong>Hint:</strong> It's more convenient to use <span class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at our <a href=\"/about/usage\">usage guide</a> for details." }}
</p>
<div class="debug_link" style="color: transparent;">
<a href="{{ ../base_uri }}/debug?q={{ fpr }}">{{ text "debug info" }}</a>
</div>
</div>
</div>
{{/with}}
{{/layout}}

View File

@ -1,14 +1,31 @@
{{#> layout }}
{{> search-form}}
<p>You can also <a href="/upload">upload</a> or <a href="/manage">manage</a> your key.</p>
{{#with page}}
{{#with error}}
<p><strong>Error</strong>: {{ this }}</p>
{{/with}}
<form action="/search" method="GET">
<div class="search">
<input type="text" class="searchTerm" id="search" name="q" autofocus placeholder="{{ text "Search by Email Address / Key ID / Fingerprint" }}">
<button type="submit" class="searchButton button">
<img src="/assets/search.svg" style="width: 1em; padding-bottom: 4px;"> {{ text "Search" }}
</button>
</div>
</form>
<p>
Find out more <a href="/about">about this service</a>.
{{ text "You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</a> your key." }}
</p>
<p>
{{ text "Find out more <a href=\"/about\">about this service</a>." }}
</p>
<hr />
<p>
<strong>News:</strong> <a href="/about/news#2019-09-12-three-months-later">Three months after launch ✨</a> (2019-09-12)
<strong>{{ text "News:" }}</strong> <a href="/about/news#2019-09-12-three-months-later">Three months after launch ✨</a> (2019-09-12)
</p>
{{/with}}
{{/layout}}

View File

@ -1,27 +1,24 @@
<!doctype html>
<html lang="en">
<html lang="{{lang}}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/assets/site.css?v=18" type="text/css"/>
<title>keys.openpgp.org</title>
</head>
<body lang="en">
<body lang="{{lang}}">
<div class="card">
<h1><a class="brand" href="/">keys.openpgp.org</a></h1>
{{#with error }}
<p><strong>Error</strong>: {{ this }}</p>
{{/with}}
{{> @partial-block }}
<div class="spacer"></div>
</div>
<div class="attribution">
<p>
<a href="https://gitlab.com/hagrid-keyserver/hagrid/">Hagrid</a>
v{{ version }} built from
{{ text "v{{ version }} built from" rerender }}
<a href="https://gitlab.com/hagrid-keyserver/hagrid/commit/{{ commit }}">{{ commit }}</a>
</p>
<p>Powered by <a href="https://sequoia-pgp.org">Sequoia-PGP</a></p>
<p>Background image retrieved from <a href="https://www.toptal.com/designers/subtlepatterns/subtle-grey/">Subtle Patterns</a> under CC BY-SA 3.0</p>
<p>{{ text "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>" }}</p>
<p>{{ text "Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0" }}</p>
</div>
</body>
</html>

View File

@ -1,8 +1,7 @@
{{#> layout }}
<h2>Maintenance Mode</h2>
<h2>{{ text "Maintenance Mode" }}</h2>
<p>
{{message}}
</p>
{{/layout}}

View File

@ -1,18 +1,25 @@
{{#> layout }}
<center><h2>Manage your key</h2></center>
{{#with page}}
{{#with error }}
<p><strong>Error</strong>: {{ this }}</p>
{{/with}}
<center><h2>{{ text "Manage your key" }}</h2></center>
<form action="/manage" method="POST">
<div class="manage">
<input type="text" name="search_term" class="manageEmail" autofocus
placeholder="Enter any verified e-mail address of your key">
placeholder="{{ text "Enter any verified e-mail address of your key" }}">
<button type="submit" class="manageButton button">
Send link
{{ text "Send link" }}
</button>
</div>
</form>
<p>
We will send you an e-mail with a link you can use to remove any of your
e-mail addresses from search.
{{ text "We will send you an e-mail with a link you can use to remove any of your e-mail addresses from search." }}
</p>
{{/with}}
{{/layout}}

View File

@ -1,11 +1,12 @@
{{#> layout }}
{{#with page}}
<p>
Managing the key <span class="fingerprint"><a href="{{key_link}}" target="_blank">{{key_fpr}}</a></span>.
{{ text "Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>." rerender }}
</p>
{{#if uid_status}}
<p style="padding-top: 1em;">
Your key is published with the following identity information:
{{ text "Your key is published with the following identity information:" }}
</p>
{{#each uid_status}}
@ -14,7 +15,7 @@
<form action="/manage/unpublish" method="post">
<input type="hidden" name="token" value="{{../token}}" />
<input type="hidden" name="address" value="{{address}}" />
<input type="submit" class="link" value="Delete">
<input type="submit" class="link" value="{{ text "Delete" }}">
</form>
</div>
<p>
@ -24,19 +25,18 @@
{{/each}}
<p style="line-height: 1.8em;">
Clicking "delete" on any address will remove it from this key. It will no longer appear in a search.<br />
To add another address, <a href="/upload">upload</a> the key again.
{{ text "Clicking \"delete\" on any address will remove it from this key. It will no longer appear in a search.<br /> To add another address, <a href=\"/upload\">upload</a> the key again." }}
</p>
{{else}}
<p style="padding-top: 1em;">
Your key is published as only non-identity information.
(<a href="/about" target="_blank">what does this mean?</a>)
{{ text "Your key is published as only non-identity information. (<a href=\"/about\" target=\"_blank\">what does this mean?</a>)" }}
</p>
<p>
To add an address, <a href="/upload">upload</a> the key again.
{{ text "To add an address, <a href=\"/upload\">upload</a> the key again." }}
</p>
{{/if}}
{{/with}}
{{/layout}}

View File

@ -1,5 +1,7 @@
{{#> layout }}
{{#with page}}
<p>
We have sent an email with further instructions to <span class="email">{{address}}</span>.
{{ text "We have sent an email with further instructions to <span class=\"email\">{{ address }}</span>" rerender }}.
</p>
{{/with}}
{{/layout}}

View File

@ -1,10 +0,0 @@
<div class="row">
<form action="/search" method=GET>
<div class="search">
<input type="text" class="searchTerm" id="search" name="q" autofocus placeholder="Search by Email Address / Key ID / Fingerprint">
<button type="submit" class="searchButton button">
<img src="/assets/search.svg" style="width: 1em; padding-bottom: 4px;"> Search
</button>
</div>
</form>
</div>

View File

@ -1,3 +1,5 @@
{{#> layout }}
<p>This address was already verified.</p>
{{#with page}}
<p>{{ text "This address was already verified." }}</p>
{{/with}}
{{/layout}}

View File

@ -1,20 +1,19 @@
{{#> layout}}
<div class="row" id="verification-result">
{{#if verified }}
{{#with page}}
<div id="verification-result">
{{#if verified}}
<p>
Your key
<span class="fingerprint">{{key_fpr}}</span>
is now published
for the identity <a href="{{userid_link}}" target="_blank"><span class="email">{{ userid }}</span></a>.
{{ text "Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</span></a>." rerender }}
</p>
{{else}}
<p>
Verification failed! Perhaps the link you used was expired?
{{ text "Verification failed! Perhaps the link you used was expired?" }}
</p>
<p>
You can <a href="/upload">try uploading again</a>.
{{ text "You can <a href=\"/upload\">try uploading again</a>." }}
</p>
{{/if}}
</div>
{{/with}}
{{/layout}}

View File

@ -1,15 +1,17 @@
{{#> layout }}
{{#with page}}
<p>
Your keys have been successfully uploaded:
{{ text "Your keys have been successfully uploaded:" }}
</p>
<ul>
{{#each keys}}
<li><span class="fingerprint"><a href="{{key_link}}" target="_blank">{{key_fpr}}</a></span></li>
<li><span class="fingerprint"><a href="{{ key_link }}" target="_blank">{{ key_fpr }}</a></span></li>
{{/each}}
</ul>
<p>
<strong>Note:</strong> To make keys searchable by address, you must upload them individually.
{{ text "<strong>Note:</strong> To make keys searchable by address, you must upload them individually." }}
</p>
{{/with}}
{{/layout}}

View File

@ -1,75 +1,66 @@
{{#> layout }}
{{#with page}}
<p>
You uploaded the key <span class="fingerprint"><a href="{{key_link}}" target="_blank">{{key_fpr}}</a></span>.
{{ text "You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>." rerender }}
</p>
{{#if is_revoked}}
<p>
<strong>This key is revoked.</strong>
It is published without identity information
(<a href="/about" target="_blank">what does this mean?</a>),
and can't be made available for search
by e-mail address.
<strong>{{ text "This key is revoked." }}</strong>
{{ text "It is published without identity information and can't be made available for search by e-mail address" }}
(<a href="/about" target="_blank">{{ text "what does this mean?" }}</a>).
</p>
{{else}}
{{#if email_published}}
<p style="padding-top: 1em;">
This key is now published with the following identity information (<a href="/about" target="_blank">what does this mean?</a>):
{{ text "This key is now published with the following identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>):" }}
</p>
{{#each email_published}}
<div class="publishedUid">
<div>Published</div>
<div>{{ text "Published" }}</div>
<p><span class="email">{{this}}</span></p>
</div>
{{/each}}
{{else}}
<p style="padding-top: 1em;">
This key is now published with only non-identity information (<a href="/about" target="_blank">what does this mean?</a>)
{{ text "This key is now published with only non-identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>)" }}
</p>
{{/if}}
{{#if email_unpublished}}
<p style="padding-top: 1em;">
To make the key available for search by e-mail address, you can verify it belongs to you:
{{ text "To make the key available for search by e-mail address, you can verify it belongs to you:" }}
</p>
{{#each email_unpublished}}
<div class="publishedUid">
<div>
{{#if requested}}
Verification Pending
{{ text "Verification Pending" }}
{{else}}
<form action="/upload/request-verify" method="post">
<input type="hidden" name="token" value="{{../token}}" />
<input type="hidden" name="address" value="{{address}}" />
<input type="submit" class="link" value="Send Verification Mail">
<input type="submit" class="link" value="{{ text "Send Verification Mail" }}">
</form>
{{/if}}
</div>
<p><span class="email">{{address}}</span></p>
<p><span class="email">{{ address }}</span></p>
</div>
{{/each}}
<p>
<strong>Note:</strong> Some providers delay e-mails for up to 15 minutes
to prevent spam. Please be patient.
{{ text "<strong>Note:</strong> Some providers delay e-mails for up to 15 minutes to prevent spam. Please be patient." }}
</p>
{{/if}}
{{#if count_unparsed}}
{{#if count_unparsed_one}}
<p style="padding-top: 1em;">
This key contains one identity that could not be parsed as an email
address.<br />
This identity can't be published
on <span class="brand">keys.openpgp.org</span>.
(<a href="/about/faq#non-email-uids" target="_blank">why?</a>)
{{ text "This key contains one identity that could not be parsed as an email address.<br /> This identity can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">why?</a>)" }}
</p>
{{else}}
<p style="padding-top: 1em;">
This key contains {{count_unparsed}} identities that could not be parsed
as an email address.<br />
These identities can't be published
on <span class="brand">keys.openpgp.org</span>.
(<a href="/about/faq#non-email-uids" target="_blank">why?</a>)
{{ text "This key contains {{ count_unparsed }} identities that could not be parsed as an email address.<br /> These identities can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">why?</a>)" rerender }}
</p>
{{/if}}
{{/if}}
@ -77,18 +68,18 @@
{{#if count_revoked}}
{{#if count_revoked_one}}
<p style="padding-top: 1em;">
This key contains one revoked identity, which is not published.
(<a href="/about/faq#revoked-uids" target="_blank">Why?</a>)
{{ text "This key contains one revoked identity, which is not published." }}
{{ text "(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)" }}
</p>
{{else}}
<p style="padding-top: 1em;">
This key contains {{count_revoked}} revoked identities, which are not
published.
(<a href="/about/faq#revoked-uids" target="_blank">Why?</a>)
{{ text "This key contains {{ count_revoked }} revoked identities, which are not published." rerender }}
{{ text "(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)" }}
</p>
{{/if}}
{{/if}}
{{/if}}
{{/with}}
{{/layout}}

View File

@ -1,18 +1,18 @@
{{#> layout }}
<div class="ui">
<center><h2>Upload your key</h2></center>
<center><h2>{{ text "Upload your key" }}</h2></center>
<form action="/upload/submit" method="POST" enctype="multipart/form-data">
<div class="upload">
<input type="file" id="keytext" name="keytext" autofocus class="fileUpload" placeholder="Your public key"/>
<input type="file" id="keytext" name="keytext" autofocus class="fileUpload" placeholder="{{ text "Your public key" }}"/>
<button type="submit" class="uploadButton button">
<img src="/assets/upload.svg" style="width: 1em;"> Upload
<img src="/assets/upload.svg" style="width: 1em;"> {{ text "Upload" }}
</button>
</div>
</form>
<p>
Need more info? Check our <a target="_blank" href="/about">intro</a> and <a target="_blank" href="/about/usage">usage guide</a>!
{{ text "Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and <a target=\"_blank\" href=\"/about/usage\">usage guide</a>!" }}
</p>
<script src="/assets/js/upload.js" async />

View File

@ -1,13 +1,17 @@
{{#> layout}}
<div class="row" id="container">
{{#with page}}
<div id="container">
<p>
Verifying your email address…
{{ text "Verifying your email address…" }}
</p>
<p>
<form method="POST" action="/verify/{{token}}" id="postform">
If the process doesn't complete after a few seconds, <input type="submit" class="textbutton" value="click here" />.
{{ text "If the process doesn't complete after a few seconds, <input type=\"submit\" class=\"textbutton\" value=\"click here\" />." }}
</form>
</p>
</div>
<script src="/assets/js/upload-verify.js" type="text/javascript"></script>
{{/with}}
{{/layout}}

56
src/gettext_strings.rs Normal file
View File

@ -0,0 +1,56 @@
use gettext_macros::t;
fn _dummy() {
t!("Error");
t!("Looks like something went wrong :(");
t!("<strong>Error:</strong> {{ internal_error }}");
t!("We found an entry for <span class=\"email\">{{ query }}</span>:");
t!("debug info");
t!("Search by Email Address / Key ID / Fingerprint");
t!("Search");
t!("You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</a> your key.");
t!("Find out more <a href=\"/about\">about this service</a>.");
t!("News:");
t!("v{{ version }} built from");
t!("Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>");
t!("Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0");
t!("Maintenance Mode");
t!("Manage your key");
t!("Enter any verified e-mail address of your key");
t!("Send link");
t!("We will send you an e-mail with a link you can use to remove any of your e-mail addresses from search.");
t!("Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>.");
t!("Your key is published with the following identity information:");
t!("Clicking \"delete\" on any address will remove it from this key. It will no longer appear in a search.<br /> To add another address, <a href=\"/upload\">upload</a> the key again.");
t!("Your key is published as only non-identity information. (<a href=\"/about\" target=\"_blank\">what does this mean?</a>)");
t!("To add an address, <a href=\"/upload\">upload</a> the key again.");
t!("We have sent an email with further instructions to <span class=\"email\">{{ address }}</span>");
t!("This address was already verified.");
t!("Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</span></a>.");
t!("Verification failed! Perhaps the link you used was expired?");
t!("You can <a href=\"/upload\">try uploading again</a>.");
t!("Your public key");
t!("Upload");
t!("Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and <a target=\"_blank\" href=\"/about/usage\">usage guide</a>!");
t!("You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>.");
t!("This key is revoked.");
t!("It is published without identity information and can't be made available for search by e-mail address");
t!("what does this mean?");
t!("This key is now published with the following identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>):");
t!("Published");
t!("This key is now published with only non-identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>)");
t!("To make the key available for search by e-mail address, you can verify it belongs to you:");
t!("Verification Pending");
t!("<strong>Note:</strong> Some providers delay e-mails for up to 15 minutes to prevent spam. Please be patient.");
t!("Send Verification Mail");
t!("This key contains one identity that could not be parsed as an email address.<br /> This identity can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">why?</a>)");
t!("This key contains {{ count_unparsed }} identities that could not be parsed as an email address.<br /> These identities can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">why?</a>)");
t!("This key contains one revoked identity, which is not published.");
t!("(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)");
t!("This key contains {{ count_revoked }} revoked identities, which are not published.");
t!("(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)");
t!("Your keys have been successfully uploaded:");
t!("<strong>Note:</strong> To make keys searchable by address, you must upload them individually.");
t!("Verifying your email address…");
t!("If the process doesn't complete after a few seconds, <input type=\"submit\" class=\"textbutton\" value=\"click here\" />.");
}

102
src/i18n.rs Normal file
View File

@ -0,0 +1,102 @@
use handlebars::{
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError
};
use std::io;
pub struct I18NHelper {
catalogs: Vec<(&'static str, gettext::Catalog)>,
}
impl I18NHelper {
pub fn new(catalogs: Vec<(&'static str, gettext::Catalog)>) -> Self {
Self { catalogs }
}
pub fn get_catalog(
&self,
lang: &str,
) -> &gettext::Catalog {
let (_, ref catalog) = self.catalogs
.iter()
.find(|(candidate, _)| *candidate == lang)
.unwrap_or_else(|| self.catalogs.get(0).unwrap());
catalog
}
// Traverse the fallback chain,
pub fn lookup<'a>(
&'a self,
lang: &str,
text_id: &'a str,
// args: Option<&HashMap<&str, FluentValue>>,
) -> &'a str {
let catalog = self.get_catalog(lang);
catalog.gettext(text_id)
// format!("Unknown localization {}", text_id)
}
}
#[derive(Default)]
struct StringOutput {
pub s: String,
}
impl Output for StringOutput {
fn write(&mut self, seg: &str) -> Result<(), io::Error> {
self.s.push_str(seg);
Ok(())
}
}
impl HelperDef for I18NHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
reg: &'reg Handlebars,
context: &'rc Context,
rcx: &mut RenderContext<'reg>,
out: &mut dyn Output,
) -> HelperResult {
let id = if let Some(id) = h.param(0) {
id
} else {
return Err(RenderError::new(
"{{text}} must have at least one parameter",
));
};
let id = if let Some(id) = id.value().as_str() {
id
} else {
return Err(RenderError::new("{{text}} takes an identifier parameter"));
};
let rerender = h
.param(1)
.and_then(|p| p
.path()
.map(|v| v == "rerender")
).unwrap_or(false);
let lang = context
.data()
.get("lang")
.expect("Language not set in context")
.as_str()
.expect("Language must be string");
let response = self.lookup(lang, &id);
if rerender {
let data = rcx.evaluate(context, ".", false).unwrap();
let response = reg.render_template(&response, data)
.map_err(RenderError::with)?;
out.write(&response).map_err(RenderError::with)?;
} else {
out.write(&response).map_err(RenderError::with)?;
}
Ok(())
}
}

View File

@ -51,6 +51,8 @@ mod sealed_state;
mod rate_limiter;
mod dump;
mod counters;
mod i18n;
mod gettext_strings;
mod web;
fn main() {

View File

@ -5,7 +5,7 @@ use rocket_i18n::I18n;
use failure::Fallible as Result;
use crate::web::{RequestOrigin, MyResponse, templates::General};
use crate::web::{RequestOrigin, MyResponse};
use crate::web::vks_web;
use crate::database::{Database, KeyDatabase, types::Email, types::Fingerprint};
use crate::mail;
@ -28,8 +28,6 @@ mod templates {
pub base_uri: String,
pub uid_status: Vec<ManageKeyUidStatus>,
pub token: String,
pub commit: String,
pub version: String,
}
#[derive(Serialize)]
@ -59,7 +57,7 @@ pub mod forms {
#[get("/manage")]
pub fn vks_manage() -> Result<MyResponse> {
Ok(MyResponse::ok("manage/manage", General::default()))
Ok(MyResponse::ok_bare("manage/manage"))
}
#[get("/manage/<token>")]
@ -94,8 +92,6 @@ pub fn vks_manage_key(
uid_status,
token,
base_uri: request_origin.get_base_uri().to_owned(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ok("manage/manage_key", context)
},

View File

@ -2,12 +2,13 @@ use rocket;
use rocket::http::{Header, Status};
use rocket::request;
use rocket::outcome::Outcome;
use rocket::response::NamedFile;
use rocket::response::{NamedFile, Responder, Response};
use rocket::config::Config;
use rocket_contrib::templates::Template;
use rocket_contrib::templates::{Template, Engines};
use rocket::http::uri::Uri;
use rocket_contrib::json::JsonValue;
use rocket::response::status::Custom;
use rocket_i18n::I18n;
use rocket_prometheus::PrometheusMetrics;
@ -20,6 +21,7 @@ use std::path::PathBuf;
use crate::mail;
use crate::tokens;
use crate::counters;
use crate::i18n::I18NHelper;
use crate::rate_limiter::RateLimiter;
use crate::database::{Database, KeyDatabase, Query};
@ -40,10 +42,22 @@ use crate::web::maintenance::MaintenanceMode;
use rocket::http::hyper::header::ContentDisposition;
pub struct HagridTemplate(&'static str, serde_json::Value);
impl Responder<'static> for HagridTemplate {
fn respond_to(self, req: &rocket::Request) -> std::result::Result<Response<'static>, Status> {
let HagridTemplate(tmpl, ctx) = self;
let i18n: I18n = req.guard().expect("Error parsing language");
let origin: RequestOrigin = req.guard().expect("Error determining request origin");
let layout_context = templates::HagridLayout::new(ctx, i18n, origin);
Template::render(tmpl, layout_context).respond_to(req)
}
}
#[derive(Responder)]
pub enum MyResponse {
#[response(status = 200, content_type = "html")]
Success(Template),
Success(HagridTemplate),
#[response(status = 200, content_type = "plain")]
Plain(String),
#[response(status = 200, content_type = "application/pgp-keys")]
@ -53,11 +67,11 @@ pub enum MyResponse {
#[response(status = 500, content_type = "html")]
ServerError(Template),
#[response(status = 404, content_type = "html")]
NotFound(Template),
NotFound(HagridTemplate),
#[response(status = 404, content_type = "html")]
NotFoundPlain(String),
#[response(status = 400, content_type = "html")]
BadRequest(Template),
BadRequest(HagridTemplate),
#[response(status = 400, content_type = "html")]
BadRequestPlain(String),
#[response(status = 503, content_type = "html")]
@ -69,8 +83,14 @@ pub enum MyResponse {
}
impl MyResponse {
pub fn ok<S: Serialize>(tmpl: &'static str, ctx: S) -> Self {
MyResponse::Success(Template::render(tmpl, ctx))
pub fn ok(tmpl: &'static str, ctx: impl Serialize) -> Self {
let context_json = serde_json::to_value(ctx).unwrap();
MyResponse::Success(HagridTemplate(tmpl, context_json))
}
pub fn ok_bare(tmpl: &'static str) -> Self {
let context_json = serde_json::to_value(templates::Bare { dummy: () }).unwrap();
MyResponse::Success(HagridTemplate(tmpl, context_json))
}
pub fn plain(s: String) -> Self {
@ -110,7 +130,7 @@ impl MyResponse {
pub fn ise(e: failure::Error) -> Self {
eprintln!("Internal error: {:?}", e);
let ctx = templates::FiveHundred{
let ctx = templates::FiveHundred {
internal_error: e.to_string(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
@ -119,12 +139,9 @@ impl MyResponse {
}
pub fn bad_request(template: &'static str, e: failure::Error) -> Self {
let ctx = templates::General {
error: Some(format!("{}", e)),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::BadRequest(Template::render(template, ctx))
let ctx = templates::Error { error: format!("{}", e) };
let context_json = serde_json::to_value(ctx).unwrap();
MyResponse::BadRequest(HagridTemplate(template, context_json))
}
pub fn bad_request_plain(message: impl Into<String>) -> Self {
@ -137,18 +154,18 @@ impl MyResponse {
pub fn not_found(
tmpl: Option<&'static str>,
message: impl Into<Option<String>>
message: impl Into<Option<String>>,
) -> Self {
MyResponse::NotFound(
Template::render(
tmpl.unwrap_or("index"),
templates::General::new(
Some(message.into()
.unwrap_or_else(|| "Key not found".to_owned())))))
let ctx = templates::Error { error: message.into()
.unwrap_or_else(|| "Key not found".to_owned()) };
let context_json = serde_json::to_value(ctx).unwrap();
MyResponse::NotFound(HagridTemplate(tmpl.unwrap_or("index"), context_json))
}
}
mod templates {
use super::{I18n, RequestOrigin};
#[derive(Serialize)]
pub struct FiveHundred {
pub internal_error: String,
@ -157,44 +174,38 @@ mod templates {
}
#[derive(Serialize)]
pub struct General {
pub struct HagridLayout<T: serde::Serialize> {
pub error: Option<String>,
pub commit: String,
pub version: String,
pub base_uri: String,
pub lang: String,
pub page: T,
}
#[derive(Serialize)]
pub struct About {
pub base_uri: String,
pub commit: String,
pub version: String,
pub struct Error {
pub error: String,
}
impl About {
pub fn new(base_uri: impl Into<String>) -> Self {
#[derive(Serialize)]
pub struct Bare {
// Dummy value to make sure {{#with page}} always passes
pub dummy: (),
}
impl<T: serde::Serialize> HagridLayout<T> {
pub fn new(page: T, i18n: I18n, origin: RequestOrigin) -> Self {
Self {
base_uri: base_uri.into(),
error: None,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
base_uri: origin.get_base_uri().to_string(),
page: page,
lang: i18n.lang.to_string(),
}
}
}
impl General {
pub fn new(error: Option<String>) -> Self {
Self {
error: error,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
}
}
}
impl Default for General {
fn default() -> Self {
Self::new(None)
}
}
}
pub struct HagridState {
@ -276,47 +287,49 @@ fn files(file: PathBuf, state: rocket::State<HagridState>) -> Option<NamedFile>
}
#[get("/")]
fn root() -> Template {
Template::render("index", templates::General::default())
fn root() -> MyResponse {
MyResponse::ok_bare("index")
}
#[get("/about")]
fn about() -> Template {
Template::render("about/about", templates::General::default())
fn about() -> MyResponse {
MyResponse::ok_bare("about/about")
}
#[get("/about/news")]
fn news() -> Template {
Template::render("about/news", templates::General::default())
fn news() -> MyResponse {
MyResponse::ok_bare("about/news")
}
#[get("/about/faq")]
fn faq() -> Template {
Template::render("about/faq", templates::General::default())
fn faq() -> MyResponse {
MyResponse::ok_bare("about/faq")
}
#[get("/about/usage")]
fn usage(state: rocket::State<HagridState>) -> Template {
Template::render("about/usage", templates::About::new(state.base_uri.clone()))
fn usage() -> MyResponse {
MyResponse::ok_bare("about/usage")
}
#[get("/about/privacy")]
fn privacy() -> Template {
Template::render("about/privacy", templates::General::default())
fn privacy() -> MyResponse {
MyResponse::ok_bare("about/privacy")
}
#[get("/about/api")]
fn apidoc() -> Template {
Template::render("about/api", templates::General::default())
fn apidoc() -> MyResponse {
MyResponse::ok_bare("about/api")
}
#[get("/about/stats")]
fn stats() -> Template {
Template::render("about/stats", templates::General::default())
fn stats() -> MyResponse {
MyResponse::ok_bare("about/stats")
}
#[get("/errors/<code>/<template>")]
fn errors(
i18n: I18n,
origin: RequestOrigin,
code: u16,
template: String,
) -> Result<Custom<Template>> {
@ -327,7 +340,7 @@ fn errors(
.ok_or(failure::err_msg("bad request"))?;
let response_body = Template::render(
format!("errors/{}-{}", code, template),
templates::General::default()
templates::HagridLayout::new(templates::Bare{dummy: ()}, i18n, origin)
);
Ok(Custom(status_code, response_body))
}
@ -399,7 +412,11 @@ fn rocket_factory(mut rocket: rocket::Rocket) -> Result<rocket::Rocket> {
let prometheus = configure_prometheus(rocket.config());
rocket = rocket
.attach(Template::fairing())
.attach(Template::custom(|engines: &mut Engines| {
let i18ns = include_i18n!();
let i18n_helper = I18NHelper::new(i18ns);
engines.handlebars.register_helper("text", Box::new(i18n_helper));
}))
.attach(maintenance_mode)
.manage(include_i18n!())
.manage(hagrid_state)

View File

@ -49,29 +49,16 @@ mod template {
pub key_fpr: String,
pub userid: String,
pub userid_link: String,
pub commit: String,
pub version: String,
}
#[derive(Serialize)]
pub struct Search {
pub query: String,
pub fpr: String,
pub base_uri: String,
pub commit: String,
pub version: String,
}
#[derive(Serialize)]
pub struct Upload {
pub commit: String,
pub version: String,
}
#[derive(Serialize)]
pub struct VerificationSent {
pub commit: String,
pub version: String,
pub key_fpr: String,
pub key_link: String,
pub is_revoked: bool,
@ -92,8 +79,6 @@ mod template {
#[derive(Serialize)]
pub struct UploadOkMultiple {
pub commit: String,
pub version: String,
pub keys: Vec<UploadOkKey>,
}
@ -167,8 +152,6 @@ impl MyResponse {
.sort_unstable_by(|fst,snd| fst.address.cmp(&snd.address));
let context = template::VerificationSent {
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
is_revoked,
key_fpr,
key_link,
@ -195,8 +178,6 @@ impl MyResponse {
.collect();
let context = template::UploadOkMultiple {
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
keys,
};
@ -206,12 +187,7 @@ impl MyResponse {
#[get("/upload")]
pub fn upload() -> MyResponse {
let context = template::Upload {
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ok("upload/upload", context)
MyResponse::ok("upload/upload", ())
}
#[post("/upload/submit", format = "multipart/form-data", data = "<data>")]
@ -247,18 +223,16 @@ pub fn process_post_form_data(
#[get("/search?<q>")]
pub fn search(
request_origin: RequestOrigin,
db: rocket::State<KeyDatabase>,
q: String,
) -> MyResponse {
match q.parse::<Query>() {
Ok(query) => key_to_response(request_origin, db, q, query),
Ok(query) => key_to_response(db, q, query),
Err(e) => MyResponse::bad_request("index", e),
}
}
fn key_to_response(
request_origin: RequestOrigin,
db: rocket::State<KeyDatabase>,
query_string: String,
query: Query,
@ -271,10 +245,7 @@ fn key_to_response(
let context = template::Search{
query: query_string,
base_uri: request_origin.get_base_uri().to_owned(),
fpr: fp.to_string(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ok("found", context)
@ -463,8 +434,6 @@ pub fn verify_confirm(
userid: email,
key_fpr: fingerprint,
userid_link,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ok("upload/publish-result", context)
@ -488,4 +457,4 @@ pub fn verify_confirm_form(
MyResponse::ok("upload/verification-form", template::VerifyForm {
token
})
}
}