Numeral display modes: Arabic, Hindi, Context and System digits (view.numeral) (#501)
* Numeral display modes: Arabic, Hindi, Context and System digits Display-only digit substitution (Word File > Options > Advanced > Numeral): ASCII and Arabic-Indic digits are substituted before shaping while clusters keep indexing the original text, so find, spelling, copy/paste and saved files see the typed digits. - layout: NumeralMode (arabic|hindi|context|system) with System resolved from LC_ALL/LANG, threaded through LayoutOptions/ParaEnv with cache invalidation; Context follows the surrounding text - engine: view.numeral command + catalog row - tests: layout tests_numeral, engine command test * Options page: Numerals picker in File > Options > Display ComboBox for the view.numeral mode (Arabic/Hindi/Context/System), mirroring Word's File > Options > Advanced > Numeral. Display-only, session-scoped like the other Display options. * fmt: collapse numeral direction branch * #501: translate the Numerals option label in every catalog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: abdo4312 <abdo4312@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1c188ba140
commit
2f58d441b6
|
|
@ -20,6 +20,7 @@ File|Backstage|Properties|file.properties
|
|||
File|Backstage|Export PDF|file.exportPdf
|
||||
File|Backstage|Export Image|file.exportPng
|
||||
File|Backstage|Options|file.options
|
||||
File|Options|Numerals|view.numeral
|
||||
File|Backstage|Share|file.share
|
||||
File|Backstage|Protect Document|file.protect
|
||||
File|Backstage|Inspect Document|file.inspect
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! View tab and status bar: views, zoom, show/hide, panes.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use wordcraft_layout::ViewMode;
|
||||
use wordcraft_layout::{NumeralMode, ViewMode};
|
||||
|
||||
use super::sel_result;
|
||||
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
|
||||
|
|
@ -83,6 +83,22 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
.pure(),
|
||||
CommandSpec::new("view.state", "View State", "View", |s, _| serde_json::to_value(&s.view).map_err(|e| CmdError::Failed(e.to_string())))
|
||||
.pure(),
|
||||
// Word › File › Options › Advanced › Numeral: how digits look on screen (display
|
||||
// only; the document text keeps the typed codepoints).
|
||||
CommandSpec::new("view.numeral", "Numerals", "File › Options › Advanced", |s, v| match v.get("value") {
|
||||
None => Ok(json!({"value": s.view.numeral})),
|
||||
Some(Value::String(name)) => {
|
||||
let Some(mode) = NumeralMode::parse(name) else {
|
||||
return Err(CmdError::Params("numeral mode: arabic|hindi|context|system".into()));
|
||||
};
|
||||
s.view.numeral = mode;
|
||||
s.relayout();
|
||||
Ok(json!({"value": s.view.numeral}))
|
||||
}
|
||||
Some(_) => Err(CmdError::Params("numeral mode: arabic|hindi|context|system".into())),
|
||||
})
|
||||
.params(r#"{"value"?: "arabic|hindi|context|system"}"#)
|
||||
.pure(),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use serde_json::Value;
|
|||
pub use io::Password;
|
||||
pub use keymap::KeyMap;
|
||||
pub use ribbon::RibbonLayout;
|
||||
pub use session::{ColumnBlock, EditSnapshot, FindState, MathEdit, Prefs, Selection, Session, ViewState};
|
||||
pub use session::{ColumnBlock, EditSnapshot, FindState, MathEdit, Prefs, Selection, Session, ViewState, set_system_locale};
|
||||
pub use wordcraft_doc as doc;
|
||||
pub use wordcraft_layout as layout;
|
||||
pub use wordcraft_proof as proof;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
//! The editing session: document, selection, undo/redo, layout, view state.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
/// The system locale (BCP 47 tag) the shell found at startup, for Numeral › System.
|
||||
static SYSTEM_LOCALE: OnceLock<String> = OnceLock::new();
|
||||
|
||||
/// Tell the engine the system locale (the shell calls this once at startup; layout never reads
|
||||
/// the environment). Headless sessions without it show Western digits for Numeral › System.
|
||||
pub fn set_system_locale(tag: &str) {
|
||||
let _ = SYSTEM_LOCALE.set(tag.to_string());
|
||||
}
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use wordcraft_doc::edit::Fragment;
|
||||
use wordcraft_doc::props::CharProps;
|
||||
use wordcraft_doc::{Document, Path, Pos, StoryRef};
|
||||
use wordcraft_layout::{DocLayout, LayoutCache, LayoutOptions, ViewMode};
|
||||
use wordcraft_layout::{DocLayout, LayoutCache, LayoutOptions, NumeralMode, ViewMode};
|
||||
|
||||
use crate::{CmdError, Registry};
|
||||
|
||||
|
|
@ -86,6 +95,9 @@ pub struct ViewState {
|
|||
pub track_changes_pane: bool,
|
||||
/// Check spelling and grammar as you type.
|
||||
pub proofing: bool,
|
||||
/// How digits look on screen (Word › File › Options › Advanced › Numeral).
|
||||
#[serde(default)]
|
||||
pub numeral: NumeralMode,
|
||||
/// Review › Hide Ink: ink strokes aren't shown on screen (they stay in the document).
|
||||
#[serde(default)]
|
||||
pub hide_ink: bool,
|
||||
|
|
@ -138,6 +150,7 @@ impl Default for ViewState {
|
|||
show_markup: true,
|
||||
track_changes_pane: false,
|
||||
proofing: true,
|
||||
numeral: NumeralMode::default(),
|
||||
hide_ink: false,
|
||||
draw: Default::default(),
|
||||
field_codes: false,
|
||||
|
|
@ -269,8 +282,8 @@ pub struct Session {
|
|||
document_id: u64,
|
||||
cache: LayoutCache,
|
||||
/// (revision, web width, view, layout, proofing, outline, (deletions left out, body
|
||||
/// deletions in balloons)).
|
||||
layout: Option<(u64, f32, ViewMode, Arc<DocLayout>, bool, Option<wordcraft_layout::outline::OutlineView>, (bool, bool))>,
|
||||
/// deletions in balloons), numeral display).
|
||||
layout: Option<(u64, f32, ViewMode, Arc<DocLayout>, bool, Option<wordcraft_layout::outline::OutlineView>, (bool, bool), NumeralMode)>,
|
||||
/// Picture edits: edited media key → original media key (Reset Picture).
|
||||
pub originals: std::collections::HashMap<String, String>,
|
||||
/// Last mutating command (Repeat).
|
||||
|
|
@ -572,11 +585,12 @@ impl Session {
|
|||
let hide_deleted = !self.view.show_markup || self.prefs.markup.hides_deletions();
|
||||
let deleted = (hide_deleted, self.revisions_in_balloons());
|
||||
let outline = (self.view.mode == ViewMode::Outline).then(|| self.view.outline.view());
|
||||
if let Some((r, w, m, l, pf, o, hd)) = &self.layout
|
||||
if let Some((r, w, m, l, pf, o, hd, n)) = &self.layout
|
||||
&& *r == self.rev
|
||||
&& (*w == ww || self.view.mode == ViewMode::Print)
|
||||
&& *m == self.view.mode
|
||||
&& *pf == self.view.proofing
|
||||
&& *n == self.view.numeral
|
||||
&& *hd == deleted
|
||||
&& *o == outline
|
||||
{
|
||||
|
|
@ -589,13 +603,14 @@ impl Session {
|
|||
hide_deleted,
|
||||
deletions_in_balloons: deleted.1,
|
||||
proofing: self.view.proofing,
|
||||
numeral: self.view.numeral.resolve(SYSTEM_LOCALE.get().map(String::as_str)),
|
||||
comment_balloons: false,
|
||||
field_codes: self.view.field_codes,
|
||||
outline: outline.clone(),
|
||||
proof: wordcraft_layout::ProofOptions { grammar: self.prefs.mark_grammar, spelling: self.prefs.spelling() },
|
||||
};
|
||||
let l = Arc::new(wordcraft_layout::layout(&self.doc, &mut self.cache, &opts));
|
||||
self.layout = Some((self.rev, ww, self.view.mode, l.clone(), self.view.proofing, outline, deleted));
|
||||
self.layout = Some((self.rev, ww, self.view.mode, l.clone(), self.view.proofing, outline, deleted, self.view.numeral));
|
||||
l
|
||||
}
|
||||
/// A layout for output (PDF, images, print): no proofing marks, print view.
|
||||
|
|
|
|||
|
|
@ -2615,6 +2615,30 @@ fn page_and_table_gridlines_toggle_independently() {
|
|||
assert_eq!(state["tableGridlines"], false);
|
||||
}
|
||||
|
||||
/// File › Options › Advanced › Numeral: the display mode for digits (display only).
|
||||
#[test]
|
||||
fn numeral_mode_sets_gets_and_rejects_junk() {
|
||||
use wordcraft_layout::NumeralMode;
|
||||
let mut s = s();
|
||||
assert_eq!(s.view.numeral, NumeralMode::Arabic, "Western digits by default");
|
||||
let r = run(&mut s, "view.numeral", json!({"value": "hindi"}));
|
||||
assert_eq!(r["value"], "hindi");
|
||||
assert_eq!(s.view.numeral, NumeralMode::Hindi);
|
||||
let r = run(&mut s, "view.numeral", json!({}));
|
||||
assert_eq!(r["value"], "hindi", "no params reads the current mode");
|
||||
run(&mut s, "view.numeral", json!({"value": "Context"}));
|
||||
assert_eq!(s.view.numeral, NumeralMode::Context, "mode names ignore case");
|
||||
assert!(s.run("view.numeral", &json!({"value": "western"})).is_err());
|
||||
assert!(s.run("view.numeral", &json!({"value": 3})).is_err());
|
||||
assert!(s.run("view.numeral", &json!({"value": ["hindi"]})).is_err());
|
||||
assert_eq!(s.view.numeral, NumeralMode::Context, "rejected values keep the mode");
|
||||
// Switching modes relayouts without disturbing the text.
|
||||
s.doc = wordcraft_doc::Document::from_text("Year 2024");
|
||||
run(&mut s, "view.numeral", json!({"value": "hindi"}));
|
||||
let _l = s.layout();
|
||||
assert_eq!(text(&s), "Year 2024");
|
||||
}
|
||||
|
||||
/// Issue #67: View › Zoom steps. Zoom In/Out leave a fit mode and step 10% from the current zoom
|
||||
/// (the UI keeps `view.zoom` equal to the shown zoom while a fit mode is on), and never get stuck
|
||||
/// short of the 10%–500% limits.
|
||||
|
|
|
|||
|
|
@ -52,6 +52,88 @@ pub enum ViewMode {
|
|||
Outline,
|
||||
}
|
||||
|
||||
/// How digits look on screen (Word › File › Options › Advanced › Numeral).
|
||||
///
|
||||
/// Display only: the document text keeps the typed codepoints, so find, spelling, copying and
|
||||
/// the saved file all see the original digits. Extended Arabic-Indic digits (U+06F0–U+06F9,
|
||||
/// Persian/Urdu ۰۱۲۳…) always show as typed; only ASCII and Arabic-Indic (U+0660–U+0669) digits
|
||||
/// are substituted.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum NumeralMode {
|
||||
/// Western digits everywhere (`0123456789`); the default, today's behaviour.
|
||||
#[default]
|
||||
Arabic,
|
||||
/// Eastern Arabic-Indic digits everywhere (`٠١٢٣٤٥٦٧٨٩`).
|
||||
Hindi,
|
||||
/// Hindi digits in and around right-to-left text, Western digits elsewhere (numbers take
|
||||
/// an even bidi level, so the surrounding text decides).
|
||||
Context,
|
||||
/// Hindi digits when the system locale is an Arabic-script language, else Western digits.
|
||||
/// Call [`NumeralMode::resolve`] before laying out.
|
||||
System,
|
||||
}
|
||||
|
||||
impl NumeralMode {
|
||||
/// Short UI label (Word's option names).
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
NumeralMode::Arabic => "Arabic",
|
||||
NumeralMode::Hindi => "Hindi",
|
||||
NumeralMode::Context => "Context",
|
||||
NumeralMode::System => "System",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a command value (`"arabic" | "hindi" | "context" | "system"`, any case).
|
||||
pub fn parse(s: &str) -> Option<NumeralMode> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"arabic" => Some(NumeralMode::Arabic),
|
||||
"hindi" => Some(NumeralMode::Hindi),
|
||||
"context" => Some(NumeralMode::Context),
|
||||
"system" => Some(NumeralMode::System),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve `System` against the system locale (a BCP 47 or POSIX tag such as `ar-EG` or
|
||||
/// `fa_IR.UTF-8`, found by the shell: layout never reads the environment). Hindi digits for
|
||||
/// Arabic-script languages, else Western; unknown locale = Western. Other modes pass through.
|
||||
pub fn resolve(self, system_locale: Option<&str>) -> NumeralMode {
|
||||
if self != NumeralMode::System {
|
||||
return self;
|
||||
}
|
||||
if system_locale.is_some_and(hindi_locale) { NumeralMode::Hindi } else { NumeralMode::Arabic }
|
||||
}
|
||||
|
||||
/// The displayed form of `c` in a run shaped with direction `rtl`.
|
||||
///
|
||||
/// One character maps to exactly one character, so shaped text stays codepoint-aligned
|
||||
/// with the document text (only byte offsets shift). A `System` mode that was not
|
||||
/// resolved falls back to Western digits.
|
||||
pub fn map(self, c: char, rtl: bool) -> char {
|
||||
let hindi = match self {
|
||||
NumeralMode::Arabic => false,
|
||||
NumeralMode::Hindi => true,
|
||||
NumeralMode::Context => rtl,
|
||||
NumeralMode::System => false,
|
||||
};
|
||||
if hindi {
|
||||
if c.is_ascii_digit() { char::from_u32(0x660 + (c as u32 - '0' as u32)).unwrap_or(c) } else { c }
|
||||
} else if ('\u{660}'..='\u{669}').contains(&c) {
|
||||
(b'0' + (c as u32 - 0x660) as u8) as char
|
||||
} else {
|
||||
c
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Does locale `tag` use Hindi digits (Arabic, Persian, Urdu, Pashto, Sindhi, Kurdish)?
|
||||
fn hindi_locale(tag: &str) -> bool {
|
||||
let lang = tag.split(['_', '-', '.', '@']).next().unwrap_or_default().to_ascii_lowercase();
|
||||
matches!(lang.as_str(), "ar" | "fa" | "ur" | "ps" | "sd" | "ku" | "ckb")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LayoutOptions {
|
||||
pub view: ViewMode,
|
||||
|
|
@ -66,6 +148,9 @@ pub struct LayoutOptions {
|
|||
pub deletions_in_balloons: bool,
|
||||
/// Check spelling as you type (squiggles).
|
||||
pub proofing: bool,
|
||||
/// How digits look on screen (display only; the text is untouched). Callers resolve
|
||||
/// `System` ([`NumeralMode::resolve`]); an unresolved `System` shows Western digits.
|
||||
pub numeral: NumeralMode,
|
||||
/// Print Markup (PDF, print, page images): widen each page by [`balloons::MARKUP_AREA`]
|
||||
/// and show the comments there in balloons joined to their anchors, as Word prints them.
|
||||
/// The editor draws its own interactive balloons and leaves this off.
|
||||
|
|
@ -409,6 +494,7 @@ fn env_hash(doc: &Document, opts: &LayoutOptions) -> u64 {
|
|||
opts.hide_deleted,
|
||||
opts.deletions_in_balloons,
|
||||
opts.proofing,
|
||||
opts.numeral,
|
||||
opts.field_codes,
|
||||
opts.proof,
|
||||
wordcraft_proof::dictionary_generation(),
|
||||
|
|
@ -549,6 +635,7 @@ impl Ctx<'_> {
|
|||
hide_deleted: false,
|
||||
table: None,
|
||||
proofing: None,
|
||||
numeral: self.opts.numeral,
|
||||
exclusions: &[],
|
||||
eq_number: 0,
|
||||
grid: para::Grid::default(),
|
||||
|
|
@ -618,6 +705,7 @@ impl Ctx<'_> {
|
|||
hide_deleted: self.hide_deleted,
|
||||
table,
|
||||
proofing: self.opts.proofing.then_some(self.opts.proof),
|
||||
numeral: self.opts.numeral,
|
||||
exclusions,
|
||||
eq_number,
|
||||
grid: self.grid,
|
||||
|
|
@ -2715,6 +2803,7 @@ fn next_first_line(ctx: &mut Ctx, block: usize, width: f32) -> f32 {
|
|||
hide_deleted: ctx.opts.hides_deleted(StoryRef::Body),
|
||||
table: None,
|
||||
proofing: None,
|
||||
numeral: ctx.opts.numeral,
|
||||
exclusions: &[],
|
||||
eq_number: ctx.eq_count,
|
||||
grid: ctx.grid,
|
||||
|
|
@ -3038,6 +3127,8 @@ mod tests_complex;
|
|||
#[cfg(test)]
|
||||
mod tests_grid;
|
||||
#[cfg(test)]
|
||||
mod tests_numeral;
|
||||
#[cfg(test)]
|
||||
mod tests_pages;
|
||||
#[cfg(test)]
|
||||
mod tests_textbox;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use wordcraft_doc::resolve::{ResolvedChar, ResolvedPara};
|
|||
use wordcraft_doc::{Document, Paragraph};
|
||||
use wordcraft_fonts::FaceRef;
|
||||
|
||||
use crate::NumeralMode;
|
||||
use crate::fields::{FIELD_SHADING, FieldCtx, field_code_text, field_text};
|
||||
|
||||
/// A face at a size with the formatting needed to draw it.
|
||||
|
|
@ -334,6 +335,8 @@ pub struct ParaEnv<'a> {
|
|||
pub table: Option<&'a CellText>,
|
||||
/// Check spelling (and grammar, as the options say) for squiggles; `None` = no squiggles.
|
||||
pub proofing: Option<crate::ProofOptions>,
|
||||
/// How digits look on screen (an unresolved `System` shows Western digits).
|
||||
pub numeral: NumeralMode,
|
||||
/// Areas text must flow around (floating objects), relative to the paragraph: x from the
|
||||
/// column's left edge, y from the top of the first line.
|
||||
pub exclusions: &'a [Exclusion],
|
||||
|
|
@ -467,6 +470,64 @@ fn is_mark(c: char) -> bool {
|
|||
matches!(unicode_bidi::bidi_class(c), NSM | BN) && !c.is_control()
|
||||
}
|
||||
|
||||
/// A digit Word's Numeral modes substitute: ASCII or Arabic-Indic (U+0660–U+0669).
|
||||
fn is_numeral_digit(c: char) -> bool {
|
||||
c.is_ascii_digit() || ('\u{660}'..='\u{669}').contains(&c)
|
||||
}
|
||||
|
||||
/// Characters skipped when looking for the text around a number: digits, spaces and the
|
||||
/// Arabic decimal/thousands separators (٫ ٬).
|
||||
fn is_number_inside(c: char) -> bool {
|
||||
is_numeral_digit(c) || ('\u{6f0}'..='\u{6f9}').contains(&c) || c == ' ' || c == '\t' || c == '\u{66b}' || c == '\u{66c}'
|
||||
}
|
||||
|
||||
/// Which digits of run `text` sit in right-to-left surroundings (Word › Numeral › Context), by
|
||||
/// byte offset. European numbers take an even bidi level, so a digit's own level says little;
|
||||
/// the nearest neighbour on either side that isn't part of the number (digits, spaces,
|
||||
/// separators) decides. One pass each way, so long runs of digits and spaces stay linear.
|
||||
/// Offsets past `levels` count as left to right.
|
||||
fn context_digit_dirs(text: &str, levels: &[u8], base: usize) -> Vec<bool> {
|
||||
let odd = |o: usize| levels.get(base.saturating_add(o)).is_some_and(|l| l % 2 == 1);
|
||||
let mut out = vec![false; text.len()];
|
||||
let mut left_rtl = false;
|
||||
for (o, c) in text.char_indices() {
|
||||
if !is_number_inside(c) {
|
||||
left_rtl = odd(o);
|
||||
} else if is_numeral_digit(c)
|
||||
&& let Some(v) = out.get_mut(o)
|
||||
{
|
||||
*v = odd(o) || left_rtl;
|
||||
}
|
||||
}
|
||||
let mut right_rtl = false;
|
||||
for (o, c) in text.char_indices().rev() {
|
||||
if !is_number_inside(c) {
|
||||
right_rtl = odd(o);
|
||||
} else if right_rtl
|
||||
&& is_numeral_digit(c)
|
||||
&& let Some(v) = out.get_mut(o)
|
||||
{
|
||||
*v = true;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Map a byte range of numeral-substituted display text back to the original text.
|
||||
///
|
||||
/// `map_starts[i]` is the display byte offset of the `i`th char and `orig_of[i]` its original
|
||||
/// byte offset (`orig_of` ends with the original length as a sentinel). Substitution is
|
||||
/// 1 char → 1 char, so both ends land on char boundaries; anything else falls back to the
|
||||
/// end of the original text instead of panicking or slicing mid-character.
|
||||
pub(crate) fn remap_range(map_starts: &[usize], orig_of: &[usize], orig_len: usize, ms: usize, me: usize) -> (usize, usize) {
|
||||
let n = map_starts.len();
|
||||
let i = map_starts.partition_point(|&b| b < ms).min(n);
|
||||
let j = map_starts.partition_point(|&b| b < me).min(n);
|
||||
let s = orig_of.get(i).copied().unwrap_or(orig_len);
|
||||
let e = orig_of.get(j).copied().unwrap_or(orig_len);
|
||||
(s.min(e), s.max(e))
|
||||
}
|
||||
|
||||
/// Bidi embedding levels per byte of `text` (UAX #9 rules up to I2), or empty when the
|
||||
/// paragraph is left to right and has no right-to-left characters.
|
||||
pub fn bidi_levels(text: &str, rtl_para: bool) -> Vec<u8> {
|
||||
|
|
@ -517,6 +578,15 @@ fn east_asian(c: char) -> bool {
|
|||
}
|
||||
|
||||
impl<'a> Builder<'a> {
|
||||
/// Displayed form of `c` at run-relative byte `at` (Word › Numeral): one char in, one
|
||||
/// char out. `rtl` is the shaping direction; in Context mode `context` (from
|
||||
/// [`context_digit_dirs`], empty otherwise) gives each digit's surroundings, since European
|
||||
/// numbers take an even bidi level.
|
||||
fn numeral_char(&self, context: &[bool], at: usize, c: char, rtl: bool) -> char {
|
||||
let dir = if !context.is_empty() && is_numeral_digit(c) { context.get(at).copied().unwrap_or(false) } else { rtl };
|
||||
self.env.numeral.map(c, dir)
|
||||
}
|
||||
|
||||
/// Style index for a resolved char in `face` (caps-scaled `size` override for small caps).
|
||||
fn style(&mut self, rc: &Arc<ResolvedChar>, face_override: Option<FaceRef>, small: bool) -> u16 {
|
||||
let r = wordcraft_fonts::word::resolve(&rc.font, rc.bold, rc.italic);
|
||||
|
|
@ -595,6 +665,7 @@ impl<'a> Builder<'a> {
|
|||
.filter(|r| !r.substituted)
|
||||
.map(|r| r.face);
|
||||
let use_levels = para_text && !self.levels.is_empty();
|
||||
let context = if self.env.numeral == NumeralMode::Context && use_levels { context_digit_dirs(text, &self.levels, base) } else { Vec::new() };
|
||||
// One fallback face for the run's East Asian characters: the first face in fallback order
|
||||
// covering every character the primary font lacks, so the run draws in a single CJK face
|
||||
// instead of mixing faces per character (e.g. a bundled Japanese Mincho for JIS-covered
|
||||
|
|
@ -616,27 +687,31 @@ impl<'a> Builder<'a> {
|
|||
let mut cur: Option<SegKey> = None;
|
||||
let mut segs: Vec<(usize, usize, SegKey)> = Vec::new();
|
||||
for (i, c) in text.char_indices() {
|
||||
let rtl = use_levels && self.levels.get(base + i).is_some_and(|l| l % 2 == 1);
|
||||
// Displayed form (Word › Numeral): one char in, one char out. Coverage and the
|
||||
// fallback face are picked for the displayed char, so Hindi digits land in an
|
||||
// Arabic-capable face and Western digits in a Latin one.
|
||||
let dc = self.numeral_char(&context, i, c, rtl);
|
||||
let mark = is_mark(c);
|
||||
let complex = match cur {
|
||||
Some(k) if mark => k.complex,
|
||||
_ => rc_cs.is_some() && rc.uses_complex(c),
|
||||
_ => rc_cs.is_some() && rc.uses_complex(dc),
|
||||
};
|
||||
let prim = if complex { primary_cs.unwrap_or(primary) } else { primary };
|
||||
let face = match cur {
|
||||
Some(k) if mark => k.face,
|
||||
_ if !complex && east_asian(c) && primary_ea.is_some_and(|f| f.covers(c)) => primary_ea,
|
||||
_ if prim.covers(c) || c.is_whitespace() || c.is_control() || c == SOFT_HYPHEN => None,
|
||||
_ if prim.covers(dc) || c.is_whitespace() || c.is_control() || c == SOFT_HYPHEN => None,
|
||||
// Stay in the fallback face the word started in while it covers the letters.
|
||||
Some(SegKey { face: Some(f), .. }) if f.covers(c) => Some(f),
|
||||
Some(SegKey { face: Some(f), .. }) if f.covers(dc) => Some(f),
|
||||
// The run's unified East Asian face, when one face covers the whole run.
|
||||
_ if !complex && ea_face.is_some() && crate::kinsoku::is_east_asian(c) => ea_face,
|
||||
_ => {
|
||||
let (bold, italic) = if complex { (rc.bold_cs, rc.italic_cs) } else { (rc.bold, rc.italic) };
|
||||
wordcraft_fonts::FontDb::global().fallback_styled(c, prim.id(), bold, italic).map(|f| FaceRef::of(&f))
|
||||
wordcraft_fonts::FontDb::global().fallback_styled(dc, prim.id(), bold, italic).map(|f| FaceRef::of(&f))
|
||||
}
|
||||
};
|
||||
let small = rc.small_caps && !rc.caps && c.is_lowercase();
|
||||
let rtl = use_levels && self.levels.get(base + i).is_some_and(|l| l % 2 == 1);
|
||||
let k = SegKey { face, small, complex, rtl };
|
||||
match cur {
|
||||
Some(p) if p.same(&k) => {}
|
||||
|
|
@ -662,21 +737,38 @@ impl<'a> Builder<'a> {
|
|||
let si = self.style(rc, key.face, small);
|
||||
let Some(st) = self.styles.get(si as usize).cloned() else { continue };
|
||||
let upper = rc.caps || small;
|
||||
// Displayed text: numeral substitution is 1 char → 1 char, so codepoints stay
|
||||
// aligned with the document and only byte offsets shift; `orig_of` maps each
|
||||
// displayed char back to its original byte offset (+ a `sub.len()` sentinel).
|
||||
let substituted = sub.char_indices().any(|(i, c)| self.numeral_char(&context, a + i, c, key.rtl) != c);
|
||||
let mut mapped = String::new();
|
||||
let mut map_starts: Vec<usize> = Vec::new();
|
||||
let mut orig_of: Vec<usize> = Vec::new();
|
||||
if substituted {
|
||||
mapped = String::with_capacity(sub.len());
|
||||
for (i, c) in sub.char_indices() {
|
||||
map_starts.push(mapped.len());
|
||||
orig_of.push(i);
|
||||
mapped.push(self.numeral_char(&context, a + i, c, key.rtl));
|
||||
}
|
||||
orig_of.push(sub.len());
|
||||
}
|
||||
let shape_text = if substituted { mapped.as_str() } else { sub };
|
||||
let map = |c: char| if upper { c.to_uppercase().next().unwrap_or(c) } else { c };
|
||||
let shaped = if use_levels {
|
||||
wordcraft_fonts::shape_run(&st.face, sub, &[], map, key.rtl)
|
||||
wordcraft_fonts::shape_run(&st.face, shape_text, &[], map, key.rtl)
|
||||
} else {
|
||||
wordcraft_fonts::shape(&st.face, sub, &[], map)
|
||||
wordcraft_fonts::shape(&st.face, shape_text, &[], map)
|
||||
};
|
||||
let k = st.size / st.face.upem.max(1.0) as f32;
|
||||
let hscale = rc.scale / 100.0;
|
||||
// Group glyphs by cluster byte offset; graphemes may span several shaper clusters.
|
||||
// Complex-script syllables the graphemes split (Myanmar vowel signs, stacked
|
||||
// consonants) stay one cluster: the caret never stops inside them.
|
||||
let bounds: Vec<usize> = wordcraft_doc::cluster::cluster_starts(sub);
|
||||
let bounds: Vec<usize> = wordcraft_doc::cluster::cluster_starts(shape_text);
|
||||
let mut gi = 0usize;
|
||||
for (bi, &gs) in bounds.iter().enumerate() {
|
||||
let ge = bounds.get(bi + 1).copied().unwrap_or(sub.len());
|
||||
let ge = bounds.get(bi + 1).copied().unwrap_or(shape_text.len());
|
||||
let g0 = self.glyphs.len() as u32;
|
||||
let mut adv = 0.0f32;
|
||||
while let Some(g) = shaped.get(gi) {
|
||||
|
|
@ -688,7 +780,7 @@ impl<'a> Builder<'a> {
|
|||
gi += 1;
|
||||
}
|
||||
let g1 = self.glyphs.len() as u32;
|
||||
let s = sub.get(gs..ge).unwrap_or("");
|
||||
let s = shape_text.get(gs..ge).unwrap_or("");
|
||||
let ch = s.chars().next().unwrap_or(' ');
|
||||
let kind = kind_override.unwrap_or(if ch == ' ' || ch == '\u{3000}' {
|
||||
ClKind::Space
|
||||
|
|
@ -698,9 +790,11 @@ impl<'a> Builder<'a> {
|
|||
ClKind::Text
|
||||
});
|
||||
let adv = if kind == ClKind::Marker { 0.0 } else { adv + rc.spacing };
|
||||
// Clusters index the document text, never the displayed text.
|
||||
let (cs, ce) = if substituted { remap_range(&map_starts, &orig_of, sub.len(), gs, ge) } else { (gs, ge) };
|
||||
self.clusters.push(Cluster {
|
||||
start: base + a + gs,
|
||||
end: base + a + ge,
|
||||
start: base + a + cs,
|
||||
end: base + a + ce,
|
||||
adv,
|
||||
kind,
|
||||
style: si,
|
||||
|
|
|
|||
|
|
@ -168,6 +168,8 @@ pub fn measure_table_columns(doc: &wordcraft_doc::Document, t: &Table) -> Vec<(f
|
|||
hide_deleted: false,
|
||||
table: text.as_ref(),
|
||||
proofing: None,
|
||||
// Measurement uses a fixed environment (like show_hidden/proofing above).
|
||||
numeral: crate::NumeralMode::default(),
|
||||
exclusions: &[],
|
||||
eq_number: 0,
|
||||
grid: crate::para::Grid::default(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
//! Numeral display modes (Word › File › Options › Advanced › Numeral): Western digits,
|
||||
//! Hindi digits, or context-following digits — display only, the text is untouched.
|
||||
|
||||
use super::*;
|
||||
use wordcraft_doc::props::ParaProps;
|
||||
use wordcraft_doc::{Pos, StoryRef};
|
||||
|
||||
fn lay_opts(text: &str, rtl_para: bool, numeral: NumeralMode) -> DocLayout {
|
||||
let mut d = Document::from_text(text);
|
||||
if rtl_para {
|
||||
let at = Pos::body(0, 0);
|
||||
d.format_paragraphs(&at, &at, &|p: &mut ParaProps| p.bidi = Some(true)).unwrap();
|
||||
}
|
||||
let mut c = LayoutCache::new();
|
||||
layout(&d, &mut c, &LayoutOptions { numeral, ..LayoutOptions::default() })
|
||||
}
|
||||
|
||||
/// Glyph ids of the first line's clusters covering `text`, in cluster order.
|
||||
fn line_gids(l: &DocLayout, text: &str) -> Vec<Vec<u32>> {
|
||||
for p in &l.pages {
|
||||
for it in &p.items {
|
||||
if let Placed::Lines { story: StoryRef::Body, para, .. } = it {
|
||||
let line = ¶.lines[0];
|
||||
let mut out = Vec::new();
|
||||
for k in line.c0..line.c1 {
|
||||
let cl = ¶.clusters[k];
|
||||
assert!(cl.start <= cl.end && cl.end <= text.len(), "cluster indexes the original text");
|
||||
let gids: Vec<u32> = para.glyphs[cl.g0 as usize..cl.g1 as usize].iter().map(|g| g.gid).collect();
|
||||
out.push(gids);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("body paragraph not laid out");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_accepts_the_four_modes_any_case() {
|
||||
assert_eq!(NumeralMode::parse("arabic"), Some(NumeralMode::Arabic));
|
||||
assert_eq!(NumeralMode::parse("hindi"), Some(NumeralMode::Hindi));
|
||||
assert_eq!(NumeralMode::parse("context"), Some(NumeralMode::Context));
|
||||
assert_eq!(NumeralMode::parse("system"), Some(NumeralMode::System));
|
||||
assert_eq!(NumeralMode::parse("Hindi"), Some(NumeralMode::Hindi));
|
||||
assert_eq!(NumeralMode::parse("CONTEXT"), Some(NumeralMode::Context));
|
||||
assert_eq!(NumeralMode::parse("western"), None);
|
||||
assert_eq!(NumeralMode::parse(""), None);
|
||||
assert_eq!(NumeralMode::parse("hindi "), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_substitutes_digits_both_ways() {
|
||||
for d in '0'..='9' {
|
||||
let hindi = char::from_u32(0x660 + (d as u32 - '0' as u32)).unwrap();
|
||||
assert_eq!(NumeralMode::Hindi.map(d, false), hindi);
|
||||
assert_eq!(NumeralMode::Hindi.map(d, true), hindi, "Hindi mode ignores direction");
|
||||
assert_eq!(NumeralMode::Arabic.map(hindi, false), d);
|
||||
assert_eq!(NumeralMode::Arabic.map(hindi, true), d, "Arabic mode converts back both ways");
|
||||
assert_eq!(NumeralMode::Arabic.map(d, false), d);
|
||||
assert_eq!(NumeralMode::Context.map(d, true), hindi);
|
||||
assert_eq!(NumeralMode::Context.map(d, false), d);
|
||||
assert_eq!(NumeralMode::Context.map(hindi, true), hindi);
|
||||
assert_eq!(NumeralMode::Context.map(hindi, false), d);
|
||||
}
|
||||
// Letters, punctuation and Persian digits are never touched.
|
||||
for c in ['A', 'ب', '.', ' ', '(', '۰', '۹'] {
|
||||
for m in [NumeralMode::Arabic, NumeralMode::Hindi, NumeralMode::Context] {
|
||||
assert_eq!(m.map(c, false), c, "{m:?} leaves {c:?} alone (ltr)");
|
||||
assert_eq!(m.map(c, true), c, "{m:?} leaves {c:?} alone (rtl)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_uses_the_given_system_locale() {
|
||||
for m in [NumeralMode::Arabic, NumeralMode::Hindi, NumeralMode::Context] {
|
||||
assert_eq!(m.resolve(Some("ar-EG")), m, "only System is resolved");
|
||||
}
|
||||
for tag in ["ar", "ar-EG", "fa_IR.UTF-8", "ur-PK", "ckb-IQ", "AR-sa"] {
|
||||
assert_eq!(NumeralMode::System.resolve(Some(tag)), NumeralMode::Hindi, "{tag}");
|
||||
}
|
||||
for tag in ["en-US", "C", "", "tr_TR", "ary"] {
|
||||
assert_eq!(NumeralMode::System.resolve(Some(tag)), NumeralMode::Arabic, "{tag}");
|
||||
}
|
||||
assert_eq!(NumeralMode::System.resolve(None), NumeralMode::Arabic, "unknown locale: Western digits");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_mode_on_a_very_long_digit_run_stays_linear() {
|
||||
// Every digit used to scan back over all the digits and spaces before it (O(n²)).
|
||||
let text = format!("سنة {}", "1 ".repeat(100_000));
|
||||
let start = std::time::Instant::now();
|
||||
let l = lay_opts(&text, false, NumeralMode::Context);
|
||||
assert!(!l.pages.is_empty());
|
||||
assert!(start.elapsed() < std::time::Duration::from_secs(60), "took {:?}", start.elapsed());
|
||||
// The digits follow the Arabic word before them.
|
||||
let short = "سنة 1 2 3";
|
||||
assert_eq!(line_gids(&lay_opts(short, false, NumeralMode::Context), short), line_gids(&lay_opts(short, false, NumeralMode::Hindi), short));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hindi_mode_reshapes_digits_but_keeps_offsets() {
|
||||
let text = "Year 2024!";
|
||||
let latin = lay_opts(text, false, NumeralMode::Arabic);
|
||||
let hindi = lay_opts(text, false, NumeralMode::Hindi);
|
||||
let a = line_gids(&latin, text);
|
||||
let h = line_gids(&hindi, text);
|
||||
assert_eq!(a.len(), h.len(), "same clusters in both modes");
|
||||
assert!(!a.is_empty());
|
||||
// Letter clusters draw identical glyphs; digit clusters draw different ones.
|
||||
for (n, (ga, gh)) in a.iter().zip(h.iter()).enumerate() {
|
||||
let ch = text.chars().nth(n).unwrap();
|
||||
if ch.is_ascii_digit() {
|
||||
assert_ne!(ga, gh, "digit {ch:?} reshaped in Hindi mode");
|
||||
assert!(!gh.is_empty(), "Hindi digit draws something");
|
||||
} else {
|
||||
assert_eq!(ga, gh, "{ch:?} untouched by Hindi mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arabic_mode_converts_hindi_digits_back() {
|
||||
let text = "عام ٢٠٢٤";
|
||||
let hindi = lay_opts(text, true, NumeralMode::Hindi);
|
||||
let arabic = lay_opts(text, true, NumeralMode::Arabic);
|
||||
let h = line_gids(&hindi, text);
|
||||
let a = line_gids(&arabic, text);
|
||||
assert_eq!(h.len(), a.len());
|
||||
let mut changed = 0;
|
||||
for (n, (gh, ga)) in h.iter().zip(a.iter()).enumerate() {
|
||||
if text.chars().nth(n).unwrap() == '٢' {
|
||||
assert_ne!(gh, ga);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
assert!(changed > 0, "typed Hindi digits display Western in Arabic mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_mode_follows_run_direction() {
|
||||
// Right-to-left paragraph: digits behave like Hindi mode.
|
||||
let rtl = "سنة 2024";
|
||||
let ctx = line_gids(&lay_opts(rtl, true, NumeralMode::Context), rtl);
|
||||
let hin = line_gids(&lay_opts(rtl, true, NumeralMode::Hindi), rtl);
|
||||
assert_eq!(ctx, hin, "digits in RTL text show Hindi in Context mode");
|
||||
// Left-to-right paragraph: digits behave like Arabic mode.
|
||||
let ltr = "Year 2024";
|
||||
let ctx = line_gids(&lay_opts(ltr, false, NumeralMode::Context), ltr);
|
||||
let ara = line_gids(&lay_opts(ltr, false, NumeralMode::Arabic), ltr);
|
||||
assert_eq!(ctx, ara, "digits in LTR text show Western in Context mode");
|
||||
// Mixed paragraph: digits next to Arabic show Hindi even in a left-to-right paragraph.
|
||||
let mixed = "Year سنة 2024";
|
||||
let ctx = line_gids(&lay_opts(mixed, false, NumeralMode::Context), mixed);
|
||||
let hin = line_gids(&lay_opts(mixed, false, NumeralMode::Hindi), mixed);
|
||||
assert_eq!(ctx, hin, "digits by Arabic text show Hindi in Context mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remap_range_maps_display_bytes_to_original() {
|
||||
use crate::para::remap_range;
|
||||
// "a5ب": '5' (1 byte) displays as '٥' (2 bytes), so display offsets shift after it.
|
||||
let orig = "a5ب";
|
||||
let disp = "a٥ب";
|
||||
let map_starts: Vec<usize> = disp.char_indices().map(|(i, _)| i).collect();
|
||||
let orig_of: Vec<usize> = orig.char_indices().map(|(i, _)| i).collect();
|
||||
let mut orig_of = orig_of;
|
||||
orig_of.push(orig.len());
|
||||
// Display "a" [0,1) → original [0,1); "٥" [1,3) → original [1,2); "ب" [3,5) → [2,4).
|
||||
assert_eq!(remap_range(&map_starts, &orig_of, orig.len(), 0, 1), (0, 1));
|
||||
assert_eq!(remap_range(&map_starts, &orig_of, orig.len(), 1, 3), (1, 2));
|
||||
assert_eq!(remap_range(&map_starts, &orig_of, orig.len(), 3, 5), (2, 4));
|
||||
assert_eq!(remap_range(&map_starts, &orig_of, orig.len(), 0, 5), (0, 4));
|
||||
// Hostile inputs never panic and stay in range.
|
||||
assert_eq!(remap_range(&[], &[], 0, 0, 0), (0, 0));
|
||||
assert_eq!(remap_range(&map_starts, &orig_of, orig.len(), 99, 200), (4, 4));
|
||||
assert_eq!(remap_range(&map_starts, &orig_of, orig.len(), 3, 1), (1, 2));
|
||||
}
|
||||
|
|
@ -2048,3 +2048,8 @@
|
|||
Opens Settings › Default apps, where you choose WordCraft for .docx and the other document types. Otevře Nastavení › Výchozí aplikace, kde zvolíte WordCraft pro .docx a další typy dokumentů.
|
||||
To open Word documents with WordCraft by default: select one in Finder, choose File › Get Info, pick WordCraft under Open with, then click Change All. Chcete-li dokumenty Word otevírat ve WordCraftu jako výchozí: vyberte jeden ve Finderu, zvolte Soubor › Informace, v části „Otevřít v aplikaci“ vyberte WordCraft a klikněte na „Změnit vše“.
|
||||
WordCraft is now the default app for Word documents. WordCraft je nyní výchozí aplikací pro dokumenty Word.
|
||||
Numerals: Číslice:
|
||||
Numerals Číslice
|
||||
Hindi Hindské
|
||||
Context Podle kontextu
|
||||
System Systémové
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -2042,3 +2042,8 @@
|
|||
Opens Settings › Default apps, where you choose WordCraft for .docx and the other document types. Öffnet Einstellungen › Standard-Apps, wo Sie WordCraft für .docx und die anderen Dokumenttypen wählen.
|
||||
To open Word documents with WordCraft by default: select one in Finder, choose File › Get Info, pick WordCraft under Open with, then click Change All. Um Word-Dokumente standardmäßig mit WordCraft zu öffnen: Wählen Sie eines im Finder aus, wählen Sie Ablage › Informationen, wählen Sie WordCraft unter „Öffnen mit“ und klicken Sie auf „Alle ändern“.
|
||||
WordCraft is now the default app for Word documents. WordCraft ist jetzt die Standard-App für Word-Dokumente.
|
||||
Numerals: Ziffern:
|
||||
Numerals Ziffern
|
||||
Hindi Hindi
|
||||
Context Kontext
|
||||
System System
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -1648,6 +1648,12 @@
|
|||
Enlarge symbol Agrandar símbolo
|
||||
Enclosure Contorno
|
||||
Circle Círculo
|
||||
Numerals: Números:
|
||||
Numerals Números
|
||||
Arabic Árabe
|
||||
Hindi Hindi
|
||||
Context Contexto
|
||||
System Sistema
|
||||
Templates Plantillas
|
||||
Templates and Add-ins Plantillas y complementos
|
||||
Attach Template Adjuntar plantilla
|
||||
|
|
@ -1993,7 +1999,6 @@
|
|||
Add line numbering Agregar numeración de líneas
|
||||
Add to Document Agregar al documento
|
||||
Address labels Etiquetas de dirección
|
||||
Arabic Árabe
|
||||
Auto Auto
|
||||
Automatically hyphenate document Separar palabras automáticamente en el documento
|
||||
Chinese (Simplified) Chino (simplificado)
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -1579,6 +1579,12 @@
|
|||
Right (vertical) Paremal (vertikaalne)
|
||||
Ruby text: Ruby-tekst:
|
||||
Size (pt): Suurus (pt):
|
||||
Numerals: Numbrid:
|
||||
Numerals Numbrid
|
||||
Arabic Araabia
|
||||
Hindi Hindi
|
||||
Context Kontekst
|
||||
System Süsteem
|
||||
Couldn't make WordCraft the default: {error} WordCrafti ei õnnestunud vaikerakenduseks määrata: {error}
|
||||
In Default apps, choose WordCraft for the document types you want it to open. Vali jaotises „Vaikerakendused“ WordCraft nende dokumenditüüpide jaoks, mida see peaks avama.
|
||||
Make WordCraft the default for Word documents… Määra WordCraft Wordi dokumentide vaikerakenduseks…
|
||||
|
|
@ -1987,7 +1993,6 @@
|
|||
Add line numbering Lisa reanummerdus
|
||||
Add to Document Lisa dokumenti
|
||||
Address labels Aadressisildid
|
||||
Arabic Araabia
|
||||
Auto Automaatne
|
||||
Automatically hyphenate document Poolita dokumenti automaatselt
|
||||
Chinese (Simplified) Hiina (lihtsustatud)
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 12.
|
|
|
@ -2427,3 +2427,8 @@ preview Title Esempio
|
|||
Opens Settings › Default apps, where you choose WordCraft for .docx and the other document types. Apre Impostazioni › App predefinite, dove scegli WordCraft per .docx e gli altri tipi di documento.
|
||||
To open Word documents with WordCraft by default: select one in Finder, choose File › Get Info, pick WordCraft under Open with, then click Change All. Per aprire i documenti Word con WordCraft per impostazione predefinita: selezionane uno nel Finder, scegli Archivio › Ottieni informazioni, scegli WordCraft in «Apri con», quindi fai clic su «Modifica tutto».
|
||||
WordCraft is now the default app for Word documents. WordCraft è ora l'app predefinita per i documenti Word.
|
||||
Numerals: Cifre:
|
||||
Numerals Cifre
|
||||
Hindi Hindi
|
||||
Context Contesto
|
||||
System Sistema
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -1632,6 +1632,12 @@
|
|||
Enlarge symbol 記号を拡大
|
||||
Enclosure 囲い
|
||||
Circle 円
|
||||
Numerals: 数字:
|
||||
Numerals 数字
|
||||
Arabic アラビア
|
||||
Hindi ヒンディー
|
||||
Context コンテキスト
|
||||
System システム
|
||||
Templates テンプレート
|
||||
Templates and Add-ins テンプレートとアドイン
|
||||
Attach Template テンプレートの添付
|
||||
|
|
@ -1977,7 +1983,6 @@
|
|||
Add line numbering 行番号を追加する
|
||||
Add to Document 文書に追加
|
||||
Address labels 宛名ラベル
|
||||
Arabic アラビア語
|
||||
Auto 自動
|
||||
Automatically hyphenate document 文書を自動的にハイフネーションする
|
||||
Chinese (Simplified) 中国語(簡体字)
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 3.
|
|
|
@ -2060,3 +2060,8 @@
|
|||
Opens Settings › Default apps, where you choose WordCraft for .docx and the other document types. Otwiera Ustawienia › Aplikacje domyślne, gdzie wybierasz WordCraft dla .docx i innych typów dokumentów.
|
||||
To open Word documents with WordCraft by default: select one in Finder, choose File › Get Info, pick WordCraft under Open with, then click Change All. Aby otwierać dokumenty Word domyślnie w WordCraft: zaznacz jeden w Finderze, wybierz Plik › Pokaż info, wybierz WordCraft w „Otwórz za pomocą”, a następnie kliknij „Zmień wszystkie”.
|
||||
WordCraft is now the default app for Word documents. WordCraft jest teraz domyślną aplikacją dla dokumentów Word.
|
||||
Numerals: Cyfry:
|
||||
Numerals Cyfry
|
||||
Hindi Hindi
|
||||
Context Kontekstowe
|
||||
System Systemowe
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -1644,6 +1644,12 @@
|
|||
Enlarge symbol Ampliar símbolo
|
||||
Enclosure Contorno
|
||||
Circle Círculo
|
||||
Numerals: Numerais:
|
||||
Numerals Numerais
|
||||
Arabic Arábico
|
||||
Hindi Hindi
|
||||
Context Contexto
|
||||
System Sistema
|
||||
Templates Modelos
|
||||
Templates and Add-ins Modelos e suplementos
|
||||
Attach Template Anexar modelo
|
||||
|
|
@ -1989,7 +1995,6 @@
|
|||
Add line numbering Adicionar numeração de linhas
|
||||
Add to Document Adicionar ao documento
|
||||
Address labels Etiquetas de endereço
|
||||
Arabic Árabe
|
||||
Auto Auto
|
||||
Automatically hyphenate document Hifenizar o documento automaticamente
|
||||
Chinese (Simplified) Chinês (simplificado)
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -1998,6 +1998,12 @@ preview Title Пример
|
|||
Search icons Поиск значков
|
||||
No icons match your search. Нет значков, соответствующих поиску.
|
||||
Recolor a one-color picture, such as an icon Перекрасить одноцветный рисунок, например значок
|
||||
Numerals: Цифры:
|
||||
Numerals Цифры
|
||||
Arabic Арабские
|
||||
Hindi Хинди
|
||||
Context Контекст
|
||||
System Системные
|
||||
Couldn't make WordCraft the default: {error} Не удалось сделать WordCraft приложением по умолчанию: {error}
|
||||
In Default apps, choose WordCraft for the document types you want it to open. В разделе «Приложения по умолчанию» выберите WordCraft для нужных типов документов.
|
||||
Make WordCraft the default for Word documents… Сделать WordCraft приложением по умолчанию для документов Word…
|
||||
|
|
@ -2323,7 +2329,6 @@ preview Title Пример
|
|||
Add line numbering Добавить нумерацию строк
|
||||
Add to Document Добавить в документ
|
||||
Address labels Адресные наклейки
|
||||
Arabic Арабский
|
||||
Auto Авто
|
||||
Automatically hyphenate document Автоматически расставлять переносы в документе
|
||||
Chinese (Simplified) Китайский (упрощённый)
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
|
|
@ -1642,6 +1642,12 @@
|
|||
Enlarge symbol Uvećaj simbol
|
||||
Enclosure Okvir
|
||||
Circle Krug
|
||||
Numerals: Brojevi:
|
||||
Numerals Brojevi
|
||||
Arabic Arapski
|
||||
Hindi Hindi
|
||||
Context Kontekst
|
||||
System Sistem
|
||||
Templates Šabloni
|
||||
Templates and Add-ins Šabloni i dodaci
|
||||
Attach Template Priloži šablon
|
||||
|
|
@ -1987,7 +1993,6 @@
|
|||
Add line numbering Dodaj brojeve redova
|
||||
Add to Document Dodaj u dokument
|
||||
Address labels Adresne nalepnice
|
||||
Arabic Arapski
|
||||
Auto Automatski
|
||||
Automatically hyphenate document Automatski deli reči u dokumentu
|
||||
Chinese (Simplified) Kineski (pojednostavljeni)
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
|
|
@ -1646,6 +1646,12 @@
|
|||
Enlarge symbol Увећај симбол
|
||||
Enclosure Оквир
|
||||
Circle Круг
|
||||
Numerals: Бројеви:
|
||||
Numerals Бројеви
|
||||
Arabic Арапски
|
||||
Hindi Хинди
|
||||
Context Контекст
|
||||
System Систем
|
||||
Templates Шаблони
|
||||
Templates and Add-ins Шаблони и додаци
|
||||
Attach Template Приложи шаблон
|
||||
|
|
@ -1991,7 +1997,6 @@
|
|||
Add line numbering Додај бројеве редова
|
||||
Add to Document Додај у документ
|
||||
Address labels Адресне налепнице
|
||||
Arabic Арапски
|
||||
Auto Аутоматски
|
||||
Automatically hyphenate document Аутоматски дели речи у документу
|
||||
Chinese (Simplified) Кинески (поједностављени)
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 11 and column 25.
|
|
|
@ -1623,6 +1623,12 @@
|
|||
Search icons Simgelerde ara
|
||||
No icons match your search. Aramanızla eşleşen simge yok.
|
||||
Recolor a one-color picture, such as an icon Simge gibi tek renkli bir resmi yeniden renklendir
|
||||
Numerals: Rakamlar:
|
||||
Numerals Rakamlar
|
||||
Arabic Arapça
|
||||
Hindi Hintçe
|
||||
Context Bağlam
|
||||
System Sistem
|
||||
Couldn't make WordCraft the default: {error} WordCraft varsayılan yapılamadı: {error}
|
||||
In Default apps, choose WordCraft for the document types you want it to open. Varsayılan uygulamalar bölümünde, WordCraft'ın açmasını istediğiniz belge türlerini seçin.
|
||||
Make WordCraft the default for Word documents… WordCraft'ı Word belgeleri için varsayılan yap…
|
||||
|
|
@ -1945,7 +1951,6 @@
|
|||
Add line numbering Satır numaralandırma ekle
|
||||
Add to Document Belgeye Ekle
|
||||
Address labels Adres etiketleri
|
||||
Arabic Arapça
|
||||
Auto Otomatik
|
||||
Automatically hyphenate document Belgede otomatik heceleme yap
|
||||
Chinese (Simplified) Çince (Basitleştirilmiş)
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 18.
|
|
|
@ -1644,6 +1644,12 @@
|
|||
Enlarge symbol Збільшити символ
|
||||
Enclosure Обведення
|
||||
Circle Коло
|
||||
Numerals: Цифри:
|
||||
Numerals Цифри
|
||||
Arabic Арабські
|
||||
Hindi Гінді
|
||||
Context Контекст
|
||||
System Системні
|
||||
Templates Шаблони
|
||||
Templates and Add-ins Шаблони й надбудови
|
||||
Attach Template Приєднати шаблон
|
||||
|
|
@ -1989,7 +1995,6 @@
|
|||
Add line numbering Додати нумерацію рядків
|
||||
Add to Document Додати до документа
|
||||
Address labels Адресні етикетки
|
||||
Arabic Арабська
|
||||
Auto Авто
|
||||
Automatically hyphenate document Автоматично переносити слова в документі
|
||||
Chinese (Simplified) Китайська (спрощена)
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 31.
|
|
|
@ -1644,6 +1644,12 @@
|
|||
Enlarge symbol 增大圈号
|
||||
Enclosure 圈号
|
||||
Circle 圆形
|
||||
Numerals: 数字:
|
||||
Numerals 数字
|
||||
Arabic 阿拉伯
|
||||
Hindi 印地
|
||||
Context 上下文
|
||||
System 系统
|
||||
Templates 模板
|
||||
Templates and Add-ins 模板和加载项
|
||||
Attach Template 附加模板
|
||||
|
|
@ -1989,7 +1995,6 @@
|
|||
Add line numbering 添加行号
|
||||
Add to Document 添加到文档
|
||||
Address labels 地址标签
|
||||
Arabic 阿拉伯语
|
||||
Auto 自动
|
||||
Automatically hyphenate document 自动断字
|
||||
Chinese (Simplified) 中文(简体)
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
|
|
@ -1633,6 +1633,12 @@
|
|||
Enlarge symbol 放大符號
|
||||
Enclosure 圍繞符號
|
||||
Circle 圓形
|
||||
Numerals: 數字:
|
||||
Numerals 數字
|
||||
Arabic 阿拉伯
|
||||
Hindi 印地
|
||||
Context 上下文
|
||||
System 系統
|
||||
Templates 範本
|
||||
Templates and Add-ins 範本與增益集
|
||||
Attach Template 附加範本
|
||||
|
|
@ -1978,7 +1984,6 @@
|
|||
Add line numbering 新增行號
|
||||
Add to Document 新增至文件
|
||||
Address labels 地址標籤
|
||||
Arabic 阿拉伯文
|
||||
Auto 自動
|
||||
Automatically hyphenate document 自動斷字
|
||||
Chinese (Simplified) 中文(簡體)
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 4.
|
|
|
@ -404,6 +404,10 @@ impl SaveChoice {
|
|||
|
||||
impl WordApp {
|
||||
pub fn new(session: Session, services: Services) -> Self {
|
||||
// Numeral › System follows the system locale, found here rather than in layout.
|
||||
if let Some(tag) = sys_locale::get_locale() {
|
||||
wordcraft_engine::set_system_locale(&tag);
|
||||
}
|
||||
WordApp {
|
||||
session,
|
||||
ui: UiState::default(),
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ fn display(app: &mut WordApp, ui: &mut Ui) {
|
|||
if ui.checkbox(&mut ruler, tl!("Show rulers")).changed() {
|
||||
let _ = app.run("view.ruler", json!({"value": ruler}));
|
||||
}
|
||||
numeral_picker(app, ui);
|
||||
ui.add_space(8.0);
|
||||
section(ui, "Printing");
|
||||
ui.checkbox(&mut app.ui.print_backgrounds, tl!("Print the page color"));
|
||||
|
|
@ -504,6 +505,24 @@ fn default_app_button(app: &mut WordApp, ui: &mut Ui) {
|
|||
}
|
||||
}
|
||||
|
||||
/// File ▸ Options ▸ Numerals: how digits look on screen (display only, like Word's
|
||||
/// File › Options › Advanced › Numeral).
|
||||
fn numeral_picker(app: &mut WordApp, ui: &mut Ui) {
|
||||
use wordcraft_layout::NumeralMode;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Numerals:"));
|
||||
let mut sel = app.session.view.numeral;
|
||||
egui::ComboBox::from_id_salt("numerals").selected_text(tl!(sel.label())).width(220.0).show_ui(ui, |ui| {
|
||||
for m in [NumeralMode::Arabic, NumeralMode::Hindi, NumeralMode::Context, NumeralMode::System] {
|
||||
ui.selectable_value(&mut sel, m, tl!(m.label()));
|
||||
}
|
||||
});
|
||||
if sel != app.session.view.numeral {
|
||||
let _ = app.run("view.numeral", json!({ "value": sel }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
|
|
|
|||
Loading…
Reference in New Issue