String packs: rich-text entries, language conditions and word rewriting; theme doctor report; translate-mode words
- Rich sentences (a link or button inside text) can be replaced by data: the inventory gives a rich entry `slots`, a pack value places the site's own elements with {e0}, {e1}, ... (each once, the last may be left out; no markup, plural or other parameters). The engine returns a pieces + element order template; the page inventory carries `rich` slot counts.
- Packs may carry `when: { languages }` (applies only while the site shows one of them; `en` matches `en-GB`) and `rewrite: { words }` (whole-word rewording of the site's own text, messages keep their parameters' values; no sentences of the site in the pack). The hook tells the engine the site's language; a rewording pack makes the delivery carry the whole inventory.
- Apps can ship built-in packs (BuiltInPacks) delivered before installed ones; syncStringPacks hands the tabs a delivery at start only when it changed.
- Translate mode words are a function the app supplies (asked at each draw) and gain a `plural` word; host validates `when` and `rewrite` envelopes.
- Theme doctor: every finding says where (token path or CSS rule), what and how to fix, as catalog keys wb.doctor.*; the Theme Builder shows the full report; the import hint is translated. Doctor words are in the 17 supported catalogs.
- waybetter strings check describes conditions and rewrites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1539cb68e1
commit
6b37744226
33 changed files with 1353 additions and 85 deletions
|
|
@ -101,7 +101,28 @@ pub fn validate_strings(bytes: &[u8]) -> Result<StringsInfo, String> {
|
|||
return Err(format!("VALIDATION: string {} is not plain text", id.chars().take(64).collect::<String>()));
|
||||
}
|
||||
}
|
||||
const KNOWN: &[&str] = &["$schema", "format", "site", "catalog", "language", "base", "name", "authors", "version", "description", "strings"];
|
||||
// Conditions and rewording rules are data too: shapes are checked here, words and values by the page runtime's own validator.
|
||||
if let Some(when) = object.get("when") {
|
||||
let ok = when.as_object().is_some_and(|w| {
|
||||
w.keys().all(|key| key == "languages")
|
||||
&& w.get("languages").and_then(serde_json::Value::as_array).is_some_and(|list| !list.is_empty() && list.len() <= 16 && list.iter().all(|tag| tag.as_str().is_some_and(valid_language)))
|
||||
});
|
||||
if !ok {
|
||||
return Err("VALIDATION: invalid \"when\"".into());
|
||||
}
|
||||
}
|
||||
if let Some(rewrite) = object.get("rewrite") {
|
||||
let ok = rewrite.as_object().is_some_and(|r| {
|
||||
r.keys().all(|key| key == "words")
|
||||
&& r.get("words").and_then(serde_json::Value::as_object).is_some_and(|words| {
|
||||
!words.is_empty() && words.len() <= 64 && words.iter().all(|(from, to)| from.chars().count() <= 40 && to.as_str().is_some_and(|to| plain(to, 40) && !to.contains(['<', '>', '{', '}'])) && !from.contains(['<', '>', '{', '}']) && !from.is_empty())
|
||||
})
|
||||
});
|
||||
if !ok {
|
||||
return Err("VALIDATION: invalid \"rewrite\"".into());
|
||||
}
|
||||
}
|
||||
const KNOWN: &[&str] = &["$schema", "format", "site", "catalog", "language", "base", "when", "rewrite", "name", "authors", "version", "description", "strings"];
|
||||
if let Some(key) = object.keys().find(|key| !KNOWN.contains(&key.as_str())) {
|
||||
return Err(format!("VALIDATION: unknown field \"{}\"", key.chars().take(40).collect::<String>()));
|
||||
}
|
||||
|
|
@ -361,6 +382,26 @@ mod tests {
|
|||
format!(r#"{{"format":1,"site":"x","catalog":"web","language":"eo","name":"Esperanto","authors":["A"],"version":"1.0.0","strings":{strings}}}"#).into_bytes()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditions_and_rewording_rules_are_plain_data() {
|
||||
let with = |extra: &str| format!(r#"{{"format":1,"site":"x","catalog":"web","language":"en-x-twitter","name":"W","authors":["A"],{extra},"strings":{{}}}}"#).into_bytes();
|
||||
let info = validate_strings(&with(r#""when":{"languages":["en","en-GB"]},"rewrite":{"words":{"Post":"Tweet","posts":"tweets"}}"#)).unwrap();
|
||||
assert_eq!((info.language.as_str(), info.strings), ("en-x-twitter", 0));
|
||||
for bad in [
|
||||
r#""when":{"languages":[]}"#,
|
||||
r#""when":{"languages":["not a tag"]}"#,
|
||||
r#""when":{"code":"x"}"#,
|
||||
r#""when":"en""#,
|
||||
r#""rewrite":{"words":{}}"#,
|
||||
r#""rewrite":{"words":{"Post":"<b>Tweet</b>"}}"#,
|
||||
r#""rewrite":{"words":{"Post":"{x}"}}"#,
|
||||
r#""rewrite":{"words":{"Post":1}}"#,
|
||||
r#""rewrite":{"words":{"Post":"Tweet"},"code":"x"}"#,
|
||||
] {
|
||||
assert!(validate_strings(&with(bad)).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packs_are_plain_data_with_the_envelope() {
|
||||
let info = validate_strings(&pack(r#"{"a0000001":"Hejmo","b0000002":"{count, plural, one {# afiŝo} other {# afiŝoj}}"}"#)).unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue