json_gettext/lib.rs
1/*!
2# JSON Get Text
3
4This library reads texts from JSON, usually for internationalization (i18n).
5
6Each **key** is a locale (for example `en_US` or `zh_TW`), and each locale maps **text names** to **values**. A value can be any JSON value, not only a string.
7
8## Example
9
10```rust,ignore
11use json_gettext::{get_text, static_json_gettext_build};
12
13let ctx = static_json_gettext_build!(
14 "en_US";
15 "en_US" => "langs/en_US.json",
16 "zh_TW" => "langs/zh_TW.json",
17)
18.unwrap();
19
20assert_eq!("Hello, world!", get_text!(ctx, "hello").unwrap());
21assert_eq!("哈囉,世界!", get_text!(ctx, "zh_TW", "hello").unwrap());
22```
23
24The argument before `;` is the **default key**. Every other key may only contain text names that also exist in the default key; this is checked when the context is built. To drop such extra texts instead of failing, build with [`JSONGetTextBuilder`] and call `allow_extra_texts(true)`.
25
26## Looking Up Texts
27
28The lookup methods differ in how they fall back to the default key:
29
30- `get_text(text)` reads a text from the default key.
31- `get_text_with_key(key, text)` reads a text from `key`, and falls back to the default key when that single text is missing.
32- `get_multiple_text_with_key(key, &[text, ...])` applies the same per-text fallback to many texts at once.
33- `get(key)` returns the whole map of texts that `key` owns. Missing texts are **not** filled in from the default key; only when `key` itself does not exist is the default key's map returned instead.
34
35The `get_text!` macro is a shortcut with three forms: `get_text!(ctx, text)`, `get_text!(ctx, key, text)`, and `get_text!(ctx, key, text1, text2, ...)`.
36
37## Features
38
39### Key Backends (mutually exclusive)
40
41By default a key is an arbitrary `String`, matched by exact string comparison. If your keys are locale tags, you can enable one of the features below to store keys as fixed-size `Copy` values from the [`unic-langid`](https://crates.io/crates/unic-langid) crate. This makes key comparison faster and normalizes the tag format, so `en-US` and `en_US` become the same key.
42
43| Feature | `Key` type | Example tag | Use when |
44|---------|------------|-------------|----------|
45| *(none)* | `Key(String)` | `en_US` | keys are arbitrary strings |
46| `locale` | `Key(Language, Option<Region>)` | `en_US` | you key by language **and** region |
47| `language` | `Key(Language)` | `en` | you key by language only |
48| `region` | `Key(Region)` | `US` | you key by region only |
49
50These three features each redefine `Key`, so **at most one** may be enabled. Enabling more than one, or enabling the internal `langid` feature on its own, fails to compile with a message telling you what to do.
51
52When a langid backend is enabled, use the `key!` macro to build a `Key` from a string literal at compile time:
53
54```rust,ignore
55use json_gettext::{key, Key};
56
57let key: Key = key!("en_US");
58```
59
60### `regex`
61
62Enables `get_filtered_text` and `get_filtered_text_with_key`, which return every text whose **name** matches a [`Regex`](https://crates.io/crates/regex). These methods do not fall back to the default key.
63
64### `rocket`
65
66Integrates with the [Rocket](https://rocket.rs) web framework. See the section below.
67
68## Rocket Support
69
70Enable the `rocket` feature, then build the context with `static_json_gettext_build_for_rocket` instead of `static_json_gettext_build`. It registers a `JSONGetTextManager`, which derefs to `JSONGetText`.
71
72```toml
73[dependencies.json-gettext]
74version = "*"
75features = ["rocket"]
76```
77
78In debug builds the manager watches the JSON files and reloads them when they change, so translations can be edited without recompiling. In release builds the JSON is embedded into the binary and is never reloaded.
79
80```rust,ignore
81#[macro_use]
82extern crate rocket;
83
84use json_gettext::{get_text, static_json_gettext_build_for_rocket, JSONGetTextManager};
85use rocket::response::Redirect;
86use rocket::State;
87
88#[get("/")]
89fn index(ctx: &State<JSONGetTextManager>) -> Redirect {
90 Redirect::temporary(uri!(hello(lang = ctx.get_default_key())))
91}
92
93#[get("/<lang>")]
94fn hello(ctx: &State<JSONGetTextManager>, lang: String) -> String {
95 format!("Ron: {}", get_text!(ctx, lang, "hello").unwrap().as_str().unwrap())
96}
97
98#[launch]
99fn rocket() -> _ {
100 rocket::build()
101 .attach(static_json_gettext_build_for_rocket!(
102 "en_US";
103 "en_US" => "langs/en_US.json",
104 "zh_TW" => "langs/zh_TW.json"
105 ))
106 .mount("/", routes![index, hello])
107}
108```
109
110### Rocket with a langid backend
111
112With a langid backend, pass `Key` values (built with `key!`) instead of strings.
113
114```rust,ignore
115#[macro_use]
116extern crate rocket;
117
118#[macro_use]
119extern crate rocket_accept_language;
120
121use json_gettext::{get_text, key, static_json_gettext_build_for_rocket, JSONGetTextManager, Key};
122use rocket::State;
123use rocket_accept_language::unic_langid::subtags::Language;
124use rocket_accept_language::AcceptLanguage;
125
126const LANGUAGE_EN: Language = language!("en");
127
128#[get("/")]
129fn index(ctx: &State<JSONGetTextManager>, accept_language: &AcceptLanguage) -> String {
130 let (language, region) =
131 accept_language.get_first_language_region().unwrap_or((LANGUAGE_EN, None));
132
133 format!("Ron: {}", get_text!(ctx, Key(language, region), "hello").unwrap().as_str().unwrap())
134}
135
136#[launch]
137fn rocket() -> _ {
138 rocket::build()
139 .attach(static_json_gettext_build_for_rocket!(
140 key!("en");
141 key!("en") => "langs/en_US.json",
142 key!("zh_TW") => "langs/zh_TW.json",
143 ))
144 .mount("/", routes![index])
145}
146```
147*/
148
149#![cfg_attr(docsrs, feature(doc_cfg))]
150
151// The `locale`, `language`, and `region` features each redefine the `Key` type, so at most one may be enabled.
152#[cfg(any(
153 all(feature = "locale", feature = "language"),
154 all(feature = "locale", feature = "region"),
155 all(feature = "language", feature = "region"),
156))]
157compile_error!(
158 "The `locale`, `language`, and `region` features are mutually exclusive; enable at most one."
159);
160
161// `langid` is only an internal umbrella feature; enabling it alone leaves the `Key` type undefined.
162#[cfg(all(
163 feature = "langid",
164 not(any(feature = "locale", feature = "language", feature = "region"))
165))]
166compile_error!(
167 "Enable one of the `locale`, `language`, or `region` features instead of enabling `langid` \
168 directly."
169);
170
171pub extern crate serde_json;
172
173#[cfg(feature = "regex")]
174pub extern crate regex;
175
176#[cfg(feature = "langid")]
177pub extern crate unic_langid;
178
179#[doc(hidden)]
180pub extern crate manifest_dir_macros;
181
182mod common;
183mod json_get_text_build_errors;
184mod macros;
185mod text_lookup;
186mod value;
187
188#[cfg(all(debug_assertions, feature = "rocket"))]
189mod mutate;
190
191#[cfg(feature = "langid")]
192mod key_copy;
193
194#[cfg(not(feature = "langid"))]
195mod key_string;
196
197pub use common::*;
198pub use json_get_text_build_errors::*;
199#[cfg(feature = "langid")]
200pub use key_copy::*;
201#[cfg(not(feature = "langid"))]
202pub use key_string::*;
203#[cfg(all(debug_assertions, feature = "rocket"))]
204use mutate::DebuggableMutate;
205#[cfg(feature = "locale")]
206pub use unic_langid::LanguageIdentifierError;
207#[cfg(any(feature = "language", feature = "region"))]
208pub use unic_langid::parser::ParserError;
209pub use value::*;