mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-04 01:10:37 -06:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9e2b92e41 | ||
|
|
d97e2f3a29 | ||
|
|
68512842aa | ||
|
|
fa37c9adbe | ||
|
|
26c662961d | ||
|
|
9819939d98 | ||
|
|
efb5071526 |
+3
-1
File diff suppressed because one or more lines are too long
@@ -529,5 +529,159 @@ mod tests {
|
||||
let _ = packs::without_region("\u{0}-\u{202e}");
|
||||
}
|
||||
|
||||
/// A font store that has never fetched the list (a fresh install), or whose cached list is
|
||||
/// for an older pin: `index` has nothing until a fetch.
|
||||
struct Uncached(FixedBackend);
|
||||
|
||||
impl Backend for Uncached {
|
||||
fn index(&self) -> Result<Vec<Pack>, String> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn fetch_index(&self, c: &packs::Cancel) -> Result<Vec<Pack>, String> {
|
||||
self.0.fetch_index(c)
|
||||
}
|
||||
fn network_on(&self) -> bool {
|
||||
self.0.network_on()
|
||||
}
|
||||
fn enable_network(&self) -> Result<(), String> {
|
||||
self.0.enable_network()
|
||||
}
|
||||
fn install(&self, pack: &Pack, cancel: &packs::Cancel, progress: &mut dyn FnMut(u64, u64)) -> Result<(), String> {
|
||||
self.0.install(pack, cancel, progress)
|
||||
}
|
||||
fn remove(&self, locale: &str) -> Result<(), String> {
|
||||
self.0.remove(locale)
|
||||
}
|
||||
fn packs_dir(&self) -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
fn installed(&self) -> Vec<packs::Installed> {
|
||||
self.0.installed()
|
||||
}
|
||||
}
|
||||
|
||||
/// The sample packs and Greenlandic (`kl`), a language no system has a dictionary for.
|
||||
fn with_greenlandic() -> FixedBackend {
|
||||
let mut b = FixedBackend::sample();
|
||||
let kl = Pack { locale: "kl".into(), name: "Kalaallisut".into(), ..b.packs[0].clone() };
|
||||
b.packs.push(kl);
|
||||
b
|
||||
}
|
||||
|
||||
fn with_uncached<T>(b: FixedBackend, f: impl FnOnce(&mut Session, &Uncached) -> T) -> T {
|
||||
let _g = LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _reset = Reset;
|
||||
let b = Arc::new(Uncached(b));
|
||||
packs::set_backend(Some(b.clone() as Arc<dyn Backend>));
|
||||
let mut s = Session::new(wordcraft_doc::Document::new());
|
||||
f(&mut s, &b)
|
||||
}
|
||||
|
||||
/// Martin's test of a fresh install: Greenlandic text, no list fetched yet. The bar still
|
||||
/// offers the language (never English, nor one with a dictionary); getting it fetches the
|
||||
/// list and installs its pack. A language the list then has no pack for (Klingon) stops
|
||||
/// being offered.
|
||||
#[test]
|
||||
fn with_no_list_yet_the_bar_still_offers_the_language() {
|
||||
with_uncached(with_greenlandic(), |s, b| {
|
||||
s.run("document.setText", &json!({"text": "Hello there\nAluu, qanoq ippit?\ntlhIngan Hol"})).unwrap();
|
||||
for (para, lang) in [(1, "kl-GL"), (2, "tlh")] {
|
||||
s.run("caret.set", &json!({"pos": {"block": para, "off": 0}})).unwrap();
|
||||
s.run("select.paragraph", &json!({})).unwrap();
|
||||
s.run("review.language", &json!({"lang": lang})).unwrap();
|
||||
}
|
||||
assert!(packs::available().is_empty() && packs::missing(s).is_empty(), "no list: no pack to offer");
|
||||
for tag in ["kl-GL", "tlh"] {
|
||||
assert!(wordcraft_proof::missing_dictionary(Some(tag)).is_some(), "{tag} has no dictionary here");
|
||||
}
|
||||
assert_eq!(packs::unlisted(s), ["kl-GL", "tlh"], "English is never offered");
|
||||
// Downloading off: getting it fails up front, and asks for nothing.
|
||||
assert!(packs::start_get("kl-GL").is_err());
|
||||
assert!(!packs::fetched());
|
||||
b.0.network.store(true, Ordering::Relaxed);
|
||||
assert_eq!(packs::start_get("kl-GL"), Ok(false), "fetched and installed here (no background thread)");
|
||||
assert!(packs::is_installed("kl"));
|
||||
assert!(packs::fetched());
|
||||
assert!(packs::unlisted(s).is_empty(), "the list is known now: Klingon, which it lacks, isn't offered");
|
||||
assert!(packs::missing(s).is_empty());
|
||||
});
|
||||
}
|
||||
|
||||
/// Review: a get refused because downloading is off isn't remembered, so a later fetch (the
|
||||
/// person's Refresh in Options) doesn't install it unasked.
|
||||
#[test]
|
||||
fn a_refused_get_is_not_installed_by_a_later_fetch() {
|
||||
with_uncached(with_greenlandic(), |_, b| {
|
||||
assert!(packs::start_get("kl-GL").is_err(), "downloading off");
|
||||
b.0.network.store(true, Ordering::Relaxed);
|
||||
assert_eq!(packs::start_refresh(), Ok(false));
|
||||
assert!(packs::find("kl").is_some(), "the list has it");
|
||||
assert!(!packs::is_installed("kl"), "but nobody asked for it after the refusal");
|
||||
});
|
||||
}
|
||||
|
||||
/// The list was fetched, but lists nothing for the language: the bar stops offering it.
|
||||
#[test]
|
||||
fn a_language_the_fetched_list_lacks_is_no_longer_offered() {
|
||||
with_uncached(FixedBackend::sample(), |s, b| {
|
||||
s.run("document.setText", &json!({"text": "tlhIngan Hol"})).unwrap();
|
||||
s.run("select.all", &json!({})).unwrap();
|
||||
s.run("review.language", &json!({"lang": "tlh"})).unwrap();
|
||||
assert_eq!(packs::unlisted(s), ["tlh"]);
|
||||
b.0.network.store(true, Ordering::Relaxed);
|
||||
assert_eq!(packs::start_get("tlh"), Ok(false));
|
||||
assert!(packs::installed().is_empty(), "nothing to install");
|
||||
assert!(packs::unlisted(s).is_empty() && packs::missing(s).is_empty());
|
||||
});
|
||||
}
|
||||
|
||||
/// With downloading already on and no list (or one for an older pin), the window fetches it
|
||||
/// once in the background, asking nothing; never with downloading off, never twice.
|
||||
#[test]
|
||||
fn a_missing_list_is_fetched_once_when_downloading_is_on() {
|
||||
let _g = LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _reset = Reset;
|
||||
let b = Arc::new(Uncached(with_greenlandic()));
|
||||
packs::set_backend(Some(b.clone() as Arc<dyn Backend>));
|
||||
assert!(!packs::refresh_if_stale(), "no background thread (the CLI, MCP): never");
|
||||
packs::set_background(Some(Arc::new(|| {})));
|
||||
assert!(!packs::refresh_if_stale(), "downloading off: no request");
|
||||
b.0.network.store(true, Ordering::Relaxed);
|
||||
assert!(!packs::refresh_if_stale(), "asked once a session: turned on later, the person's own click fetches");
|
||||
// A new session (the backend set again) with downloading on.
|
||||
packs::set_backend(Some(b.clone() as Arc<dyn Backend>));
|
||||
let mut s = Session::new(wordcraft_doc::Document::new());
|
||||
s.run("document.setText", &json!({"text": "Aluu"})).unwrap();
|
||||
s.run("select.all", &json!({})).unwrap();
|
||||
s.run("review.language", &json!({"lang": "kl"})).unwrap();
|
||||
let before = packs::finished();
|
||||
assert!(packs::refresh_if_stale());
|
||||
wait_for_finish(before);
|
||||
assert!(packs::fetched());
|
||||
assert_eq!(packs::missing(&s).into_iter().map(|p| p.locale).collect::<Vec<_>>(), ["kl"], "the bar offers the pack");
|
||||
assert!(!packs::refresh_if_stale(), "once a session");
|
||||
assert!(packs::installed().is_empty(), "nothing installed without a click");
|
||||
}
|
||||
|
||||
/// Martin's test: choosing a language with nothing selected (the caret only) sets the typing
|
||||
/// language; the bar offers its pack before anything is typed.
|
||||
#[test]
|
||||
fn the_caret_typing_language_asks_for_its_pack() {
|
||||
with_backend(FixedBackend::sample(), |s, _| {
|
||||
s.run("document.setText", &json!({"text": "Hello there"})).unwrap();
|
||||
s.run("caret.set", &json!({"pos": {"block": 0, "off": 11}})).unwrap();
|
||||
assert!(packs::missing(s).is_empty());
|
||||
s.run("review.language", &json!({"lang": "nl-NL"})).unwrap();
|
||||
assert!(s.sel.is_collapsed());
|
||||
assert!(!packs::document_languages(s).iter().any(|l| l == "nl-NL"), "no Dutch text yet");
|
||||
assert_eq!(packs::caret_language(s).as_deref(), Some("nl-NL"));
|
||||
assert_eq!(packs::missing(s).into_iter().map(|p| p.locale).collect::<Vec<_>>(), ["nl"]);
|
||||
// Do not check spelling or grammar at the caret: nothing asked.
|
||||
s.run("review.language", &json!({"lang": "nl-NL", "noProof": true})).unwrap();
|
||||
assert_eq!(packs::caret_language(s), None);
|
||||
assert!(packs::missing(s).is_empty());
|
||||
});
|
||||
}
|
||||
|
||||
use crate::packs::Pack;
|
||||
}
|
||||
|
||||
+140
-3
@@ -143,6 +143,9 @@ pub fn set_backend(backend: Option<Arc<dyn Backend>>) {
|
||||
let dir = backend.as_ref().and_then(|b| b.packs_dir());
|
||||
*BACKEND.write().unwrap_or_else(PoisonError::into_inner) = backend;
|
||||
set_index(None);
|
||||
FETCHED.store(false, Ordering::Relaxed);
|
||||
AUTO_REFRESHED.store(false, Ordering::Relaxed);
|
||||
WANTED.lock().unwrap_or_else(PoisonError::into_inner).clear();
|
||||
jobs().errors.clear();
|
||||
wordcraft_proof::packs::set_packs_dir(dir);
|
||||
}
|
||||
@@ -219,6 +222,8 @@ pub fn refresh(cancel: &Cancel) -> Result<Vec<Pack>, String> {
|
||||
}
|
||||
let index = clean_index(b.fetch_index(cancel)?);
|
||||
set_index(Some(index.clone()));
|
||||
FETCHED.store(true, Ordering::Relaxed);
|
||||
install_wanted(&index);
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
@@ -706,8 +711,33 @@ fn has_pack_for_tag(tag: &str) -> bool {
|
||||
PACK_LOOKUP.lock().unwrap_or_else(PoisonError::into_inner).get(generation, tag, || pack_for_tag(tag, &available()).is_some())
|
||||
}
|
||||
|
||||
/// The packs the document's text asks for that are available and not installed: what the
|
||||
/// missing-pack bar offers. Never a language without a pack, nor one proofing can't use.
|
||||
/// The language new text at the caret gets (its typing language, resolved through the styles
|
||||
/// and the document default), unless it is marked Do not check spelling or grammar: choosing a
|
||||
/// language with nothing selected asks for its pack before anything is typed in it.
|
||||
pub fn caret_language(s: &Session) -> Option<String> {
|
||||
let style = s.doc.para_at(&s.sel.focus).and_then(|p| p.props.style.as_deref());
|
||||
let rc = s.doc.styles.resolve_char(style, &s.typing_props());
|
||||
if rc.no_proof {
|
||||
return None;
|
||||
}
|
||||
Some(rc.lang.unwrap_or_else(|| "en-US".into()))
|
||||
}
|
||||
|
||||
/// The languages the missing-pack bar looks at: the body text's ([`document_languages`]), then
|
||||
/// the caret's typing language ([`caret_language`]).
|
||||
pub fn wanted_languages(s: &Session) -> Vec<String> {
|
||||
let mut out = document_languages(s);
|
||||
if let Some(tag) = caret_language(s)
|
||||
&& !out.contains(&tag)
|
||||
{
|
||||
out.push(tag);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The packs the document's text (or the caret's typing language) asks for that are available
|
||||
/// and not installed: what the missing-pack bar offers. Never a language without a pack, nor one
|
||||
/// proofing can't use.
|
||||
pub fn missing(s: &Session) -> Vec<Pack> {
|
||||
let packs = available();
|
||||
if packs.is_empty() {
|
||||
@@ -715,7 +745,7 @@ pub fn missing(s: &Session) -> Vec<Pack> {
|
||||
}
|
||||
let installed = installed();
|
||||
let mut out: Vec<Pack> = Vec::new();
|
||||
for tag in document_languages(s) {
|
||||
for tag in wanted_languages(s) {
|
||||
if proofable(&tag)
|
||||
&& let Some(p) = pack_for_tag(&tag, &packs)
|
||||
&& !installed.iter().any(|i| i.locale == p.locale)
|
||||
@@ -727,6 +757,113 @@ pub fn missing(s: &Session) -> Vec<Pack> {
|
||||
out
|
||||
}
|
||||
|
||||
/// The index was fetched this session (since the backend was set): what it lists is then the
|
||||
/// whole answer, even when it lists nothing.
|
||||
static FETCHED: AtomicBool = AtomicBool::new(false);
|
||||
/// The automatic fetch of a missing or stale list ([`refresh_if_stale`]) was tried.
|
||||
static AUTO_REFRESHED: AtomicBool = AtomicBool::new(false);
|
||||
/// Languages (document tags) whose pack the person asked for before the list was known
|
||||
/// ([`start_get`]): installed once a fetch lists one.
|
||||
static WANTED: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
|
||||
/// Whether the index was fetched since the backend was set.
|
||||
pub fn fetched() -> bool {
|
||||
FETCHED.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The languages the document (or the caret) asks for while there is no list of packs yet (a
|
||||
/// fresh install, or a list cached for an older pin): proofable, not English, with no dictionary
|
||||
/// and no pack installed, one per proofing language, as the document tags them (`tr-TR`). The
|
||||
/// bar offers to get them: the list is fetched first, and the language then installed if it has
|
||||
/// a pack. Nothing once a list is known or was fetched this session ([`missing`] then answers).
|
||||
pub fn unlisted(s: &Session) -> Vec<String> {
|
||||
if backend().is_none() || fetched() || available_len() > 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let installed = installed();
|
||||
let mut seen: Vec<wordcraft_proof::ProofLang> = Vec::new();
|
||||
let mut out = Vec::new();
|
||||
for tag in wanted_languages(s) {
|
||||
let base = tag.split(['-', '_']).next().unwrap_or("").to_ascii_lowercase();
|
||||
let lang = wordcraft_proof::ProofLang::from_tag(Some(&tag));
|
||||
if base.is_empty()
|
||||
|| base == "en"
|
||||
|| !proofable(&tag)
|
||||
|| wordcraft_proof::missing_dictionary(Some(&tag)).is_none()
|
||||
|| seen.contains(&lang)
|
||||
|| installed.iter().any(|i| i.locale == base || tag.eq_ignore_ascii_case(&i.locale))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
seen.push(lang);
|
||||
out.push(tag);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The person asked for `tag`'s pack before the list was known (the bar's Install, after their
|
||||
/// consent turned downloading on): fetch the list ([`start_refresh`]), then install the pack it
|
||||
/// lists for `tag`, if any; with none, the bar stops offering it. Downloading must be on.
|
||||
/// Returns whether it is still running.
|
||||
pub fn start_get(tag: &str) -> Result<bool, String> {
|
||||
let tag = tag.trim();
|
||||
if tag.is_empty() || tag.len() > MAX_LOCALE_LEN {
|
||||
return Err(format!("`{tag}` isn't a language tag"));
|
||||
}
|
||||
if let Some(p) = pack_for_tag(tag, &available()) {
|
||||
return start_install(&p.locale);
|
||||
}
|
||||
let b = backend().ok_or_else(no_store)?;
|
||||
if !b.network_on() {
|
||||
// Never queued: a later fetch mustn't install what was refused.
|
||||
return Err(network_off());
|
||||
}
|
||||
let queued = {
|
||||
let mut w = WANTED.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let new = !w.iter().any(|t| t == tag) && w.len() < MAX_DOC_LANGUAGES;
|
||||
if new {
|
||||
w.push(tag.to_string());
|
||||
}
|
||||
new
|
||||
};
|
||||
let r = start_refresh();
|
||||
if r.is_err() && queued {
|
||||
WANTED.lock().unwrap_or_else(PoisonError::into_inner).retain(|t| t != tag);
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// After a fetch: install the packs asked for before the list was known ([`start_get`]).
|
||||
fn install_wanted(index: &[Pack]) {
|
||||
let wanted = std::mem::take(&mut *WANTED.lock().unwrap_or_else(PoisonError::into_inner));
|
||||
for tag in wanted {
|
||||
let Some(p) = pack_for_tag(&tag, index) else { continue };
|
||||
if is_installed(&p.locale) {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = start_install(&p.locale) {
|
||||
set_error(&p.locale, Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Once a session (the first time it is asked once requests run on a thread, [`runs_in_background`]:
|
||||
/// the window), with downloading already on and no list (none cached, or one cached for an older
|
||||
/// pin): fetch it in the background, so the bar can offer packs. The person's consent is the
|
||||
/// downloading setting itself; nothing is asked. Its failure is recorded for Options (""). The
|
||||
/// window asks every frame: after the first time this reads nothing (the downloading setting is
|
||||
/// a file). Returns whether a fetch was started.
|
||||
pub fn refresh_if_stale() -> bool {
|
||||
let Some(b) = backend() else { return false };
|
||||
if !runs_in_background() || AUTO_REFRESHED.swap(true, Ordering::Relaxed) {
|
||||
return false;
|
||||
}
|
||||
if !b.network_on() || fetched() || fetching() || available_len() > 0 {
|
||||
return false;
|
||||
}
|
||||
start_refresh().is_ok()
|
||||
}
|
||||
|
||||
/// A backend with a fixed list and installs that only record themselves: tests and `ui_shot`.
|
||||
#[derive(Default)]
|
||||
pub struct FixedBackend {
|
||||
|
||||
@@ -16,6 +16,8 @@ use crate::theme::{Tokens, regular, semibold};
|
||||
|
||||
mod balloons;
|
||||
mod context_menu;
|
||||
#[cfg(test)]
|
||||
pub(crate) use context_menu::issue_heading;
|
||||
mod cursor;
|
||||
mod geometry;
|
||||
mod input;
|
||||
|
||||
@@ -48,6 +48,17 @@ fn equation_menu(app: &mut WordApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The heading of a proofing issue's context menu, in the interface language like the rest of
|
||||
/// the menu: a spelling issue's own message is in the text's language (craft-proof's, "Faute
|
||||
/// d'orthographe possible" for French text), so it says Possible spelling mistake instead.
|
||||
/// English grammar rules' messages are catalog keys; other languages' rules explain in their own.
|
||||
pub(crate) fn issue_heading(issue: &serde_json::Value) -> String {
|
||||
if issue.get("kind").and_then(|k| k.as_str()) == Some("spelling") {
|
||||
return tl!("Possible spelling mistake").to_string();
|
||||
}
|
||||
tl!(issue.get("message").and_then(|m| m.as_str()).unwrap_or("")).to_string()
|
||||
}
|
||||
|
||||
pub(super) fn context_menu(app: &mut WordApp, ui: &mut Ui) {
|
||||
if app.session.math.is_some() {
|
||||
return equation_menu(app, ui);
|
||||
@@ -67,8 +78,7 @@ pub(super) fn context_menu(app: &mut WordApp, ui: &mut Ui) {
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect())
|
||||
.unwrap_or_default();
|
||||
// English rules' messages are catalog keys; Polish rules already explain in Polish.
|
||||
ui.label(egui::RichText::new(tl!(issue.get("message").and_then(|m| m.as_str()).unwrap_or(""))).small().weak());
|
||||
ui.label(egui::RichText::new(issue_heading(&issue)).small().weak());
|
||||
if sugg.is_empty() {
|
||||
ui.label(egui::RichText::new(tl!("(no suggestions)")).italics());
|
||||
}
|
||||
|
||||
@@ -75,21 +75,39 @@ pub(crate) struct Runtime {
|
||||
/// Failures the window had itself (turning downloading on, starting, removing), by locale
|
||||
/// ("" for the list), shown until the next try. The engine keeps the installs' own.
|
||||
errors: HashMap<String, String>,
|
||||
/// (document, locale) pairs whose bar was closed.
|
||||
/// (document, pack locale) pairs whose bar was closed.
|
||||
dismissed: HashSet<(u64, String)>,
|
||||
/// The missing packs of the shown document, what they were worked out from, and when.
|
||||
missing: Option<(MissingKey, f64, Vec<Pack>)>,
|
||||
/// (document, language tag) pairs whose bar was closed before the list was known: the pack
|
||||
/// the list then has for the tag stays closed.
|
||||
dismissed_tags: HashSet<(u64, String)>,
|
||||
/// What the bar offers for the shown document, what it was worked out from, and when.
|
||||
missing: Option<(MissingKey, f64, Offers)>,
|
||||
/// [`packs::finished`] when last looked at.
|
||||
finished: u64,
|
||||
/// The person clicked the bar's Install before the list was known: the list's failure then
|
||||
/// shows in the bar (an automatic fetch's stays in Options).
|
||||
asked: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
/// What the bar can offer.
|
||||
#[derive(Clone, Default)]
|
||||
struct Offers {
|
||||
/// Available packs the document asks for ([`packs::missing`]).
|
||||
packs: Vec<Pack>,
|
||||
/// Languages it asks for while there is no list yet ([`packs::unlisted`]).
|
||||
unlisted: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
struct MissingKey {
|
||||
document: u64,
|
||||
rev: u64,
|
||||
generation: u64,
|
||||
/// [`packs::index_generation`]: the list changed.
|
||||
packs: u64,
|
||||
/// The caret's typing language ([`packs::caret_language`]): choosing a language with nothing
|
||||
/// selected changes no text.
|
||||
caret: Option<String>,
|
||||
}
|
||||
|
||||
/// Bytes as people read them: `1.9 MB`, `640 KB`.
|
||||
@@ -158,6 +176,8 @@ pub(crate) fn tick(app: &mut WordApp) {
|
||||
{
|
||||
packs::set_background(Some(Arc::new(move || ctx.request_repaint())));
|
||||
}
|
||||
// Downloading already on and no list (none cached, or one for an older pin): fetch it once.
|
||||
packs::refresh_if_stale();
|
||||
let finished = packs::finished();
|
||||
if finished != app.packs_rt.finished {
|
||||
app.packs_rt.finished = finished;
|
||||
@@ -244,25 +264,45 @@ fn remove(app: &mut WordApp, locale: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The packs the shown document asks for. Worked out again when the document, the packs or the
|
||||
/// list changed; while it is being typed in, at most once a second.
|
||||
fn missing(app: &mut WordApp, now: f64) -> Vec<Pack> {
|
||||
/// What the bar offers for the shown document. Worked out again when the document, the caret's
|
||||
/// language, the packs or the list changed; while it is being typed in, at most once a second.
|
||||
fn missing(app: &mut WordApp, now: f64) -> Offers {
|
||||
let key = MissingKey {
|
||||
document: app.session.document_id(),
|
||||
rev: app.session.rev(),
|
||||
generation: wordcraft_engine::proof::dictionary_generation(),
|
||||
packs: packs::index_generation(),
|
||||
caret: packs::caret_language(&app.session),
|
||||
};
|
||||
if let Some((k, at, m)) = &app.packs_rt.missing
|
||||
&& (*k == key || (MissingKey { rev: key.rev, ..*k } == key && now - at < MISSING_REFRESH_SECS))
|
||||
&& (*k == key || (MissingKey { rev: key.rev, ..k.clone() } == key && now - at < MISSING_REFRESH_SECS))
|
||||
{
|
||||
return m.clone();
|
||||
}
|
||||
let m = packs::missing(&app.session);
|
||||
let packs = packs::missing(&app.session);
|
||||
let unlisted = if packs.is_empty() { packs::unlisted(&app.session) } else { Vec::new() };
|
||||
let m = Offers { packs, unlisted };
|
||||
app.packs_rt.missing = Some((key, now, m.clone()));
|
||||
m
|
||||
}
|
||||
|
||||
/// Get `tag`'s pack before the list is known: the person clicked the bar's Install. Their click
|
||||
/// turns on downloading ([`consent`]); the list is fetched, then the pack installed if it lists
|
||||
/// one (on the engine's thread).
|
||||
fn start_get(app: &mut WordApp, ctx: &egui::Context, tag: &str) {
|
||||
app.packs_rt.errors.remove("");
|
||||
if !consent(app, ctx, "") {
|
||||
return;
|
||||
}
|
||||
app.packs_rt.asked = true;
|
||||
if let Err(e) = packs::start_get(tag) {
|
||||
app.packs_rt
|
||||
.errors
|
||||
.insert(String::new(), crate::i18n::fmt(tl!("Couldn't get the list of languages: {error}"), &[("error", &shown_error(&e))]));
|
||||
}
|
||||
app.packs_rt.missing = None;
|
||||
}
|
||||
|
||||
/// The missing-pack bar under the ribbon: "<Language> proofing isn't installed. Install (<size>)",
|
||||
/// for the first language of the document with an available pack that isn't installed. Closing it
|
||||
/// hides it for that language in this document. Never shown for a language with no pack.
|
||||
@@ -272,7 +312,13 @@ pub(crate) fn bar(app: &mut WordApp, ui: &mut Ui) {
|
||||
}
|
||||
let document = app.session.document_id();
|
||||
let now = ui.input(|i| i.time);
|
||||
let Some(pack) = missing(app, now).into_iter().find(|p| !app.packs_rt.dismissed.contains(&(document, p.locale.clone()))) else {
|
||||
let offers = missing(app, now);
|
||||
// The whole list, for a tag closed before it was known (only then: it is a copy).
|
||||
let index = if app.packs_rt.dismissed_tags.is_empty() { Vec::new() } else { packs::available() };
|
||||
let Some(pack) = offers.packs.into_iter().find(|p| !dismissed(app, document, p, &index)) else {
|
||||
if let Some(tag) = offers.unlisted.into_iter().find(|t| !app.packs_rt.dismissed_tags.contains(&(document, t.clone()))) {
|
||||
unlisted_bar(app, ui, &tag);
|
||||
}
|
||||
return;
|
||||
};
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -311,6 +357,67 @@ pub(crate) fn bar(app: &mut WordApp, ui: &mut Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether the bar for `pack` was closed in `document`: for the pack itself, or for a language
|
||||
/// tag it serves, closed before the list was known (`kl-GL` for `kl`).
|
||||
fn dismissed(app: &WordApp, document: u64, pack: &Pack, index: &[Pack]) -> bool {
|
||||
// A closed pack is that pack alone (closing `de-ch` leaves `de`); a closed tag is the pack it
|
||||
// finds in the whole list (`de-CH` is `de-ch`'s, not `de`'s, once the list has both).
|
||||
app.packs_rt.dismissed.contains(&(document, pack.locale.clone()))
|
||||
|| app
|
||||
.packs_rt
|
||||
.dismissed_tags
|
||||
.iter()
|
||||
.any(|(d, tag)| *d == document && packs::pack_for_tag(tag, index).is_some_and(|p| p.locale == pack.locale))
|
||||
}
|
||||
|
||||
/// The bar for a language the document asks for while there is no list of packs yet (a fresh
|
||||
/// install, or a list cached for an older pin): "<Language> proofing isn't installed. Install".
|
||||
/// Install fetches the list, then installs the language's pack if it has one; if not, the bar
|
||||
/// goes. Hidden while an automatic fetch of the list runs.
|
||||
fn unlisted_bar(app: &mut WordApp, ui: &mut Ui, tag: &str) {
|
||||
let fetching = packs::fetching();
|
||||
if fetching && !app.packs_rt.asked {
|
||||
return;
|
||||
}
|
||||
let document = app.session.document_id();
|
||||
let t = Tokens::get(ui.ctx());
|
||||
egui::Panel::top("language_pack_bar")
|
||||
.exact_size(36.0)
|
||||
.show_separator_line(false)
|
||||
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(12, 0)))
|
||||
.show(ui, |ui| {
|
||||
let bottom = ui.max_rect().bottom();
|
||||
let full = ui.max_rect();
|
||||
ui.painter().hline(full.x_range(), bottom - 0.5, egui::Stroke::new(1.0, t.border));
|
||||
ui.horizontal_centered(|ui| {
|
||||
let language = shown(&language_of(&tag.to_ascii_lowercase(), ""), MAX_BAR_NAME);
|
||||
ui.label(RichText::new(crate::i18n::fmt(tl!("{language} proofing isn't installed."), &[("language", &language)])).color(t.text));
|
||||
if fetching {
|
||||
ui.spinner();
|
||||
ui.label(RichText::new(tl!("Getting the list of languages…")).color(t.text_dim));
|
||||
} else {
|
||||
let mut hover = vec![tl!("Gets the list of Language packs, then this language's pack if there is one.").to_string()];
|
||||
if packs::backend().is_some_and(|b| !b.network_on()) {
|
||||
hover.push(tl!("Installing turns on downloading of fonts and Language packs from GitHub.").to_string());
|
||||
}
|
||||
if ui.add(crate::widgets::primary_button(ui, tl!("Install").to_string())).on_hover_text(hover.join("\n")).clicked() {
|
||||
start_get(app, &ui.ctx().clone(), tag);
|
||||
}
|
||||
}
|
||||
// The window's own failure (consent), else the list's when the person asked.
|
||||
let e = app.packs_rt.errors.get("").cloned().or_else(|| if app.packs_rt.asked { error_of(app, "") } else { None });
|
||||
if let Some(e) = e {
|
||||
ui.add(egui::Label::new(RichText::new(e).color(t.red)).truncate());
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if crate::widgets::icon_button(ui, "close", tl!("Not now")).clicked() {
|
||||
app.packs_rt.dismissed_tags.insert((document, tag.to_string()));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// The bar's Install tooltip: the licence and source, that it turns on downloading when it is
|
||||
/// off, and the test source when packs come from one.
|
||||
fn licence_hover(pack: &Pack) -> String {
|
||||
@@ -920,4 +1027,122 @@ mod tests {
|
||||
}
|
||||
assert!(packs::is_installed("pl"));
|
||||
}
|
||||
|
||||
/// A font store that never fetched the list (a fresh install): `index` has nothing.
|
||||
struct Uncached(FixedBackend);
|
||||
|
||||
impl Backend for Uncached {
|
||||
fn index(&self) -> Result<Vec<packs::Pack>, String> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn fetch_index(&self, c: &packs::Cancel) -> Result<Vec<packs::Pack>, String> {
|
||||
self.0.fetch_index(c)
|
||||
}
|
||||
fn network_on(&self) -> bool {
|
||||
self.0.network_on()
|
||||
}
|
||||
fn enable_network(&self) -> Result<(), String> {
|
||||
self.0.enable_network()
|
||||
}
|
||||
fn install(&self, pack: &packs::Pack, cancel: &packs::Cancel, progress: &mut dyn FnMut(u64, u64)) -> Result<(), String> {
|
||||
self.0.install(pack, cancel, progress)
|
||||
}
|
||||
fn remove(&self, locale: &str) -> Result<(), String> {
|
||||
self.0.remove(locale)
|
||||
}
|
||||
fn packs_dir(&self) -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
fn installed(&self) -> Vec<packs::Installed> {
|
||||
self.0.installed()
|
||||
}
|
||||
}
|
||||
|
||||
/// Martin's test of a fresh install: no list fetched, Greenlandic text. The bar still offers
|
||||
/// it; a faked click leaves downloading off, and the person's click fetches the list and
|
||||
/// installs the pack.
|
||||
#[test]
|
||||
fn with_no_list_yet_the_bar_offers_the_language_and_gets_it() {
|
||||
let _g = BACKEND_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
crate::theme::apply(&ctx, crate::theme::Appearance::Light);
|
||||
ctx.run_ui(egui::RawInput::default(), |_| {}).drop_without_applying_deltas();
|
||||
ctx.enable_accesskit();
|
||||
struct Reset;
|
||||
impl Drop for Reset {
|
||||
fn drop(&mut self) {
|
||||
packs::set_background(None);
|
||||
packs::set_backend(None);
|
||||
}
|
||||
}
|
||||
let _reset = Reset;
|
||||
let mut inner = FixedBackend::sample();
|
||||
let kl = packs::Pack { locale: "kl".into(), name: "Kalaallisut".into(), ..inner.packs[0].clone() };
|
||||
inner.packs.push(kl);
|
||||
let backend = std::sync::Arc::new(Uncached(inner));
|
||||
packs::set_backend(Some(backend.clone() as std::sync::Arc<dyn Backend>));
|
||||
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), crate::Services::default());
|
||||
a.run("document.setText", json!({"text": "Aluu, qanoq ippit?"})).unwrap();
|
||||
a.run("select.all", json!({})).unwrap();
|
||||
a.run("review.language", json!({"lang": "kl-GL"})).unwrap();
|
||||
let mut tree = frame(&mut a, &ctx, Vec::new());
|
||||
for _ in 0..4 {
|
||||
tree = frame(&mut a, &ctx, Vec::new());
|
||||
}
|
||||
assert!(packs::available().is_empty());
|
||||
let install = spot(&tree, "Install").expect("the bar offers Greenlandic with no list");
|
||||
as_agent(&mut a, &ctx, "ui.click", json!({"x": install.0, "y": install.1}));
|
||||
assert!(!backend.0.network.load(Ordering::Relaxed), "a faked click leaves downloading off");
|
||||
assert!(!packs::fetched() && !packs::is_installed("kl"));
|
||||
assert!(a.packs_rt.errors.get("").is_some_and(|e| e.contains("Only you")), "{:?}", a.packs_rt.errors);
|
||||
user_click(&mut a, &ctx, install);
|
||||
assert!(backend.0.network.load(Ordering::Relaxed), "the person's click turns it on");
|
||||
for _ in 0..500 {
|
||||
if packs::is_installed("kl") {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
assert!(packs::fetched(), "the list was fetched");
|
||||
assert!(packs::is_installed("kl"), "then its pack installed");
|
||||
}
|
||||
|
||||
/// Review: a language whose bar was closed before the list was known stays closed once the
|
||||
/// list lists its pack (the dismissal is kept by document tag, the pack bar by pack locale).
|
||||
#[test]
|
||||
fn a_language_closed_before_the_list_stays_closed_after_it() {
|
||||
let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), crate::Services::default());
|
||||
let doc = a.session.document_id();
|
||||
let kl = packs::Pack { locale: "kl".into(), ..FixedBackend::sample().packs[0].clone() };
|
||||
let pt_pt = packs::Pack { locale: "pt-pt".into(), ..kl.clone() };
|
||||
let index = vec![kl.clone(), pt_pt.clone()];
|
||||
assert!(!super::dismissed(&a, doc, &kl, &index));
|
||||
a.packs_rt.dismissed_tags.insert((doc, "kl-GL".into()));
|
||||
assert!(super::dismissed(&a, doc, &kl, &index), "closed as kl-GL: kl stays closed");
|
||||
assert!(!super::dismissed(&a, doc, &pt_pt, &index), "other languages aren't");
|
||||
assert!(!super::dismissed(&a, doc.wrapping_add(1), &kl, &index), "nor other documents");
|
||||
// Closing a regional pack's bar leaves the base language's (Swiss German, then German).
|
||||
let de = packs::Pack { locale: "de".into(), ..kl.clone() };
|
||||
let de_ch = packs::Pack { locale: "de-ch".into(), ..kl.clone() };
|
||||
let index = vec![de.clone(), de_ch.clone()];
|
||||
a.packs_rt.dismissed.insert((doc, "de-ch".into()));
|
||||
assert!(super::dismissed(&a, doc, &de_ch, &index));
|
||||
assert!(!super::dismissed(&a, doc, &de, &index), "German's bar still shows");
|
||||
// Closed as de-CH before the list was known: Swiss German's pack, not German's.
|
||||
a.packs_rt.dismissed.clear();
|
||||
a.packs_rt.dismissed_tags.insert((doc, "de-CH".into()));
|
||||
assert!(super::dismissed(&a, doc, &de_ch, &index));
|
||||
assert!(!super::dismissed(&a, doc, &de, &index), "a de-CH tag doesn't close German");
|
||||
}
|
||||
|
||||
/// Martin's test: a spelling issue's context menu heading is in the interface language, not
|
||||
/// the text's (craft-proof's message for French text is "Faute d'orthographe possible").
|
||||
#[test]
|
||||
fn the_spelling_heading_is_in_the_interface_language() {
|
||||
let french = json!({"kind": "spelling", "message": "Faute d\u{2019}orthographe possible", "lang": "fr"});
|
||||
assert_eq!(crate::canvas::issue_heading(&french), tl!("Possible spelling mistake"));
|
||||
let grammar = json!({"kind": "grammar", "message": "Repeated word"});
|
||||
assert_eq!(crate::canvas::issue_heading(&grammar), tl!("Repeated word"));
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
# Localization parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** trivial (Language lists: every installed and available Language pack's language can be chosen in Set Proofing Language, installed ones in Review › Language and the status bar; craft-i18n.rev at 9d6d54d: region names and the Language (Region) template in 35 locales, craft-i18n#145); previously trivial (craft-i18n.rev at cde4900: rows for Options › Language, the missing-pack bar and 28 Language pack language names in 35 locales, craft-i18n#136); previously trivial (craft-proof pin: French elision suggestions first, ca-ES-valencia reads the ca-valencia pack); previously minor (Arabic `ar` interface: craft-i18n catalog, mirrored shell, font fallback, #1195; 28 languages); previously trivial (Language packs: language-packs-2 is published and pinned: 75 packs, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch and Swedish (Sweden, Finland)); previously trivial (Language packs: language-packs-1 is published and pinned, 61 packs; the language requests it covers); previously minor (Language packs: languages beyond English install their spelling dictionary and hyphenation as a pack from File › Options › Language or the missing-pack bar; the default build embeds English Language data only) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** trivial (Language packs: the missing-pack bar works on a fresh install and counts the caret's language); previously trivial (Language lists: every installed and available Language pack's language can be chosen in Set Proofing Language, installed ones in Review › Language and the status bar; craft-i18n.rev at 9d6d54d: region names and the Language (Region) template in 35 locales, craft-i18n#145); previously trivial (craft-i18n.rev at cde4900: rows for Options › Language, the missing-pack bar and 28 Language pack language names in 35 locales, craft-i18n#136); previously trivial (craft-proof pin: French elision suggestions first, ca-ES-valencia reads the ca-valencia pack); previously minor (Arabic `ar` interface: craft-i18n catalog, mirrored shell, font fallback, #1195; 28 languages); previously trivial (Language packs: language-packs-2 is published and pinned: 75 packs, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch and Swedish (Sweden, Finland)); previously trivial (Language packs: language-packs-1 is published and pinned, 61 packs; the language requests it covers); previously minor (Language packs: languages beyond English install their spelling dictionary and hyphenation as a pack from File › Options › Language or the missing-pack bar; the default build embeds English Language data only) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
Interface languages, script support and proofing per language. How translation works (adding a
|
||||
language, the embedded catalogs): `crates/ui-egui/src/i18n.rs`. The catalogs, their format and
|
||||
@@ -174,7 +174,11 @@ pinned pack index and installed through the font store into `<font store>/langua
|
||||
agents). A pack is read at once; the lookup order is the dictionaries folder, the installed pack,
|
||||
then the system's Hunspell dictionaries.
|
||||
- A document with text in a language whose pack is available but not installed shows a bar
|
||||
offering it; it never shows for a language without a pack, and closes per document.
|
||||
offering it, and closes per document. Once the list of packs is known it never shows for a
|
||||
language without a pack; before then (a fresh install) it offers a language with no dictionary
|
||||
provisionally: Install fetches the list, then installs the pack if there is one, and the bar goes
|
||||
if there isn't. The caret's typing language counts, and the spelling context menu's heading is in
|
||||
the interface language.
|
||||
- Help › About › Language data shows the full licence texts of the embedded English data and the
|
||||
installed packs.
|
||||
- One language list: Set Proofing Language lists the built-in languages and every installed or
|
||||
@@ -268,6 +272,7 @@ WordCraft keeps 2,54 cm).
|
||||
| 2026-11-03 | trivial | craft-proof pin moved to craft-libs main with #589 and #590: French suggestions offer the elided form first (`Jadore` → `J'adore` before `J adore`; English keeps plural fixes first), and Valencian (`ca-ES-valencia`) text reads the `ca-valencia` pack |
|
||||
| 2026-11-03 | minor | Arabic (`ar`) interface: ~2,730 catalog rows from craft-i18n, `ar-*` locales, mirrored shell (ribbon, panes, File page, dialogs, menus), Arabic UI font fallback, document canvas independent (#1195); 28 languages; Hebrew UI still open |
|
||||
| 2026-11-03 | trivial | Language lists: Set Proofing Language, Review › Language and the status bar offer the Language pack languages (Bulgarian was in none); every published pack named in English (Belarusian); Valencian is `ca-ES-valencia`; rows are matched by tag, not by the name shown |
|
||||
| 2026-11-03 | trivial | Language packs (CP4.3): the missing-pack bar offers a language before the list of packs is fetched (a fresh install, or a list cached for an older pin) and gets it on Install; with downloading on, a missing or stale list is fetched once in the background; the caret's typing language counts; the spelling context menu's heading is in the interface language |
|
||||
| 2026-11-03 | trivial | Language packs: language-packs-2 published and pinned, 75 packs, with Danish, German, Dutch, Swedish, Norwegian, Hungarian, Estonian, Croatian, Persian and Esperanto |
|
||||
| 2026-11-02 | trivial | Language packs (CP4.4): language-packs-1 published and pinned, 61 packs; Lithuanian, Romanian, Catalan, French, Greek, Slovak, Serbian and Latvian available; Dutch, German and the other compounding languages wait on craft-proof; Basque, Ukrainian and Uyghur excluded |
|
||||
| 2026-11-01 | minor | Language packs (CP4.3–CP4.5): install and remove a language's spelling and hyphenation from File › Options › Language or the missing-pack bar, About › Language data, the commands; the default build embeds English data only; the language requests point at their packs: Lithuanian and Romanian available once language-packs-1 is published, the others waiting on craft-proof engine support |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Installing languages and spelling dictionaries
|
||||
|
||||
> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-11-03 · **Change:** trivial (Language packs: language-packs-2 is published and pinned: 75 packs, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch and Swedish (Sweden, Finland); Mongolian and Thai aren't packs yet); previously trivial (Language packs: the first set, language-packs-1, is published and pinned: 61 languages, and which aren't packs yet); previously minor (Language packs: install a language's spelling and hyphenation from File › Options › Language, or from the bar a document in that language shows; they are searched after the dictionaries folder and before the system's dictionaries; previously trivial: the Flatpak's own dictionaries folder; previously trivial: WordCraft's own dictionaries folder is searched on macOS and Linux too, as on Windows, so a dictionary dropped there works without a setting; previously minor: ISO 8859-9 Turkish dictionaries are read, #3692; previously minor: Spanish: `es_ES`, `es_MX`, `es_419` and `es_UY` with their fallbacks, #3360, #3553; previously trivial: every Hunspell language shows (no dictionary), not only Polish and Portuguese, and Indonesian is listed, #3150; previously trivial: ISO 8859-1 dictionaries are read, #2824)
|
||||
> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-11-03 · **Change:** trivial (Language packs: the missing-pack bar works on a fresh install and counts the caret's language); previously trivial (Language packs: language-packs-2 is published and pinned: 75 packs, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch and Swedish (Sweden, Finland); Mongolian and Thai aren't packs yet); previously trivial (Language packs: the first set, language-packs-1, is published and pinned: 61 languages, and which aren't packs yet); previously minor (Language packs: install a language's spelling and hyphenation from File › Options › Language, or from the bar a document in that language shows; they are searched after the dictionaries folder and before the system's dictionaries; previously trivial: the Flatpak's own dictionaries folder; previously trivial: WordCraft's own dictionaries folder is searched on macOS and Linux too, as on Windows, so a dictionary dropped there works without a setting; previously minor: ISO 8859-9 Turkish dictionaries are read, #3692; previously minor: Spanish: `es_ES`, `es_MX`, `es_419` and `es_UY` with their fallbacks, #3360, #3553; previously trivial: every Hunspell language shows (no dictionary), not only Polish and Portuguese, and Indonesian is listed, #3150; previously trivial: ISO 8859-1 dictionaries are read, #2824)
|
||||
|
||||
English spelling works out of the box. Polish, Russian, Portuguese, Czech, Italian, Croatian,
|
||||
Turkish, Indonesian and Spanish spelling use a standard **Hunspell** dictionary (an `.aff` and a `.dic` file) that
|
||||
@@ -32,7 +32,12 @@ built in; every other language is a pack.
|
||||
again. The first time, click **Show available languages** to get the list.
|
||||
- When a document has text in a language whose pack isn't installed, a bar under the ribbon says
|
||||
*Dutch proofing isn't installed.* with an **Install** button showing the download size. ✕ hides
|
||||
it for that document. It never shows for a language that has no pack.
|
||||
it for that document. Once WordCraft has the list of packs, the bar shows only for a language
|
||||
that has a pack. Before the list has been fetched (a fresh install, or the list of an older
|
||||
WordCraft), it can't know, so it shows for a language that has no dictionary, with **Install**
|
||||
alone: clicking it gets the list, then the pack if there is one (if not, the bar goes). With
|
||||
downloading on, WordCraft fetches a missing or out-of-date list by itself once. Choosing a
|
||||
language with nothing selected counts too.
|
||||
- Installing needs **downloading** on: the same single setting as WordCraft's font downloads
|
||||
(the font store). Clicking Install, Show available languages or Refresh turns it on; nothing is
|
||||
downloaded before. Each pack is checked against the sha256 in WordCraft's pinned list before it
|
||||
@@ -183,6 +188,7 @@ Never commit dictionary files to this repository: they stay on the user's system
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-03 | trivial | Language packs (CP4.3): the missing-pack bar offers a language before the list is fetched and gets the list, then the pack, on Install; a missing or stale list is fetched once while downloading is on; the caret's typing language counts |
|
||||
| 2026-11-03 | trivial | Language packs: language-packs-2 is published and pinned (75 languages, adding the compounding languages); Mongolian and Thai aren't packs yet, and why |
|
||||
| 2026-11-02 | trivial | Language packs: language-packs-1 is published and pinned (61 languages); which languages aren't packs yet and why (CP4.4) |
|
||||
| 2026-11-02 | trivial | The Flatpak's own dictionaries folder (`~/.var/app/ai.storyteller.wordcraft/data/wordcraft/dictionaries`); the host's folders are outside its sandbox |
|
||||
|
||||
Reference in New Issue
Block a user