Skip to content

Commit 9bf53bf

Browse files
authored
Merge pull request #792 from Manishearth/fluent
Basic Fluent implementation
2 parents ae508d6 + 4c1818b commit 9bf53bf

File tree

23 files changed

+1508
-753
lines changed

23 files changed

+1508
-753
lines changed

Cargo.lock

Lines changed: 1002 additions & 595 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ edition = "2018"
66

77
[dependencies]
88
lazy_static = "1.2.0"
9+
fluent = "0.5"
10+
fluent-bundle = "0.6.0"
11+
fluent-syntax = "0.9.0"
912
rand = "0.6"
13+
regex = "1"
1014
rocket = "0.4.1"
1115
serde = "1.0"
1216
serde_derive = "1.0"
@@ -16,6 +20,7 @@ reqwest = "0.9.5"
1620
toml = "0.4"
1721
serde_json = "1.0"
1822
rust_team_data = { git = "https://github.com/rust-lang/team" }
23+
handlebars = "1.1.0"
1924

2025
[dependencies.rocket_contrib]
2126
version = "*"

src/i18n.rs

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
use handlebars::{
2+
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError,
3+
Renderable,
4+
};
5+
6+
use handlebars::template::{Parameter, TemplateElement};
7+
use rocket::http::RawStr;
8+
use rocket::request::FromParam;
9+
use serde_json::Value as Json;
10+
use std::collections::HashMap;
11+
use std::fs::read_dir;
12+
use std::fs::File;
13+
use std::io;
14+
use std::io::prelude::*;
15+
use std::path::Path;
16+
17+
use fluent_bundle::{FluentBundle, FluentResource, FluentValue};
18+
19+
lazy_static! {
20+
static ref RESOURCES: HashMap<String, Vec<FluentResource>> = build_resources();
21+
static ref BUNDLES: HashMap<String, FluentBundle<'static>> = build_bundles();
22+
}
23+
24+
pub struct I18NHelper {
25+
bundles: &'static HashMap<String, FluentBundle<'static>>,
26+
}
27+
28+
impl I18NHelper {
29+
pub fn new() -> Self {
30+
Self { bundles: &*BUNDLES }
31+
}
32+
pub fn i18n_token(
33+
&self,
34+
lang: &str,
35+
text_id: &str,
36+
args: Option<&HashMap<&str, FluentValue>>,
37+
) -> String {
38+
if let Some(bundle) = self.bundles.get(lang) {
39+
if bundle.has_message(text_id) {
40+
let (value, _errors) = bundle
41+
.format(text_id, args)
42+
.expect("Failed to format a message.");
43+
return value;
44+
} else if lang != "en-US" {
45+
let bundle = self
46+
.bundles
47+
.get("en-US")
48+
.expect("Must have English localization");
49+
let (value, _errors) = bundle
50+
.format(text_id, args)
51+
.expect("Failed to format a message.");
52+
return value;
53+
}
54+
}
55+
format!("Unknown localization {}", text_id)
56+
}
57+
}
58+
59+
#[derive(Default)]
60+
struct StringOutput {
61+
pub s: String,
62+
}
63+
64+
impl Output for StringOutput {
65+
fn write(&mut self, seg: &str) -> Result<(), io::Error> {
66+
self.s.push_str(seg);
67+
Ok(())
68+
}
69+
}
70+
71+
impl HelperDef for I18NHelper {
72+
fn call<'reg: 'rc, 'rc>(
73+
&self,
74+
h: &Helper<'reg, 'rc>,
75+
reg: &'reg Handlebars,
76+
context: &'rc Context,
77+
rcx: &mut RenderContext<'reg>,
78+
out: &mut dyn Output,
79+
) -> HelperResult {
80+
let id = if let Some(id) = h.param(0) {
81+
id
82+
} else {
83+
return Err(RenderError::new(
84+
"{{text}} must have at least one parameter",
85+
));
86+
};
87+
88+
let id = if let Some(id) = id.path() {
89+
id
90+
} else {
91+
return Err(RenderError::new("{{text}} takes an identifier parameter"));
92+
};
93+
94+
let mut args = if h.hash().is_empty() {
95+
None
96+
} else {
97+
let map = h
98+
.hash()
99+
.iter()
100+
.filter_map(|(k, v)| {
101+
let json = v.value();
102+
let val = match *json {
103+
Json::Number(ref n) => FluentValue::Number(n.to_string()),
104+
Json::String(ref s) => FluentValue::String(s.to_string()),
105+
_ => return None,
106+
};
107+
Some((&**k, val))
108+
})
109+
.collect();
110+
Some(map)
111+
};
112+
113+
if let Some(tpl) = h.template() {
114+
if args.is_none() {
115+
args = Some(HashMap::new());
116+
}
117+
let args = args.as_mut().unwrap();
118+
for element in &tpl.elements {
119+
if let TemplateElement::HelperBlock(ref block) = element {
120+
if block.name != "textparam" {
121+
return Err(RenderError::new(format!(
122+
"{{{{text}}}} can only contain {{{{textparam}}}} elements, not {}",
123+
block.name
124+
)));
125+
}
126+
let id = if let Some(el) = block.params.get(0) {
127+
if let Parameter::Name(ref s) = *el {
128+
s
129+
} else {
130+
return Err(RenderError::new(
131+
"{{textparam}} takes an identifier parameter",
132+
));
133+
}
134+
} else {
135+
return Err(RenderError::new("{{textparam}} must have one parameter"));
136+
};
137+
if let Some(ref tpl) = block.template {
138+
let mut s = StringOutput::default();
139+
tpl.render(reg, context, rcx, &mut s)?;
140+
args.insert(&*id, FluentValue::String(s.s));
141+
}
142+
}
143+
}
144+
}
145+
let lang = context
146+
.data()
147+
.get("lang")
148+
.expect("Language not set in context")
149+
.as_str()
150+
.expect("Language must be string");
151+
let response = self.i18n_token(lang, &id, args.as_ref());
152+
out.write(&response).map_err(RenderError::with)
153+
}
154+
}
155+
156+
pub fn read_from_file<P: AsRef<Path>>(filename: P) -> io::Result<FluentResource> {
157+
let mut file = File::open(filename)?;
158+
let mut string = String::new();
159+
160+
file.read_to_string(&mut string)?;
161+
162+
Ok(FluentResource::try_new(string).expect("File did not parse!"))
163+
}
164+
165+
pub fn read_from_dir<P: AsRef<Path>>(dirname: P) -> io::Result<Vec<FluentResource>> {
166+
let mut result = Vec::new();
167+
for dir_entry in read_dir(dirname)? {
168+
let entry = dir_entry?;
169+
let resource = read_from_file(entry.path())?;
170+
result.push(resource);
171+
}
172+
Ok(result)
173+
}
174+
175+
pub fn create_bundle(lang: &str, resources: &'static Vec<FluentResource>) -> FluentBundle<'static> {
176+
let mut bundle = FluentBundle::new(&[lang]);
177+
178+
for res in resources {
179+
bundle
180+
.add_resource(res)
181+
.expect("Failed to add FTL resources to the bundle.");
182+
}
183+
184+
bundle
185+
}
186+
187+
fn build_resources() -> HashMap<String, Vec<FluentResource>> {
188+
let mut all_resources = HashMap::new();
189+
let entries = read_dir("./templates/fluent-resource").unwrap();
190+
for entry in entries {
191+
let entry = entry.unwrap();
192+
if let Ok(lang) = entry.file_name().into_string() {
193+
let resources = read_from_dir(entry.path()).unwrap();
194+
all_resources.insert(lang, resources);
195+
}
196+
}
197+
all_resources
198+
}
199+
200+
fn build_bundles() -> HashMap<String, FluentBundle<'static>> {
201+
let mut bundles = HashMap::new();
202+
for (ref k, ref v) in &*RESOURCES {
203+
bundles.insert(k.to_string(), create_bundle(&k, &v));
204+
}
205+
bundles
206+
}
207+
208+
pub struct SupportedLocale(pub String);
209+
210+
impl<'r> FromParam<'r> for SupportedLocale {
211+
type Error = ();
212+
213+
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
214+
let param = param.percent_decode().map_err(|_| ())?;
215+
if BUNDLES.get(param.as_ref()).is_some() {
216+
Ok(SupportedLocale(param.into()))
217+
} else {
218+
Err(())
219+
}
220+
}
221+
}

0 commit comments

Comments
 (0)