mirror of
https://github.com/actix/examples
synced 2024-12-18 00:13:57 +01:00
add fluent templates example
This commit is contained in:
parent
0f08afee18
commit
8770694e11
3
.github/workflows/linux.yml
vendored
3
.github/workflows/linux.yml
vendored
@ -50,8 +50,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
with:
|
with:
|
||||||
command: check
|
command: check
|
||||||
# TODO: remove exclude protobuf-example when upgraded; currently fails on nightly
|
args: --workspace --bins --examples --tests
|
||||||
args: --workspace --bins --examples --tests --exclude protobuf-example
|
|
||||||
|
|
||||||
- name: start redis
|
- name: start redis
|
||||||
uses: supercharge/redis-github-action@1.1.0
|
uses: supercharge/redis-github-action@1.1.0
|
||||||
|
@ -45,6 +45,7 @@ members = [
|
|||||||
"server-sent-events",
|
"server-sent-events",
|
||||||
"shutdown-server",
|
"shutdown-server",
|
||||||
"templating/askama",
|
"templating/askama",
|
||||||
|
# "templating/fluent",
|
||||||
"templating/handlebars",
|
"templating/handlebars",
|
||||||
"templating/sailfish",
|
"templating/sailfish",
|
||||||
"templating/tera",
|
"templating/tera",
|
||||||
@ -60,4 +61,11 @@ members = [
|
|||||||
exclude = [
|
exclude = [
|
||||||
# uses incompatible libsqlite-sys to other examples
|
# uses incompatible libsqlite-sys to other examples
|
||||||
"databases/diesel",
|
"databases/diesel",
|
||||||
|
|
||||||
|
# takes infinity time to compile for some unknown reason
|
||||||
|
"templating/fluent",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# [patch.crates-io]
|
||||||
|
# fluent-templates = { git = "https://github.com/robjtede/fluent-templates.git", branch = "fix-circ-dep" }
|
||||||
|
# fluent-template-macros = { git = "https://github.com/robjtede/fluent-templates.git", branch = "fix-circ-dep" }
|
||||||
|
@ -5,9 +5,9 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
diesel = { version = "1.4.8", features = ["sqlite", "r2d2"] }
|
diesel = { version = "1.4", features = ["sqlite", "r2d2"] }
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
@ -5,5 +5,5 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
mongodb = "2.0.0"
|
mongodb = "2"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
16
templating/fluent/Cargo.toml
Normal file
16
templating/fluent/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "templating-fluent"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
actix-web = "4"
|
||||||
|
actix-web-lab = "0.16"
|
||||||
|
fluent-templates = { version = "0.7", features = ["handlebars"] }
|
||||||
|
handlebars = { version = "4.3", features = ["dir_source"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
fluent-templates = { git = "https://github.com/robjtede/fluent-templates.git", branch = "fix-circ-dep" }
|
||||||
|
fluent-template-macros = { git = "https://github.com/robjtede/fluent-templates.git", branch = "fix-circ-dep" }
|
33
templating/fluent/README.md
Normal file
33
templating/fluent/README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Fluent Templates
|
||||||
|
|
||||||
|
This is an example of how to integrate [Fluent Templates](https://crates.io/crates/fluent-templates) with Actix Web using the Handlebars templating engine.
|
||||||
|
|
||||||
|
# Directory Structure
|
||||||
|
|
||||||
|
- `src`: Rust source code for web server and endpoint configuration
|
||||||
|
- `templates`: Handlebars templates (no text content is stored in the templates)
|
||||||
|
- `locales`: Fluent files containing translations for English (en) and French (fr)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd templating/fluent
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
After starting the server, you may visit the following pages:
|
||||||
|
|
||||||
|
- http://localhost:8080
|
||||||
|
- http://localhost:8080/Alice/documents
|
||||||
|
- http://localhost:8080/Bob/passwords
|
||||||
|
- http://localhost:8080/some-non-existing-page - 404 error rendered using template
|
||||||
|
|
||||||
|
This example implements language selection using the standard Accept-Language header, which is sent by browsers according to OS/browser settings. To view the translated pages, pass the Accept-Encoding header with `en` or `fr`. Values which do not have associated translation files will fall back to English.
|
||||||
|
|
||||||
|
```
|
||||||
|
# using HTTPie
|
||||||
|
http :8080/Alice/documents Accept-Language:fr
|
||||||
|
|
||||||
|
# using cURL
|
||||||
|
curl http://localhost:8080/Alice/documents -H 'accept-language: fr'
|
||||||
|
```
|
8
templating/fluent/locales/en/main.ftl
Normal file
8
templating/fluent/locales/en/main.ftl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
home-title = Fluent Templates Example
|
||||||
|
home-description = This is an example of how to use Fluent templates with Actix Web.
|
||||||
|
|
||||||
|
user-title = { $name }'s Homepage
|
||||||
|
user-welcome = Welcome back, { $name }
|
||||||
|
user-data = Here's your { $data }.
|
||||||
|
|
||||||
|
error-not-found = Page not found
|
8
templating/fluent/locales/fr/main.ftl
Normal file
8
templating/fluent/locales/fr/main.ftl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
home-title = Un Exemple de Fluent Templates
|
||||||
|
home-description = Ceci est un exemple d'utilisation des modèles Fluent avec Actix Web.
|
||||||
|
|
||||||
|
user-title = La page d'accueil de { $name }
|
||||||
|
user-welcome = Bienvenue, { $name }
|
||||||
|
user-data = Voici votre { $data }.
|
||||||
|
|
||||||
|
error-not-found = Page non trouvée
|
40
templating/fluent/src/lang_choice.rs
Normal file
40
templating/fluent/src/lang_choice.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
future::{ready, Ready},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_web::{dev, http::header::AcceptLanguage, FromRequest, HttpMessage as _, HttpRequest};
|
||||||
|
use fluent_templates::LanguageIdentifier;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// A convenient extractor that finds the clients's preferred language based on an Accept-Language
|
||||||
|
/// header and falls back to English if header is not found. Serializes easily in Handlebars data.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct LangChoice(String);
|
||||||
|
|
||||||
|
impl LangChoice {
|
||||||
|
pub(crate) fn from_req(req: &HttpRequest) -> Self {
|
||||||
|
let lang = req
|
||||||
|
.get_header::<AcceptLanguage>()
|
||||||
|
.and_then(|lang| lang.preference().into_item())
|
||||||
|
.map(|lang| lang.to_string())
|
||||||
|
.map_or_else(|| "en".to_owned(), |lang| lang.to_string());
|
||||||
|
|
||||||
|
Self(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lang_id(&self) -> LanguageIdentifier {
|
||||||
|
// unwrap: lang ID should be valid given extraction method
|
||||||
|
self.0.parse().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRequest for LangChoice {
|
||||||
|
type Error = Infallible;
|
||||||
|
type Future = Ready<Result<Self, Self::Error>>;
|
||||||
|
|
||||||
|
fn from_request(req: &HttpRequest, _pl: &mut dev::Payload) -> Self::Future {
|
||||||
|
ready(Ok(Self::from_req(req)))
|
||||||
|
}
|
||||||
|
}
|
127
templating/fluent/src/main.rs
Normal file
127
templating/fluent/src/main.rs
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
body::BoxBody,
|
||||||
|
dev::ServiceResponse,
|
||||||
|
get,
|
||||||
|
http::{header::ContentType, StatusCode},
|
||||||
|
middleware::{ErrorHandlerResponse, ErrorHandlers},
|
||||||
|
web, App, HttpResponse, HttpServer, Responder, Result,
|
||||||
|
};
|
||||||
|
use actix_web_lab::{extract::Path, respond::Html};
|
||||||
|
use fluent_templates::{static_loader, FluentLoader, Loader as _};
|
||||||
|
use handlebars::Handlebars;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
mod lang_choice;
|
||||||
|
use self::lang_choice::LangChoice;
|
||||||
|
|
||||||
|
static_loader! {
|
||||||
|
static LOCALES = {
|
||||||
|
locales: "./locales",
|
||||||
|
fallback_language: "en",
|
||||||
|
|
||||||
|
// removes unicode isolating marks around arguments
|
||||||
|
// you typically should only set to false when testing.
|
||||||
|
customise: |bundle| bundle.set_use_isolating(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index(hb: web::Data<Handlebars<'_>>, lang: LangChoice) -> impl Responder {
|
||||||
|
let data = json!({ "lang": lang });
|
||||||
|
let body = hb.render("index", &data).unwrap();
|
||||||
|
Html(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/{user}/{data}")]
|
||||||
|
async fn user(
|
||||||
|
hb: web::Data<Handlebars<'_>>,
|
||||||
|
Path(info): Path<(String, String)>,
|
||||||
|
lang: LangChoice,
|
||||||
|
) -> impl Responder {
|
||||||
|
let data = json!({
|
||||||
|
"lang": lang,
|
||||||
|
"user": info.0,
|
||||||
|
"data": info.1
|
||||||
|
});
|
||||||
|
let body = hb.render("user", &data).unwrap();
|
||||||
|
Html(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
// Handlebars uses a repository for the compiled templates. This object must be shared between
|
||||||
|
// the application threads, and is therefore passed to the App in an Arc.
|
||||||
|
let mut handlebars = Handlebars::new();
|
||||||
|
|
||||||
|
// register template dir with Handlebars registry
|
||||||
|
handlebars
|
||||||
|
.register_templates_directory(".html", "./templates")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// register Fluent helper with Handlebars registry
|
||||||
|
handlebars.register_helper("fluent", Box::new(FluentLoader::new(&*LOCALES)));
|
||||||
|
|
||||||
|
let handlebars = web::Data::new(handlebars);
|
||||||
|
|
||||||
|
HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(error_handlers())
|
||||||
|
.app_data(web::Data::clone(&handlebars))
|
||||||
|
.service(index)
|
||||||
|
.service(user)
|
||||||
|
})
|
||||||
|
.bind(("127.0.0.1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom error handlers, to return HTML responses when an error occurs.
|
||||||
|
fn error_handlers() -> ErrorHandlers<BoxBody> {
|
||||||
|
ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handler for a 404 Page not found error.
|
||||||
|
fn not_found<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<BoxBody>> {
|
||||||
|
let lang = LangChoice::from_req(res.request()).lang_id();
|
||||||
|
let error = LOCALES.lookup(&lang, "error-not-found").unwrap();
|
||||||
|
|
||||||
|
let response = get_error_response(&res, &error);
|
||||||
|
|
||||||
|
Ok(ErrorHandlerResponse::Response(ServiceResponse::new(
|
||||||
|
res.into_parts().0,
|
||||||
|
response.map_into_left_body(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic error handler.
|
||||||
|
fn get_error_response<B>(res: &ServiceResponse<B>, error: &str) -> HttpResponse<BoxBody> {
|
||||||
|
let req = res.request();
|
||||||
|
let lang = LangChoice::from_req(req);
|
||||||
|
|
||||||
|
// Provide a fallback to a simple plain text response in case an error occurs during the
|
||||||
|
// rendering of the error page.
|
||||||
|
|
||||||
|
let hb = req
|
||||||
|
.app_data::<web::Data<Handlebars>>()
|
||||||
|
.expect("correctly set up handlebars in app data");
|
||||||
|
|
||||||
|
let data = json!({
|
||||||
|
"lang": lang,
|
||||||
|
"error": error,
|
||||||
|
"status_code": res.status().as_str()
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = hb.render("error", &data);
|
||||||
|
|
||||||
|
match body {
|
||||||
|
Ok(body) => HttpResponse::build(res.status())
|
||||||
|
.content_type(ContentType::html())
|
||||||
|
.body(body),
|
||||||
|
|
||||||
|
Err(_) => HttpResponse::build(res.status())
|
||||||
|
.content_type(ContentType::plaintext())
|
||||||
|
.body(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
10
templating/fluent/templates/error.html
Normal file
10
templating/fluent/templates/error.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>{{ error }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ status_code }} {{ error }}</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
11
templating/fluent/templates/index.html
Normal file
11
templating/fluent/templates/index.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ fluent "home-title" }}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>{{ fluent "home-title" }}</h1>
|
||||||
|
<p>{{ fluent "home-description" }}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
12
templating/fluent/templates/user.html
Normal file
12
templating/fluent/templates/user.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>{{ fluent "user-title" name=user }}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>{{ fluent "user-welcome" name=user }}</h1>
|
||||||
|
<p>{{ fluent "user-data" data=data }}</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user