diff --git a/README.md b/README.md index 7fb02d27..ce745eb2 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ The **Variables** tab adds a variable with a name, an `^FN` slot and a default v ### Fonts -The **Fonts** tab uploads TrueType fonts and assigns their `^CW` alias. +The **Fonts** tab uploads TrueType fonts and assigns their `^CW` alias. On the desktop the tab also offers the fonts installed on this computer. ### Pages diff --git a/packages/core/src/lib/customFonts.ts b/packages/core/src/lib/customFonts.ts index e341f9e5..5f640e17 100644 --- a/packages/core/src/lib/customFonts.ts +++ b/packages/core/src/lib/customFonts.ts @@ -63,7 +63,7 @@ export const ZPL_BUILTIN_FONT_IDS = [ ] as const; /** The printer path for a locally picked file: a real device, up to 8 name chars, and an extension - * ^A@ and ^CW can reference (spec p.63, p.168): .TTE stays, everything else is .TTF. */ + * ^A@ and ^CW can reference (spec p.63, p.168). .TTE stays, .OTF and the rest become .TTF (p.184). */ export function printerFontFileName(fileName: string): string | undefined { const first = fileName[0]?.toUpperCase() ?? ""; const typedDevice = fileName[1] === ":" && (STORAGE_DEVICES as readonly string[]).includes(first) ? `${first}:` : undefined; @@ -77,16 +77,20 @@ export function printerFontFileName(fileName: string): string | undefined { export type FontUploadIssue = "notAFont" | "nameUnusable" | "nameTaken"; -/** The gate a picked file passes before its bytes enter the cache; reads the file once. */ -export async function prepareFontUpload( - file: File, - typedName = "", -): Promise<{ ok: true; path: string; bytes: Uint8Array } | { ok: false; reason: FontUploadIssue }> { +export type FontNameIssue = Exclude; +export type PreparedFont = { ok: true; path: string; bytes: Uint8Array } | { ok: false; reason: FontNameIssue }; + +/** The gate a picked file passes before its bytes enter the cache. */ +export async function prepareFontUpload(file: File, typedName = ""): Promise { if (!isFontFile(file)) return { ok: false, reason: "notAFont" }; - const path = printerFontFileName(typedName.trim() || file.name); + return prepareFontBytes(file.name, new Uint8Array(await file.arrayBuffer()), typedName); +} + +/** The name and collision half of the gate, for bytes that arrive without a File. */ +export function prepareFontBytes(fileName: string, bytes: Uint8Array, typedName = ""): PreparedFont { + const path = printerFontFileName(typedName.trim() || fileName); if (!path) return { ok: false, reason: "nameUnusable" }; - const bytes = new Uint8Array(await file.arrayBuffer()); - // Two picked files can fold onto one printer name; a different file must not take the first one's row. + // Two picked files can fold onto one printer name. A different file must not take the first one's row. if (holdsOtherBytes(path, bytes)) return { ok: false, reason: "nameTaken" }; return { ok: true, path, bytes }; } diff --git a/packages/core/src/lib/fontCache.ts b/packages/core/src/lib/fontCache.ts index 376170d1..b66b766a 100644 --- a/packages/core/src/lib/fontCache.ts +++ b/packages/core/src/lib/fontCache.ts @@ -25,7 +25,10 @@ export const MAX_FONT_BYTES = 4 * 1024 * 1024; * grows, and the live ZPL view rebuilds the payload on every edit. */ export const EMBED_WARN_FONT_BYTES = 1024 * 1024; -const FONT_EXT_RE = /\.(ttf|otf|tte)$/i; +const FONT_EXTENSIONS = ["ttf", "otf", "tte"]; +const FONT_EXT_RE = new RegExp(`\\.(${FONT_EXTENSIONS.join("|")})$`, "i"); +/** The `accept` list of a font picker, so a picker never offers what the gate refuses. */ +export const FONT_FILE_ACCEPT = FONT_EXTENSIONS.flatMap((ext) => [`.${ext}`, `.${ext.toUpperCase()}`]).join(","); /** What `loadFontFile` will take, answerable before a byte is read. */ export const isFontFile = (file: File): boolean => FONT_EXT_RE.test(file.name) && file.size <= MAX_FONT_BYTES; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a18d5295..41c8e3c0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,7 @@ mod package; mod preview; mod print; mod scope; +mod system_fonts; mod transport; mod updates; mod usb; @@ -17,6 +18,7 @@ use tauri::Manager; pub fn run() { let builder = tauri::Builder::default() .manage(mcp::McpState::default()) + .manage(system_fonts::SystemFonts::default()) // In setup, not manage(): loading the persisted grants needs the app paths. .setup(|app| { let grants = scope::PathGrants::load(app.handle()); @@ -32,6 +34,8 @@ pub fn run() { usb::send_zpl_usb, usb::query_zpl_usb, usb::setup_usb_access, + system_fonts::list_system_fonts, + system_fonts::read_system_font, db::db_list_tables, db::db_fetch, db::db_set_password, diff --git a/src-tauri/src/system_fonts.rs b/src-tauri/src/system_fonts.rs new file mode 100644 index 00000000..bec61944 --- /dev/null +++ b/src-tauri/src/system_fonts.rs @@ -0,0 +1,402 @@ +//! A path leaves this module only through the listing, and the read serves only what the listing +//! named. The tables are read here, not by a font crate: the parser crate is unmaintained +//! (RUSTSEC-2026-0192) and three tables suffice. +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +use serde::Serialize; + +use crate::transport::blocking; + +/// The font cache's own cap, so the listing only offers rows the cache will take. +const MAX_FONT_BYTES: u64 = 4 * 1024 * 1024; + +/// The scan reads every font file, so one session keeps its result. +#[derive(Default)] +pub struct SystemFonts { + listing: Mutex>>, +} + +impl SystemFonts { + fn listed(&self) -> Result>, String> { + Ok(self.listing.lock().map_err(|e| e.to_string())?.clone()) + } + + fn keep(&self, fonts: Vec) -> Result<(), String> { + *self.listing.lock().map_err(|e| e.to_string())? = Some(fonts); + Ok(()) + } + + fn allows(&self, path: &str) -> Result { + let listing = self.listing.lock().map_err(|e| e.to_string())?; + Ok(allows(listing.as_deref().unwrap_or(&[]), path)) + } +} + +#[derive(Serialize, Clone, Debug, PartialEq)] +pub struct SystemFont { + pub family: String, + pub style: String, + pub path: String, + pub file_name: String, + pub bytes: u64, + pub restricted: bool, + pub variable: bool, +} + +#[tauri::command] +pub async fn list_system_fonts( + state: tauri::State<'_, SystemFonts>, +) -> Result, String> { + if let Some(fonts) = state.listed()? { + return Ok(fonts); + } + let fonts = blocking(scan).await?; + state.keep(fonts.clone())?; + Ok(fonts) +} + +#[tauri::command] +pub async fn read_system_font( + state: tauri::State<'_, SystemFonts>, + path: String, +) -> Result { + let allowed = state.allows(&path)?; + let bytes = blocking(move || read_listed(allowed, &path)).await??; + Ok(tauri::ipc::Response::new(bytes)) +} + +/// The listing keeps a restricted font for its row, but the license forbids serving its bytes. +fn allows(listed: &[SystemFont], path: &str) -> bool { + listed.iter().any(|f| !f.restricted && f.path == path) +} + +/// The file may have changed since the listing, so it is judged again. +fn read_listed(allowed: bool, path: &str) -> Result, String> { + if !allowed { + return Err("not listed".to_string()); + } + let mut bytes = Vec::new(); + std::fs::File::open(path) + .and_then(|f| f.take(MAX_FONT_BYTES + 1).read_to_end(&mut bytes)) + .map_err(|e| e.to_string())?; + if bytes.len() as u64 > MAX_FONT_BYTES { + return Err("too large".to_string()); + } + match read_face(&bytes) { + Some(face) if !face.restricted => Ok(bytes), + Some(_) => Err("restricted".to_string()), + None => Err("not a font".to_string()), + } +} + +/// The printer takes one face per file, so collections stay out. +fn single_face_file(path: &Path) -> bool { + matches!( + path + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.to_ascii_lowercase()) + .as_deref(), + Some("ttf") | Some("otf") | Some("tte") + ) +} + +fn font_dirs() -> Vec { + let home = || std::env::var_os("HOME").map(PathBuf::from); + let mut dirs = Vec::new(); + if cfg!(target_os = "windows") { + if let Some(root) = std::env::var_os("SYSTEMROOT") { + dirs.push(PathBuf::from(root).join("Fonts")); + } + if let Some(local) = std::env::var_os("LOCALAPPDATA") { + dirs.push(PathBuf::from(local).join("Microsoft/Windows/Fonts")); + } + } else if cfg!(target_os = "macos") { + dirs.extend( + [ + "/Library/Fonts", + "/System/Library/Fonts", + "/Network/Library/Fonts", + ] + .map(PathBuf::from), + ); + if let Some(home) = home() { + dirs.push(home.join("Library/Fonts")); + } + } else { + dirs.extend(["/usr/share/fonts", "/usr/local/share/fonts"].map(PathBuf::from)); + if let Some(home) = home() { + dirs.push(home.join(".fonts")); + dirs.push(home.join(".local/share/fonts")); + } + } + dirs +} + +fn walk(dir: &Path, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + // file_type does not follow links, so a symlink cycle cannot recurse forever. + if entry.file_type().is_ok_and(|t| t.is_dir()) { + walk(&path, out); + } else if single_face_file(&path) { + out.push(path); + } + } +} + +fn scan() -> Vec { + let mut files = Vec::new(); + for dir in font_dirs() { + walk(&dir, &mut files); + } + let mut out = Vec::new(); + for path in files { + let Ok(meta) = std::fs::metadata(&path) else { + continue; + }; + if meta.len() > MAX_FONT_BYTES { + continue; + } + let Ok(bytes) = std::fs::read(&path) else { + continue; + }; + let Some(face) = read_face(&bytes) else { + continue; + }; + out.push(SystemFont { + family: face.family, + style: face.style, + path: path.to_string_lossy().into_owned(), + file_name: path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(), + bytes: meta.len(), + restricted: face.restricted, + variable: face.variable, + }); + } + out.sort_by(|a, b| a.family.cmp(&b.family).then(a.style.cmp(&b.style))); + out +} + +struct Face { + family: String, + style: String, + restricted: bool, + variable: bool, +} + +fn u16_at(b: &[u8], at: usize) -> Option { + b.get(at..at + 2).map(|s| u16::from_be_bytes([s[0], s[1]])) +} + +fn u32_at(b: &[u8], at: usize) -> Option { + b.get(at..at + 4) + .map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]])) +} + +fn table<'a>(bytes: &'a [u8], tag: &[u8; 4]) -> Option<&'a [u8]> { + let count = u16_at(bytes, 4)? as usize; + (0..count).find_map(|i| { + let at = 12 + i * 16; + if bytes.get(at..at + 4)? != tag { + return None; + } + let offset = u32_at(bytes, at + 8)? as usize; + let length = u32_at(bytes, at + 12)? as usize; + bytes.get(offset..offset.checked_add(length)?) + }) +} + +/// Windows Unicode names first, Macintosh Roman as the fallback the oldest fonts carry. +fn name_string(name: &[u8], id: u16) -> Option { + let count = u16_at(name, 2)? as usize; + let strings = u16_at(name, 4)? as usize; + let mut mac = None; + for i in 0..count { + let rec = 6 + i * 12; + if u16_at(name, rec + 6)? != id { + continue; + } + let platform = u16_at(name, rec)?; + let encoding = u16_at(name, rec + 2)?; + let len = u16_at(name, rec + 8)? as usize; + let at = strings + u16_at(name, rec + 10)? as usize; + let data = name.get(at..at + len)?; + match (platform, encoding) { + (3, 1) | (3, 10) | (0, _) => { + let units: Vec = data + .chunks_exact(2) + .map(|c| u16::from_be_bytes([c[0], c[1]])) + .collect(); + return Some(String::from_utf16_lossy(&units)); + } + (1, 0) if mac.is_none() => mac = Some(data.iter().map(|&b| b as char).collect()), + _ => {} + } + } + mac +} + +/// OS/2 fsType: bit 1 forbids any embedding, bit 9 allows bitmaps only, and ~DY sends outlines. +const FSTYPE_RESTRICTED: u16 = 0x0002; +const FSTYPE_BITMAP_ONLY: u16 = 0x0200; + +fn read_face(bytes: &[u8]) -> Option { + let version = bytes.get(..4)?; + if version != [0, 1, 0, 0] && version != b"OTTO" && version != b"true" { + return None; + } + let name = table(bytes, b"name")?; + let family = name_string(name, 16).or_else(|| name_string(name, 1))?; + let style = name_string(name, 17) + .or_else(|| name_string(name, 2)) + .unwrap_or_else(|| "Regular".to_string()); + let fs_type = table(bytes, b"OS/2") + .and_then(|t| u16_at(t, 8)) + .unwrap_or(0); + Some(Face { + family, + style, + restricted: fs_type & (FSTYPE_RESTRICTED | FSTYPE_BITMAP_ONLY) != 0, + variable: table(bytes, b"fvar").is_some(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn listed(path: &str, restricted: bool) -> SystemFont { + SystemFont { + family: "Arial".into(), + style: "Regular".into(), + path: path.into(), + file_name: "arial.ttf".into(), + bytes: 1, + restricted, + variable: false, + } + } + + fn font(family: &str, style: &str, fs_type: u16, variable: bool) -> Vec { + let utf16 = |s: &str| { + s.encode_utf16() + .flat_map(u16::to_be_bytes) + .collect::>() + }; + let (fam, sty) = (utf16(family), utf16(style)); + let mut name = Vec::new(); + name.extend(0u16.to_be_bytes()); + name.extend(2u16.to_be_bytes()); + name.extend((6u16 + 2 * 12).to_be_bytes()); + for (id, data, offset) in [(1u16, &fam, 0u16), (2, &sty, fam.len() as u16)] { + for v in [3u16, 1, 0x0409, id, data.len() as u16, offset] { + name.extend(v.to_be_bytes()); + } + } + name.extend(&fam); + name.extend(&sty); + let mut os2 = vec![0u8; 78]; + os2[8..10].copy_from_slice(&fs_type.to_be_bytes()); + let mut tables: Vec<(&[u8; 4], Vec)> = vec![(b"OS/2", os2), (b"name", name)]; + if variable { + tables.push((b"fvar", vec![0u8; 16])); + } + let mut out = Vec::new(); + out.extend(0x0001_0000u32.to_be_bytes()); + out.extend((tables.len() as u16).to_be_bytes()); + out.extend([0u8; 6]); + let mut offset = 12 + tables.len() * 16; + for (tag, data) in &tables { + out.extend(*tag); + out.extend(0u32.to_be_bytes()); + out.extend((offset as u32).to_be_bytes()); + out.extend((data.len() as u32).to_be_bytes()); + offset += data.len(); + } + for (_, data) in &tables { + out.extend(data); + } + out + } + + #[test] + fn reads_names_and_flags_from_the_tables() { + let face = read_face(&font("Noto Sans", "Bold Italic", 0, true)).unwrap(); + assert_eq!( + (face.family.as_str(), face.style.as_str()), + ("Noto Sans", "Bold Italic") + ); + assert!(!face.restricted); + assert!(face.variable); + assert!( + read_face(&font("A", "B", FSTYPE_RESTRICTED, false)) + .unwrap() + .restricted + ); + assert!( + read_face(&font("A", "B", FSTYPE_BITMAP_ONLY, false)) + .unwrap() + .restricted + ); + assert!( + !read_face(&font("A", "B", 0x0008, false)) + .unwrap() + .restricted + ); + assert!(read_face(b"not a font at all").is_none()); + assert!(read_face(&[]).is_none()); + } + + #[test] + fn serves_the_listed_spelling_only() { + let fonts = [listed("C:/Windows/Fonts/arial.ttf", false)]; + assert!(allows(&fonts, "C:/Windows/Fonts/arial.ttf")); + assert!(!allows(&fonts, "C:/Windows/Fonts/../Fonts/arial.ttf")); + assert!(!allows(&fonts, "c:/windows/fonts/arial.ttf")); + assert!(!allows(&fonts, "C:/Windows/Fonts/other.ttf")); + assert!(!allows( + &[listed("C:/Windows/Fonts/arial.ttf", true)], + "C:/Windows/Fonts/arial.ttf" + )); + } + + #[test] + fn reads_only_when_allowed_and_within_the_cap() { + let dir = std::env::temp_dir().join(format!("zplab-font-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let file = dir.join("a.ttf"); + let open = font("A", "B", 0, false); + std::fs::write(&file, &open).unwrap(); + let path = file.to_string_lossy().into_owned(); + assert_eq!(read_listed(true, &path).unwrap(), open); + assert_eq!(read_listed(false, &path).unwrap_err(), "not listed"); + std::fs::write(&file, font("A", "B", FSTYPE_BITMAP_ONLY, false)).unwrap(); + assert_eq!(read_listed(true, &path).unwrap_err(), "restricted"); + std::fs::write(&file, b"font").unwrap(); + assert_eq!(read_listed(true, &path).unwrap_err(), "not a font"); + let big = dir.join("big.ttf"); + std::fs::write(&big, vec![0u8; (MAX_FONT_BYTES + 1) as usize]).unwrap(); + assert_eq!( + read_listed(true, &big.to_string_lossy()).unwrap_err(), + "too large" + ); + std::fs::remove_dir_all(&dir).unwrap(); + } + + #[test] + fn lists_single_face_files_only() { + assert!(single_face_file(Path::new("C:/Windows/Fonts/ARIAL.TTF"))); + assert!(single_face_file(Path::new("/usr/share/fonts/a.otf"))); + assert!(single_face_file(Path::new("/usr/share/fonts/a.tte"))); + assert!(!single_face_file(Path::new("/usr/share/fonts/a.ttc"))); + } +} diff --git a/src/components/Fonts/AddFontDialog.tsx b/src/components/Fonts/AddFontDialog.tsx new file mode 100644 index 00000000..98f8a2a7 --- /dev/null +++ b/src/components/Fonts/AddFontDialog.tsx @@ -0,0 +1,284 @@ +import { useEffect, useId, useRef, useState } from 'react'; +import { XMarkIcon } from '@heroicons/react/16/solid'; +import { FONT_FILE_ACCEPT, loadFontBytes } from '@zplab/core/lib/fontCache'; +import { prepareFontBytes, prepareFontUpload, printerFontFileName, type FontNameIssue } from '@zplab/core/lib/customFonts'; +import { useT } from '../../hooks/useT'; +import { useUpload } from '../../hooks/useUpload'; +import { isDesktopShell } from '../../lib/platform'; +import { systemFontFaceStyle } from '../../lib/fontFaceStyle'; +import { listSystemFonts, readSystemFont, type SystemFont } from '../../lib/systemFonts'; +import { ariaDisabledCls, inputCls, labelCls } from '../ui/formStyles'; +import { DialogShell } from '../ui/DialogShell'; + +interface AddFontDialogProps { + /** `uploadedPath` is the stored printer path on success, undefined when the dialog closes without adding. */ + onDone: (uploadedPath?: string) => void; +} + +/** Picked but not yet cached. The gate runs on Add, so a rejected name stays editable. */ +type FontCandidate = File | SystemFont; + +const candidateFileName = (candidate: FontCandidate) => (candidate instanceof File ? candidate.name : candidate.file_name); + +/** aria-disabled, not disabled: a locked control keeps focus, so the dialog's own key handler still sees Escape. */ +const lockedBtnCls = `px-3 py-1.5 rounded text-xs font-mono transition-colors ${ariaDisabledCls}`; +const secondaryBtnCls = `${lockedBtnCls} whitespace-nowrap border`; + +/** Modal because the installed-fonts list needs more room than the sidebar gives. */ +export function AddFontDialog({ onDone }: AddFontDialogProps) { + const t = useT(); + const fileRef = useRef(null); + // Closing abandons a pending read, so its result must not reach the cache. + const aborted = useRef(false); + const [candidate, setCandidate] = useState(null); + const [name, setName] = useState(''); + const titleId = useId(); + const listLabelId = useId(); + const issueId = useId(); + const { busy, issue, start: addFont } = useUpload<'error' | FontNameIssue, FontCandidate>(async (input) => { + const prepared = + input instanceof File ? await prepareFontUpload(input, name) : prepareFontBytes(input.file_name, await readSystemFont(input.path), name); + if (aborted.current) return null; + if (!prepared.ok) return prepared.reason === 'notAFont' ? 'error' : prepared.reason; + await loadFontBytes(prepared.bytes, prepared.path); + onDone(prepared.path); + return null; + }, 'error'); + + const select = (next: FontCandidate) => { + setCandidate(next); + const fileName = candidateFileName(next); + setName(printerFontFileName(fileName) ?? fileName); + }; + + const close = () => { + aborted.current = true; + onDone(); + }; + + const canAdd = !busy && candidate !== null && name.trim() !== ''; + const submit = () => { + if (canAdd && candidate) addFont(candidate); + }; + const nameRejected = issue !== null && issue !== 'error'; + + return ( + +
+

+ {t.fonts.addFont} +

+ +
+ +
{ + e.preventDefault(); + submit(); + }} + > +
+ {isDesktopShell && ( +
+

+ {t.fonts.fromComputer} +

+ +
+ )} + + { + const file = e.target.files?.[0]; + if (file) select(file); + e.target.value = ''; + }} + /> + +
+ + {candidate instanceof File && ( + + {candidate.name} + + )} +
+ +
+ + setName(e.target.value)} + readOnly={busy} + aria-disabled={busy} + aria-invalid={nameRejected || undefined} + aria-describedby={issue ? issueId : undefined} + autoFocus={!isDesktopShell} + /> + {issue && ( + + )} +
+
+ +
+ + +
+
+
+ ); +} + +// ── SystemFontList ───────────────────────────────────────────────────────────── + +function SystemFontList({ + labelId, + busy, + selectedPath, + onSelect, +}: { + labelId: string; + busy: boolean; + selectedPath: string | undefined; + onSelect: (font: SystemFont) => void; +}) { + const t = useT(); + const [fonts, setFonts] = useState(null); + const [failed, setFailed] = useState(false); + const [query, setQuery] = useState(''); + useEffect(() => { + let current = true; + listSystemFonts().then( + (list) => current && setFonts(list), + () => current && setFailed(true), + ); + return () => { + current = false; + }; + }, []); + const shown = (fonts ?? []).filter((f) => `${f.family} ${f.style}`.toLowerCase().includes(query.trim().toLowerCase())); + const selectedIndex = shown.findIndex((f) => f.path === selectedPath); + + const moveSelection = (list: HTMLElement, step: number) => { + const from = selectedIndex < 0 ? (step > 0 ? -1 : shown.length) : selectedIndex; + for (let i = from + step; i >= 0 && i < shown.length; i += step) { + const next = shown[i]; + if (!next || next.restricted) continue; + onSelect(next); + list.querySelectorAll('[role="option"]')[i]?.focus(); + return; + } + }; + + return ( +
{ + if (busy || (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')) return; + e.preventDefault(); + moveSelection(e.currentTarget, e.key === 'ArrowDown' ? 1 : -1); + }} + > + setQuery(e.target.value)} + aria-label={t.fonts.filterFonts} + readOnly={busy} + aria-disabled={busy} + autoFocus + /> + {fonts === null && !failed &&

…

} + {fonts !== null && fonts.length === 0 &&

{t.fonts.noSystemFonts}

} + {fonts !== null && fonts.length > 0 && shown.length === 0 &&

{t.fonts.noFilterMatch}

} + {failed &&

{t.fonts.systemFontsFailed}

} +
+ {shown.map((font, i) => { + const selected = font.path === selectedPath; + return ( + // One tab stop for the whole list: Tab reaches the name field without walking every row. + + ); + })} +
+
+ ); +} diff --git a/src/components/Fonts/FontManager.systemFonts.test.tsx b/src/components/Fonts/FontManager.systemFonts.test.tsx new file mode 100644 index 00000000..f6c8a065 --- /dev/null +++ b/src/components/Fonts/FontManager.systemFonts.test.tsx @@ -0,0 +1,191 @@ +// @vitest-environment jsdom +import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; +import { render, cleanup, fireEvent, act, waitFor, within } from "@testing-library/react"; +import { FontManager } from "./FontManager"; +import { useLabelStore } from "../../store/labelStore"; +import { cachedFontPath, getAllFonts, removeFont } from "@zplab/core/lib/fontCache"; +import { fallbackTranslations as en } from "../../locales"; + +vi.mock("../../lib/platform", async (importOriginal) => ({ ...(await importOriginal>()), isDesktopShell: true })); +const readSystemFont = vi.fn(); +vi.mock("../../lib/systemFonts", () => ({ + listSystemFonts: vi.fn().mockResolvedValue([ + { family: "Arial", style: "Regular", path: "C:\\Windows\\Fonts\\arial.ttf", file_name: "arial.ttf", bytes: 1024, restricted: false, variable: false }, + { family: "Noto Sans", style: "Bold", path: "C:\\Windows\\Fonts\\NotoSans-Bold.ttf", file_name: "NotoSans-Bold.ttf", bytes: 1024, restricted: false, variable: false }, + { family: "Noto Sans", style: "Regular", path: "C:\\Windows\\Fonts\\NotoSans-Regular.ttf", file_name: "NotoSans-Regular.ttf", bytes: 1024, restricted: false, variable: false }, + { family: "Locked", style: "Bold", path: "C:\\Windows\\Fonts\\locked.ttf", file_name: "locked.ttf", bytes: 2048, restricted: true, variable: false }, + ]), + readSystemFont: (path: string) => readSystemFont(path), +})); + +beforeEach(() => { + readSystemFont.mockReset().mockResolvedValue(new Uint8Array([0, 1, 0, 0])); + act(() => useLabelStore.setState({ pages: [{ objects: [] }], label: { widthMm: 70, heightMm: 40, dpmm: 8 }, sourceEdit: { status: "off" } })); +}); + +afterEach(() => { + cleanup(); + for (const f of getAllFonts()) removeFont(cachedFontPath(f)); +}); + +const open = (r: ReturnType) => fireEvent.click(r.getByText(en.fonts.addFont)); +const add = (r: ReturnType) => + within(r.getByRole("dialog")).getByRole("button", { name: en.fonts.addFont }) as HTMLButtonElement; +const nameField = (r: ReturnType) => r.getByPlaceholderText(en.fonts.printerFilenamePlaceholder) as HTMLInputElement; + +describe("FontManager installed fonts", () => { + it("draws each row in its own face", async () => { + const r = render(); + open(r); + await waitFor(() => expect(r.getByText("Arial")).toBeTruthy()); + expect(r.getByText("Arial").style.fontFamily).toBe('"Arial"'); + }); + + it("fills the printer filename from the picked row, then caches it on Add", async () => { + const r = render(); + open(r); + await waitFor(() => expect(r.getByText("Arial")).toBeTruthy()); + fireEvent.click(r.getByText("Arial")); + expect(nameField(r).value).toBe("E:ARIAL.TTF"); + expect(readSystemFont).not.toHaveBeenCalled(); + await act(async () => { + fireEvent.click(add(r)); + }); + expect(readSystemFont).toHaveBeenCalledWith("C:\\Windows\\Fonts\\arial.ttf"); + expect(getAllFonts().map(cachedFontPath)).toEqual(["E:ARIAL.TTF"]); + }); + + it("keeps a font with a restricted license out of reach", async () => { + const r = render(); + open(r); + await waitFor(() => expect(r.getByText("Locked")).toBeTruthy()); + const row = r.getByText("Locked").closest("button") as HTMLButtonElement; + expect(row.getAttribute("aria-disabled")).toBe("true"); + expect(row.title).toBe(en.fonts.restrictedLicense); + await act(async () => { + fireEvent.click(row); + }); + expect(nameField(r).value).toBe(""); + expect(add(r).getAttribute("aria-disabled")).toBe("true"); + expect(readSystemFont).not.toHaveBeenCalled(); + }); + + it("takes an edited printer filename over the one derived from the row", async () => { + const r = render(); + open(r); + await waitFor(() => expect(r.getByText("Arial")).toBeTruthy()); + fireEvent.click(r.getByText("Arial")); + expect(nameField(r).value).toBe("E:ARIAL.TTF"); + fireEvent.change(nameField(r), { target: { value: "R:LOGO" } }); + await act(async () => { + fireEvent.click(add(r)); + }); + expect(getAllFonts().map(cachedFontPath)).toEqual(["R:LOGO.TTF"]); + }); + + it("refuses a second style whose stem folds onto a cached printer name, and takes it under an edited name", async () => { + const r = render(); + open(r); + await waitFor(() => expect(r.getAllByText("Noto Sans")).toHaveLength(2)); + fireEvent.click(r.getAllByText("Noto Sans")[0] as HTMLElement); + await act(async () => { + fireEvent.click(add(r)); + }); + expect(getAllFonts().map(cachedFontPath)).toEqual(["E:NOTOSANS.TTF"]); + readSystemFont.mockResolvedValue(new Uint8Array([0, 1, 0, 1])); + open(r); + await waitFor(() => expect(r.getAllByText("Noto Sans")).toHaveLength(2)); + fireEvent.click(r.getAllByText("Noto Sans")[1] as HTMLElement); + await act(async () => { + fireEvent.click(add(r)); + }); + expect(r.getByText(en.fonts.nameTaken)).toBeTruthy(); + expect(getAllFonts().map(cachedFontPath)).toEqual(["E:NOTOSANS.TTF"]); + fireEvent.change(nameField(r), { target: { value: "E:NOTOBOLD.TTF" } }); + await act(async () => { + fireEvent.click(add(r)); + }); + expect(getAllFonts().map(cachedFontPath).sort()).toEqual(["E:NOTOBOLD.TTF", "E:NOTOSANS.TTF"]); + }); + + it("locks the form while a read is still landing, yet keeps every control focusable for Escape", async () => { + let finish: (bytes: Uint8Array) => void = () => undefined; + readSystemFont.mockReturnValue(new Promise((resolve) => (finish = resolve))); + const r = render(); + open(r); + await waitFor(() => expect(r.getByText("Arial")).toBeTruthy()); + fireEvent.click(r.getByText("Arial")); + expect(add(r).getAttribute("aria-disabled")).toBe("false"); + await act(async () => { + fireEvent.click(add(r)); + }); + expect(add(r).getAttribute("aria-disabled")).toBe("true"); + expect(nameField(r).readOnly).toBe(true); + expect(r.getByRole("dialog").querySelector(":disabled")).toBeNull(); + await act(async () => { + finish(new Uint8Array([0, 1, 0, 0])); + }); + expect(getAllFonts().map(cachedFontPath)).toEqual(["E:ARIAL.TTF"]); + }); + + it("closes on Escape while a read is pending and drops the late result", async () => { + let finish: (bytes: Uint8Array) => void = () => undefined; + readSystemFont.mockReturnValue(new Promise((resolve) => (finish = resolve))); + const r = render(); + open(r); + await waitFor(() => expect(r.getByText("Arial")).toBeTruthy()); + fireEvent.click(r.getByText("Arial")); + await act(async () => { + fireEvent.click(add(r)); + }); + fireEvent.keyDown(r.getByRole("dialog"), { key: "Escape" }); + expect(r.queryByRole("dialog")).toBeNull(); + await act(async () => { + finish(new Uint8Array([0, 1, 0, 0])); + }); + expect(getAllFonts()).toHaveLength(0); + }); + + it("refuses to add when the printer filename is only blanks", async () => { + const r = render(); + open(r); + await waitFor(() => expect(r.getByText("Arial")).toBeTruthy()); + fireEvent.click(r.getByText("Arial")); + fireEvent.change(nameField(r), { target: { value: " " } }); + expect(add(r).getAttribute("aria-disabled")).toBe("true"); + await act(async () => { + fireEvent.click(add(r)); + }); + expect(readSystemFont).not.toHaveBeenCalled(); + expect(getAllFonts()).toHaveLength(0); + }); + + it("moves the selection with the arrow keys, also from the filter field, and adds on submitting the name field", async () => { + const r = render(); + open(r); + await waitFor(() => expect(r.getByText("Arial")).toBeTruthy()); + const list = r.getByRole("listbox"); + fireEvent.keyDown(r.getByLabelText(en.fonts.filterFonts), { key: "ArrowDown" }); + expect(nameField(r).value).toBe("E:ARIAL.TTF"); + expect(r.getAllByRole("option")[0]?.getAttribute("aria-selected")).toBe("true"); + fireEvent.keyDown(list, { key: "ArrowDown" }); + expect(nameField(r).value).toBe("E:NOTOSANS.TTF"); + fireEvent.keyDown(list, { key: "ArrowUp" }); + expect(nameField(r).value).toBe("E:ARIAL.TTF"); + await act(async () => { + fireEvent.submit(nameField(r)); + }); + expect(getAllFonts().map(cachedFontPath)).toEqual(["E:ARIAL.TTF"]); + }); + + it("narrows the list with the filter", async () => { + const r = render(); + open(r); + await waitFor(() => expect(r.getByText("Arial")).toBeTruthy()); + fireEvent.change(r.getByLabelText(en.fonts.filterFonts), { target: { value: "lock" } }); + expect(r.queryByText("Arial")).toBeNull(); + expect(r.getByText("Locked")).toBeTruthy(); + fireEvent.change(r.getByLabelText(en.fonts.filterFonts), { target: { value: "zzz" } }); + expect(r.getByText(en.fonts.noFilterMatch)).toBeTruthy(); + }); +}); diff --git a/src/components/Fonts/FontManager.test.tsx b/src/components/Fonts/FontManager.test.tsx index 4af559f2..93af1928 100644 --- a/src/components/Fonts/FontManager.test.tsx +++ b/src/components/Fonts/FontManager.test.tsx @@ -22,12 +22,16 @@ afterEach(() => { for (const f of getAllFonts()) removeFont(cachedFontPath(f)); }); -const pick = async (r: ReturnType, file: File) => { +const addFile = async (r: ReturnType, file: File) => { fireEvent.click(r.getByText("Add font")); - const input = r.container.querySelector('input[type="file"]') as HTMLInputElement; + // The dialog is portaled, so the hidden input sits outside `container`. + const input = r.baseElement.querySelector('input[type="file"]') as HTMLInputElement; await act(async () => { fireEvent.change(input, { target: { files: [file] } }); }); + await act(async () => { + fireEvent.click(within(r.getByRole("dialog")).getByRole("button", { name: "Add font" })); + }); }; const deleteButton = (r: ReturnType) => r.getByLabelText("Delete") as HTMLButtonElement; @@ -124,6 +128,43 @@ describe("FontManager delete owners", () => { }); }); +describe("FontManager preview face", () => { + it("draws a cached font's name in the face the cache registered", () => { + const r = render(); + expect(r.getByTitle("E:ARIAL.TTF").style.fontFamily).toMatch(/^"zpl-/); + }); + + it("names a font no browser face took, and leaves its row unstyled", async () => { + const real = globalThis.FontFace; + Object.defineProperty(globalThis, "FontFace", { + configurable: true, + value: class { + load() { + return Promise.reject(new Error("rejected")); + } + }, + }); + try { + await loadFontBytes(new Uint8Array([0, 1, 0, 0, 9]), "E:BROKEN.TTF"); + } finally { + Object.defineProperty(globalThis, "FontFace", { configurable: true, value: real }); + } + const r = render(); + expect(r.getByText(/cannot render this font/)).toBeTruthy(); + expect(r.getByTitle("E:BROKEN.TTF").style.fontFamily).toBe(""); + }); +}); + +describe("FontManager on the web", () => { + it("offers no installed-fonts list, the file being the only source", () => { + const r = render(); + fireEvent.click(r.getByText("Add font")); + expect(r.queryByLabelText("Filter fonts")).toBeNull(); + expect(r.queryByRole("listbox")).toBeNull(); + expect(within(r.getByRole("dialog")).getByText("Upload")).toBeTruthy(); + }); +}); + describe("FontManager manual mappings", () => { it("names a mapping that promises an embed but has no bytes to ship", () => { act(() => useLabelStore.setState({ label: { widthMm: 70, heightMm: 40, dpmm: 8, customFonts: [{ alias: "M", path: "E:GONE.TTF", embedInZpl: true }] } })); @@ -215,21 +256,21 @@ describe("FontManager alias", () => { describe("FontManager upload", () => { it("refuses a file whose name folds onto a cached font holding other bytes", async () => { const r = render(); - await pick(r, new File(["other bytes"], "arial.ttf")); - expect(r.getByText(/Rename the file/)).toBeTruthy(); + await addFile(r, new File(["other bytes"], "arial.ttf")); + expect(r.getByText(/different printer filename/)).toBeTruthy(); expect(getAllFonts()).toHaveLength(1); }); it("refuses a file whose name leaves no printer name", async () => { const r = render(); - await pick(r, new File(["x"], "日本.ttf")); + await addFile(r, new File(["x"], "日本.ttf")); expect(r.getByText(/leaves no printer name/)).toBeTruthy(); expect(getAllFonts()).toHaveLength(1); }); it("stores a picked file under the .TTF printer name ^CW can reference", async () => { const r = render(); - await pick(r, new File(["x"], "My Logo.otf")); + await addFile(r, new File(["x"], "My Logo.otf")); expect(getAllFonts().map((f) => f.name).sort()).toEqual(["E:ARIAL.TTF", "E:MYLOGO.TTF"]); }); }); diff --git a/src/components/Fonts/FontManager.tsx b/src/components/Fonts/FontManager.tsx index a4ecff4f..4f61154d 100644 --- a/src/components/Fonts/FontManager.tsx +++ b/src/components/Fonts/FontManager.tsx @@ -1,8 +1,7 @@ -import { useRef, useState, type FocusEvent } from 'react'; +import { useState, type FocusEvent } from 'react'; import { PlusIcon, TrashIcon, InformationCircleIcon } from '@heroicons/react/16/solid'; import { hasFontBytes, - loadFontBytes, removeFont, getFontFamily, isEmbedLarge, @@ -11,7 +10,7 @@ import { import { useCachedFonts } from '../../hooks/useCachedFonts'; import { useLabelStore, useFileDeletability, forgetHistoryUsing, type DeleteBlock } from '../../store/labelStore'; import { useT } from '../../hooks/useT'; -import { useUpload } from '../../hooks/useUpload'; +import { cachedFontFaceStyle } from '../../lib/fontFaceStyle'; import { storageKey, storageRefMatchesPath } from '@zplab/core/lib/storagePath'; import { listsStoredFont } from '@zplab/core/lib/storedObjects'; import { @@ -19,12 +18,11 @@ import { isBuiltinFontId, nextFreeAlias, normalizeAlias, - prepareFontUpload, - type FontUploadIssue, upsertCustomFontMapping, } from '@zplab/core/lib/customFonts'; -import { inputCls, labelCls } from '../Properties/styles'; +import { inputCls } from '../ui/formStyles'; import { CollapsibleSection } from '../ui/CollapsibleSection'; +import { AddFontDialog } from './AddFontDialog'; import { ConfirmDialog } from '../ui/ConfirmDialog'; import { Tooltip } from '../ui/Tooltip'; import type { CustomFontMapping } from '@zplab/core/types/LabelConfig'; @@ -152,7 +150,7 @@ export function FontManager() { duplicate={isDuplicateAlias(alias)} delivery={fontDelivery(entry, path, setupFonts)} embedLarge={isEmbedLarge(path)} - previewMissing={!getFontFamily(path)} + previewFamily={getFontFamily(path)} blockedBy={deletability(path).blockedBy} listed={listsStoredFont(path, setupFonts)} onAliasChange={(v) => setAliasForPath(path, v)} @@ -163,8 +161,12 @@ export function FontManager() { })} - {adding ? ( - setAdding(true)}> + + + {t.fonts.addFont} + + {adding && ( + { // Auto-assign the next free alias when the upload succeeds. // Closes the "what now?" gap between the upload finishing @@ -182,11 +184,6 @@ export function FontManager() { setAdding(false); }} /> - ) : ( - )} void; @@ -258,7 +254,7 @@ function FontEntry({ duplicate, delivery, embedLarge, - previewMissing, + previewFamily, blockedBy, listed, onAliasChange, @@ -280,6 +276,7 @@ function FontEntry({
{name} @@ -335,7 +332,7 @@ function FontEntry({ {t.fonts.builtinAliasWarning}

)} - {previewMissing && ( + {!previewFamily && (

{t.fonts.faceRejected}

)} {embedLarge && delivery === 'job' && ( @@ -456,74 +453,3 @@ function ManualMappingsSection({
); } - -// ── AddFontForm ──────────────────────────────────────────────────────────────── - -interface AddFontFormProps { - /** `uploadedPath` is the stored printer path on success, undefined on cancel or failure. */ - onDone: (uploadedPath?: string) => void; -} - -function AddFontForm({ onDone }: AddFontFormProps) { - const t = useT(); - const fileRef = useRef(null); - const [name, setName] = useState(''); - const { busy: uploading, issue: uploadIssue, start: uploadFile } = useUpload<'error' | Exclude>(async (file) => { - // The typed name wins. Otherwise the picked file's own name is almost always the intended one. - const prepared = await prepareFontUpload(file, name); - if (!prepared.ok) return prepared.reason === 'notAFont' ? 'error' : prepared.reason; - await loadFontBytes(prepared.bytes, prepared.path); - onDone(prepared.path); - return null; - }, 'error'); - - return ( -
-
- - setName(e.target.value)} - autoFocus - /> -
- - { - const file = e.target.files?.[0]; - if (file) uploadFile(file); - e.target.value = ''; - }} - /> - - {uploadIssue && ( -

{{ error: t.fonts.uploadError, nameTaken: t.fonts.nameTaken, nameUnusable: t.fonts.nameUnusable }[uploadIssue]}

- )} - -
- - -
-
- ); -} diff --git a/src/components/PrinterSettings/StoredFontsTab.test.tsx b/src/components/PrinterSettings/StoredFontsTab.test.tsx index b6e20459..bd29bf78 100644 --- a/src/components/PrinterSettings/StoredFontsTab.test.tsx +++ b/src/components/PrinterSettings/StoredFontsTab.test.tsx @@ -151,7 +151,7 @@ describe("StoredFontsTab", () => { fireEvent.change(getByLabelText(/Upload font/), { target: { files: [new File(["other bytes"], "arial.ttf")] } }); }); expect(getAllFonts()).toHaveLength(1); - expect(getByText(/Rename the file/)).toBeTruthy(); + expect(getByText(/different printer filename/)).toBeTruthy(); expect(useLabelStore.getState().printerProfile.setupFonts).toBeUndefined(); }); diff --git a/src/components/PrinterSettings/StoredFontsTab.tsx b/src/components/PrinterSettings/StoredFontsTab.tsx index 9f54a3e7..14d7cb05 100644 --- a/src/components/PrinterSettings/StoredFontsTab.tsx +++ b/src/components/PrinterSettings/StoredFontsTab.tsx @@ -1,7 +1,7 @@ import { useRef, useState } from "react"; import { useT } from "../../hooks/useT"; import { Tooltip } from "../ui/Tooltip"; -import { cachedFontPath, loadFontBytes } from "@zplab/core/lib/fontCache"; +import { FONT_FILE_ACCEPT, cachedFontPath, loadFontBytes } from "@zplab/core/lib/fontCache"; import { findSetupEntry, withSetupEntry, withoutSetupEntry } from "@zplab/core/lib/setupEntries"; import { isTrueTypeFileName, prepareFontUpload, printerFontFileName, type FontUploadIssue } from "@zplab/core/lib/customFonts"; import { listsStoredFont } from "@zplab/core/lib/storedObjects"; @@ -56,7 +56,7 @@ export function StoredFontsTab() { setRepairTarget(target); fileRef.current?.click(); }; - const { busy: uploading, issue: uploadIssue, start: uploadFile } = useUpload<"error" | "refused" | FontUploadIssue, [target?: string]>(async (file, target) => { + const { busy: uploading, issue: uploadIssue, start: uploadFile } = useUpload<"error" | "refused" | FontUploadIssue, File, [target?: string]>(async (file, target) => { const prepared = await prepareFontUpload(file, target); if (!prepared.ok) return prepared.reason; await loadFontBytes(prepared.bytes, prepared.path); @@ -77,7 +77,7 @@ export function StoredFontsTab() { { diff --git a/src/components/ui/formStyles.ts b/src/components/ui/formStyles.ts index ebede16b..9e3199a5 100644 --- a/src/components/ui/formStyles.ts +++ b/src/components/ui/formStyles.ts @@ -7,6 +7,8 @@ export const labelCls = 'font-mono text-[10px] text-muted uppercase tracking-wid export const buttonCls = 'px-3 py-1.5 rounded text-xs font-mono bg-surface-2 border border-border text-text hover:bg-border transition-colors'; /** `buttonCls` hovers, so a disabled button needs its hover taken back too. */ export const disabledCls = 'disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-surface-2'; +/** The same look for a control that locks with `aria-disabled` and keeps focus. */ +export const ariaDisabledCls = 'aria-disabled:opacity-40 aria-disabled:cursor-not-allowed'; /** Content-builder launch button under a token field. Disabled while a variable * is present, since the builder writes a literal that can't coexist with chips. */ export const builderButtonCls = `self-start text-xs px-2 py-1 rounded border border-border bg-surface-2 hover:bg-border transition-colors ${disabledCls}`; diff --git a/src/hooks/useUpload.ts b/src/hooks/useUpload.ts index 12521b10..f00c27ad 100644 --- a/src/hooks/useUpload.ts +++ b/src/hooks/useUpload.ts @@ -1,13 +1,13 @@ import { useState } from "react"; /** `run` resolves null when it worked, else its own failure value. */ -export function useUpload(run: (file: File, ...args: A) => Promise, onThrow: E) { +export function useUpload(run: (input: I, ...args: A) => Promise, onThrow: E) { const [busy, setBusy] = useState(false); const [issue, setIssue] = useState(null); - const start = (file: File, ...args: A) => { + const start = (input: I, ...args: A) => { setIssue(null); setBusy(true); - void run(file, ...args) + void run(input, ...args) .then(setIssue) // No logging path in this app, so the inline hint is the only signal. .catch(() => setIssue(onThrow)) diff --git a/src/lib/customFonts.test.ts b/src/lib/customFonts.test.ts index fddf7573..31faab4f 100644 --- a/src/lib/customFonts.test.ts +++ b/src/lib/customFonts.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from "vitest"; +import { prepareFontBytes } from "@zplab/core/lib/customFonts"; import { builtinFontFamily, dropLegacyFontBindings, @@ -342,3 +343,12 @@ describe("resolveDefaultPrinterFontName", () => { ).toBeUndefined(); }); }); + +describe("prepareFontBytes", () => { + it("prefers the typed printer name over the source file name, and refuses a name with no usable stem", () => { + const bytes = new Uint8Array([0, 1, 0, 0]); + expect(prepareFontBytes("arial.otf", bytes)).toEqual({ ok: true, path: "E:ARIAL.TTF", bytes }); + expect(prepareFontBytes("arial.ttf", bytes, "R:LOGO")).toEqual({ ok: true, path: "R:LOGO.TTF", bytes }); + expect(prepareFontBytes("...", bytes)).toEqual({ ok: false, reason: "nameUnusable" }); + }); +}); diff --git a/src/lib/fontFaceStyle.test.ts b/src/lib/fontFaceStyle.test.ts new file mode 100644 index 00000000..f228144a --- /dev/null +++ b/src/lib/fontFaceStyle.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { cachedFontFaceStyle, systemFontFaceStyle } from "./fontFaceStyle"; + +const font = (family: string, style: string) => ({ family, style }); + +describe("systemFontFaceStyle", () => { + it("maps compound and single style words to weight and slant", () => { + expect(systemFontFaceStyle(font("Noto Sans", "Extra Bold Italic"))).toEqual({ fontFamily: '"Noto Sans"', fontWeight: 800, fontStyle: "italic" }); + expect(systemFontFaceStyle(font("A", "Semi-Bold"))).toEqual({ fontFamily: '"A"', fontWeight: 600, fontStyle: undefined }); + expect(systemFontFaceStyle(font("A", "Ultra Light"))).toEqual({ fontFamily: '"A"', fontWeight: 200, fontStyle: undefined }); + expect(systemFontFaceStyle(font("A", "Bold"))).toEqual({ fontFamily: '"A"', fontWeight: 700, fontStyle: undefined }); + }); + + it("leaves a style with no known weight word unweighted", () => { + expect(systemFontFaceStyle(font("A", "Condensed Oblique"))).toEqual({ fontFamily: '"A"', fontWeight: undefined, fontStyle: "italic" }); + }); + + it("strips quotes from the family", () => { + expect(systemFontFaceStyle(font('Say "Hi"', "Regular")).fontFamily).toBe('"Say Hi"'); + }); +}); + +describe("cachedFontFaceStyle", () => { + it("quotes a registered face and leaves a font without one unstyled", () => { + expect(cachedFontFaceStyle("zpl-7")).toEqual({ fontFamily: '"zpl-7"' }); + expect(cachedFontFaceStyle(undefined)).toBeUndefined(); + }); +}); diff --git a/src/lib/fontFaceStyle.ts b/src/lib/fontFaceStyle.ts new file mode 100644 index 00000000..16eddafa --- /dev/null +++ b/src/lib/fontFaceStyle.ts @@ -0,0 +1,41 @@ +import type { CSSProperties } from 'react'; + +/** A family with spaces or a leading digit needs the quotes, so an inner quote has to go. */ +const quoted = (family: string) => `"${family.replace(/["\\]/g, '')}"`; + +const WEIGHTS: Record = { + thin: 100, + hairline: 100, + extralight: 200, + ultralight: 200, + light: 300, + book: 400, + normal: 400, + regular: 400, + medium: 500, + demibold: 600, + semibold: 600, + bold: 700, + extrabold: 800, + ultrabold: 800, + black: 900, + heavy: 900, +}; + +/** The webview resolves an installed family by name, so the preview needs no bytes off disk. */ +export function systemFontFaceStyle(font: { family: string; style: string }): CSSProperties { + const words = font.style.toLowerCase().split(/[\s_-]+/).filter(Boolean); + // Extra Light is one key and must beat light, so pairs come first. + const pairs = words.slice(1).map((w, i) => words[i] + w); + const weight = [...pairs, ...words].find((w) => Object.hasOwn(WEIGHTS, w)); + return { + fontFamily: quoted(font.family), + fontWeight: weight === undefined ? undefined : WEIGHTS[weight], + fontStyle: words.includes('italic') || words.includes('oblique') ? 'italic' : undefined, + }; +} + +/** An undefined family means the cache registered no `zpl-` face for these bytes. */ +export function cachedFontFaceStyle(family: string | undefined): CSSProperties | undefined { + return family ? { fontFamily: quoted(family) } : undefined; +} diff --git a/src/lib/systemFonts.ts b/src/lib/systemFonts.ts new file mode 100644 index 00000000..1341256b --- /dev/null +++ b/src/lib/systemFonts.ts @@ -0,0 +1,25 @@ +import { isDesktopShell } from "./platform"; + +/** Mirrors the Rust `SystemFont` DTO. */ +export interface SystemFont { + family: string; + style: string; + path: string; + file_name: string; + bytes: number; + restricted: boolean; + variable: boolean; +} + +export async function listSystemFonts(): Promise { + if (!isDesktopShell) return []; + const { invoke } = await import("@tauri-apps/api/core"); + return invoke("list_system_fonts"); +} + +/** Only a path the listing returned is served. */ +export async function readSystemFont(path: string): Promise { + if (!isDesktopShell) throw new Error("desktop only"); + const { invoke } = await import("@tauri-apps/api/core"); + return new Uint8Array(await invoke("read_system_font", { path })); +} diff --git a/src/locales/ar.ts b/src/locales/ar.ts index f202da8b..16dd8d80 100644 --- a/src/locales/ar.ts +++ b/src/locales/ar.ts @@ -1708,6 +1708,13 @@ const ar = { printerFilename: 'اسم ملف الطابعة', printerFilenamePlaceholder: 'مثال: ARIAL.TTF', upload: 'رفع', + fromComputer: 'من هذا الكمبيوتر', + filterFonts: 'تصفية الخطوط', + noSystemFonts: 'لا يوجد خط مثبت مناسب. يتم استثناء الخطوط التي يزيد حجمها عن 4 MiB ومجموعات الخطوط.', + noFilterMatch: 'لا يوجد خط مثبت يطابق التصفية.', + systemFontsFailed: 'تعذّر قراءة الخطوط المثبتة.', + restrictedLicense: 'ترخيص الخط يمنع التضمين.', + variableFont: 'خط متغير، تستخدم الطابعة نسخته الافتراضية فقط.', cancel: 'إلغاء', delete: 'حذف', deleteConfirm: 'هل تريد حذف هذا الخط من المتصفح؟ التصاميم التي حفظتها سابقًا وتستخدمه ستفقده.', @@ -1715,7 +1722,7 @@ const ar = { deleteHistoryManyFmt: 'سيحذف ذلك أيضاً {n} خطوات في سجل التراجع تستخدمه.', uploadError: 'تعذّر تحميل ملف الخط', faceRejected: 'لا يمكن للمتصفح عرض هذا الخط في المعاينة. قد تقبله الطابعة رغم ذلك.', - nameTaken: 'يوجد بالفعل خط مخزَّن مؤقتًا بهذا الاسم للطابعة ببايتات مختلفة. أعد تسمية الملف أو احذف الخط المخزَّن مؤقتًا أولاً.', + nameTaken: 'يوجد بالفعل خط مخزَّن مؤقتًا بهذا الاسم للطابعة ببايتات مختلفة. استخدم اسم ملف طابعة مختلفًا، أو احذف الخط المخزَّن مؤقتًا أولاً.', nameUnusable: 'لا يترك اسم الملف أي اسم صالح للطابعة بعد اختزاله إلى أحرف وأرقام. أعد تسمية الملف أولاً.', inUse: 'لا يزال قيد الاستخدام من قِبل حقل نص أو اسم مستعار لهذا التصميم.', inProfile: 'لا يزال يُوفَّر بواسطة ملف تعريف الطابعة. أزِله أولاً من ضِمن الكائنات.', diff --git a/src/locales/bg.ts b/src/locales/bg.ts index 5a18186e..e731cc1a 100644 --- a/src/locales/bg.ts +++ b/src/locales/bg.ts @@ -1708,6 +1708,13 @@ const bg = { printerFilename: 'Файлово наименование на принтера', printerFilenamePlaceholder: 'напр. ARIAL.TTF', upload: 'Качване', + fromComputer: 'От този компютър', + filterFonts: 'Филтриране на шрифтове', + noSystemFonts: 'Няма инсталиран шрифт, който да отговаря. Шрифтове над 4 MiB и шрифтови колекции се пропускат.', + noFilterMatch: 'Няма инсталиран шрифт, който да отговаря на филтъра.', + systemFontsFailed: 'Неуспешно прочитане на инсталираните шрифтове.', + restrictedLicense: 'Лицензът на шрифта не позволява вграждане.', + variableFont: 'Променлив шрифт, принтерът използва само подразбиращия се екземпляр.', cancel: 'Отказ', delete: 'Изтриване', deleteConfirm: 'Да се изтрие ли този шрифт от браузъра? Дизайните, които сте запазили по-рано и го използват, ще го загубят.', @@ -1715,7 +1722,7 @@ const bg = { deleteHistoryManyFmt: 'Това също ще премахне {n} стъпки в историята на отмяна, които го използват.', uploadError: 'Неуспешно зареждане на шрифтов файл', faceRejected: 'Браузърът не може да покаже този шрифт в прегледа. Принтерът може въпреки това да го приеме.', - nameTaken: 'Вече има кеширан шрифт с това име на принтера с различни байтове. Преименувайте файла или първо изтрийте кеширания шрифт.', + nameTaken: 'Вече има кеширан шрифт с това име на принтера с различни байтове. Използвайте друго файлово наименование на принтера, или първо изтрийте кеширания шрифт.', nameUnusable: 'Името на файла не оставя име на принтера, след като се сведе до букви и цифри. Първо преименувайте файла.', inUse: 'Все още се използва от текстово поле или псевдоним на този дизайн.', inProfile: 'Все още се предоставя от принтерния профил. Премахнете го първо от Обекти.', diff --git a/src/locales/cs.ts b/src/locales/cs.ts index 0ae94ed5..8f35624c 100644 --- a/src/locales/cs.ts +++ b/src/locales/cs.ts @@ -1708,6 +1708,13 @@ const cs = { printerFilename: 'Název souboru tiskárny', printerFilenamePlaceholder: 'např. ARIAL.TTF', upload: 'Nahrát', + fromComputer: 'Z tohoto počítače', + filterFonts: 'Filtrovat písma', + noSystemFonts: 'Žádné nainstalované písmo nevyhovuje. Písma nad 4 MiB a kolekce písem se vynechávají.', + noFilterMatch: 'Žádné nainstalované písmo neodpovídá filtru.', + systemFontsFailed: 'Nainstalované fonty se nepodařilo načíst.', + restrictedLicense: 'Licence písma zakazuje vložení.', + variableFont: 'Variabilní písmo, tiskárna používá jen jeho výchozí instanci.', cancel: 'Zrušit', delete: 'Smazat', deleteConfirm: 'Smazat toto písmo z prohlížeče? Dříve uložené návrhy, které je používají, o ně přijdou.', @@ -1715,7 +1722,7 @@ const cs = { deleteHistoryManyFmt: 'Tím se navíc odstraní {n} kroků v historii zpět, které ho používají.', uploadError: 'Soubor písma se nepodařilo načíst', faceRejected: 'Prohlížeč nedokáže tento font zobrazit v náhledu. Tiskárna jej přesto může přijmout.', - nameTaken: 'Pod tímto názvem tiskárny je již v mezipaměti uložen font s jinými bajty. Přejmenujte soubor, nebo nejprve odstraňte uložený font z mezipaměti.', + nameTaken: 'Pod tímto názvem tiskárny je již v mezipaměti uložen font s jinými bajty. Použijte jiný název souboru tiskárny, nebo nejprve odstraňte uložený font z mezipaměti.', nameUnusable: 'Název souboru po zredukování na písmena a číslice nedává žádný název tiskárny. Nejprve přejmenujte soubor.', inUse: 'Stále ho používá textové pole nebo alias tohoto návrhu.', inProfile: 'Stále ji poskytuje profil tiskárny. Nejprve ji odeberte v části Objekty.', diff --git a/src/locales/da.ts b/src/locales/da.ts index f2643305..206f62c7 100644 --- a/src/locales/da.ts +++ b/src/locales/da.ts @@ -1708,6 +1708,13 @@ const da = { printerFilename: 'Printerfilnavn', printerFilenamePlaceholder: 'f.eks. ARIAL.TTF', upload: 'Upload', + fromComputer: 'Fra denne computer', + filterFonts: 'Filtrer skrifttyper', + noSystemFonts: 'Ingen installeret skrifttype passer. Skrifttyper over 4 MiB og skrifttypesamlinger udelades.', + noFilterMatch: 'Ingen installeret skrifttype matcher filteret.', + systemFontsFailed: 'De installerede skrifttyper kunne ikke læses.', + restrictedLicense: 'Skrifttypens licens forbyder indlejring.', + variableFont: 'Variabel skrifttype, printeren bruger kun standardinstansen.', cancel: 'Annuller', delete: 'Slet', deleteConfirm: 'Slet denne skrifttype fra browseren? Designs, du har gemt tidligere, og som bruger den, mister den.', @@ -1715,7 +1722,7 @@ const da = { deleteHistoryManyFmt: 'Dette fjerner også {n} trin i fortryd-historikken, der bruger det.', uploadError: 'Skriftfilen kunne ikke indlaeses', faceRejected: 'Browseren kan ikke vise denne skrifttype i forhåndsvisningen. Printeren accepterer den muligvis alligevel.', - nameTaken: 'Der er allerede en skrifttype cachelagret med dette printernavn med andre byte. Omdøb filen, eller slet først den cachelagrede skrifttype.', + nameTaken: 'Der er allerede en skrifttype cachelagret med dette printernavn med andre byte. Brug et andet printerfilnavn, eller slet først den cachelagrede skrifttype.', nameUnusable: 'Filnavnet giver intet printernavn tilbage, når det reduceres til bogstaver og tal. Omdøb filen først.', inUse: 'Bruges stadig af et tekstfelt eller et alias for dette design.', inProfile: 'Leveres stadig af printerprofilen. Fjern den under Objekter først.', diff --git a/src/locales/de.ts b/src/locales/de.ts index a0733b8c..507fb5ac 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1746,6 +1746,13 @@ const de = { printerFilename: 'Druckerdateiname', printerFilenamePlaceholder: 'z. B. ARIAL.TTF', upload: 'Hochladen', + fromComputer: 'Von diesem Computer', + filterFonts: 'Schriften filtern', + noSystemFonts: 'Keine installierte Schrift passt. Schriften über 4 MiB und Schriftsammlungen werden ausgelassen.', + noFilterMatch: 'Keine installierte Schrift passt zum Filter.', + systemFontsFailed: 'Die installierten Schriftarten konnten nicht gelesen werden.', + restrictedLicense: 'Die Lizenz dieser Schrift verbietet das Einbetten.', + variableFont: 'Variable Schrift, der Drucker verwendet nur die Standardinstanz.', cancel: 'Abbrechen', delete: 'Löschen', deleteConfirm: 'Diese Schrift aus dem Browser löschen? Früher gespeicherte Designs, die sie benutzen, verlieren sie.', @@ -1753,7 +1760,7 @@ const de = { deleteHistoryManyFmt: 'Dabei entfallen auch {n} Schritte im Undo-Verlauf, die sie benutzen.', uploadError: 'Schriftdatei konnte nicht geladen werden', faceRejected: 'Der Browser kann diese Schrift nicht für die Vorschau darstellen. Der Drucker nimmt sie möglicherweise trotzdem an.', - nameTaken: 'Unter diesem Druckernamen ist bereits eine Schriftart mit anderen Bytes im Cache. Benenne die Datei um, oder lösche zuerst die zwischengespeicherte Schriftart.', + nameTaken: 'Unter diesem Druckernamen ist bereits eine Schriftart mit anderen Bytes im Cache. Verwende einen anderen Druckerdateinamen, oder lösche zuerst die zwischengespeicherte Schriftart.', nameUnusable: 'Der Dateiname ergibt keinen Druckernamen mehr, wenn er auf Buchstaben und Ziffern reduziert wird. Benenne die Datei zuerst um.', inUse: 'Wird noch von einem Textfeld oder einem Alias dieses Designs verwendet.', inProfile: 'Wird noch vom Druckerprofil bereitgestellt. Entferne sie zuerst unter Objekte.', diff --git a/src/locales/el.ts b/src/locales/el.ts index e42b6b60..c397589f 100644 --- a/src/locales/el.ts +++ b/src/locales/el.ts @@ -1708,6 +1708,13 @@ const el = { printerFilename: 'Όνομα αρχείου εκτυπωτή', printerFilenamePlaceholder: 'π.χ. ARIAL.TTF', upload: 'Μεταφόρτωση', + fromComputer: 'Από αυτόν τον υπολογιστή', + filterFonts: 'Φιλτράρισμα γραμματοσειρών', + noSystemFonts: 'Καμία εγκατεστημένη γραμματοσειρά δεν ταιριάζει. Γραμματοσειρές πάνω από 4 MiB και συλλογές γραμματοσειρών παραλείπονται.', + noFilterMatch: 'Καμία εγκατεστημένη γραμματοσειρά δεν ταιριάζει με το φίλτρο.', + systemFontsFailed: 'Δεν ήταν δυνατή η ανάγνωση των εγκατεστημένων γραμματοσειρών.', + restrictedLicense: 'Η άδεια χρήσης της γραμματοσειράς δεν επιτρέπει την ενσωμάτωση.', + variableFont: 'Μεταβλητή γραμματοσειρά, ο εκτυπωτής χρησιμοποιεί μόνο την προεπιλεγμένη παρουσία της.', cancel: 'Ακύρωση', delete: 'Διαγραφή', deleteConfirm: 'Διαγραφή αυτής της γραμματοσειράς από τον φυλλομετρητή; Τα σχέδια που αποθηκεύσατε παλαιότερα και τη χρησιμοποιούν θα τη χάσουν.', @@ -1715,7 +1722,7 @@ const el = { deleteHistoryManyFmt: 'Αυτό αφαιρεί επίσης {n} βήματα στο ιστορικό αναιρέσεων που τη χρησιμοποιούν.', uploadError: 'Δεν ήταν δυνατή η φόρτωση του αρχείου γραμματοσειράς', faceRejected: "Το πρόγραμμα περιήγησης δεν μπορεί να εμφανίσει αυτή τη γραμματοσειρά στην προεπισκόπηση. Ο εκτυπωτής ενδέχεται να την αποδεχτεί παρ' όλα αυτά.", - nameTaken: 'Υπάρχει ήδη αποθηκευμένη στην κρυφή μνήμη γραμματοσειρά με αυτό το όνομα εκτυπωτή αλλά με διαφορετικά byte. Μετονομάστε το αρχείο ή διαγράψτε πρώτα τη γραμματοσειρά από την κρυφή μνήμη.', + nameTaken: 'Υπάρχει ήδη αποθηκευμένη στην κρυφή μνήμη γραμματοσειρά με αυτό το όνομα εκτυπωτή αλλά με διαφορετικά byte. Χρησιμοποιήστε διαφορετικό όνομα αρχείου εκτυπωτή, ή διαγράψτε πρώτα τη γραμματοσειρά από την κρυφή μνήμη.', nameUnusable: 'Το όνομα αρχείου δεν αφήνει κανένα όνομα εκτυπωτή όταν μειωθεί σε γράμματα και ψηφία. Μετονομάστε πρώτα το αρχείο.', inUse: 'Χρησιμοποιείται ακόμη από ένα πεδίο κειμένου ή ένα ψευδώνυμο αυτής της σχεδίασης.', inProfile: 'Παρέχεται ακόμη από το προφίλ εκτυπωτή. Αφαιρέστε την πρώτα από τα Αντικείμενα.', diff --git a/src/locales/en.ts b/src/locales/en.ts index 30df6d5f..c39bdc34 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1746,6 +1746,13 @@ const en = { printerFilename: 'Printer filename', printerFilenamePlaceholder: 'e.g. ARIAL.TTF', upload: 'Upload', + fromComputer: 'From this computer', + filterFonts: 'Filter fonts', + noSystemFonts: 'No installed font fits. Fonts above 4 MiB and font collections are left out.', + noFilterMatch: 'No installed font matches the filter.', + systemFontsFailed: 'Could not read the installed fonts.', + restrictedLicense: "The font's license forbids embedding.", + variableFont: 'Variable font, the printer uses its default instance.', cancel: 'Cancel', delete: 'Delete', deleteConfirm: 'Delete this font from the browser? Designs you saved earlier that use it will lose it.', @@ -1753,7 +1760,7 @@ const en = { deleteHistoryManyFmt: 'This also drops {n} steps in the undo history that use it.', uploadError: 'Could not load font file', faceRejected: 'The browser cannot render this font for the preview. The printer may still accept it.', - nameTaken: 'A font with this printer name is already cached with different bytes. Rename the file, or delete the cached font first.', + nameTaken: 'A font with this printer name is already cached with different bytes. Use a different printer filename, or delete the cached font first.', nameUnusable: 'The file name leaves no printer name once reduced to letters and digits. Rename the file first.', inUse: 'Still used by a text field or an alias of this design.', inProfile: 'Still provisioned by the printer profile. Remove it under Objects first.', diff --git a/src/locales/es.ts b/src/locales/es.ts index 7e9ddd9e..7e7b250f 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1708,6 +1708,13 @@ const es = { printerFilename: 'Nombre de archivo de impresora', printerFilenamePlaceholder: 'p. ej. ARIAL.TTF', upload: 'Subir', + fromComputer: 'Desde este equipo', + filterFonts: 'Filtrar fuentes', + noSystemFonts: 'Ninguna fuente instalada coincide. Las fuentes de más de 4 MiB y las colecciones de fuentes se excluyen.', + noFilterMatch: 'Ninguna fuente instalada coincide con el filtro.', + systemFontsFailed: 'No se pudieron leer las fuentes instaladas.', + restrictedLicense: 'La licencia de la fuente prohíbe incrustarla.', + variableFont: 'Fuente variable, la impresora usa solo su instancia predeterminada.', cancel: 'Cancelar', delete: 'Eliminar', deleteConfirm: '¿Eliminar esta fuente del navegador? Los diseños que guardaste antes y la usan la perderán.', @@ -1715,7 +1722,7 @@ const es = { deleteHistoryManyFmt: 'Esto también elimina {n} pasos en el historial de deshacer que la usan.', uploadError: 'No se pudo cargar el archivo de fuente', faceRejected: 'El navegador no puede representar esta fuente en la vista previa. Es posible que la impresora la acepte de todos modos.', - nameTaken: 'Ya hay una fuente en caché con este nombre de impresora pero con bytes distintos. Renombra el archivo, o elimina primero la fuente en caché.', + nameTaken: 'Ya hay una fuente en caché con este nombre de impresora pero con bytes distintos. Usa un nombre de archivo de impresora distinto, o elimina primero la fuente en caché.', nameUnusable: 'El nombre del archivo no deja ningún nombre de impresora al reducirlo a letras y dígitos. Renombra el archivo primero.', inUse: 'Todavía la usa un campo de texto o un alias de este diseño.', inProfile: 'Todavía la proporciona el perfil de impresora. Elimínala primero en Objetos.', diff --git a/src/locales/et.ts b/src/locales/et.ts index caee8d1d..b20e2c38 100644 --- a/src/locales/et.ts +++ b/src/locales/et.ts @@ -1708,6 +1708,13 @@ const et = { printerFilename: 'Printeri failinimi', printerFilenamePlaceholder: 'nt. ARIAL.TTF', upload: 'Laadi üles', + fromComputer: 'Sellest arvutist', + filterFonts: 'Filtreeri fonte', + noSystemFonts: 'Ükski installitud font ei sobi. Fondid üle 4 MiB ja fondikollektsioonid jäetakse välja.', + noFilterMatch: 'Ükski installitud font ei vasta filtrile.', + systemFontsFailed: 'Installitud fonte ei saanud lugeda.', + restrictedLicense: 'Fondi litsents keelab manustamise.', + variableFont: 'Muutuv font, printer kasutab ainult selle vaikeeksemplari.', cancel: 'Tühista', delete: 'Kustuta', deleteConfirm: 'Kustutada see font brauserist? Varem salvestatud kujundused, mis seda kasutavad, kaotavad selle.', @@ -1715,7 +1722,7 @@ const et = { deleteHistoryManyFmt: 'Selle käigus kustub ka {n} sammu tagasivõtmise ajaloos, mis seda kasutavad.', uploadError: 'Fondifaili ei saanud laadida', faceRejected: 'Brauser ei suuda seda fonti eelvaates kuvada. Printer võib selle siiski vastu võtta.', - nameTaken: 'Selle printeri nimega on juba puhvris font teistsuguste baitidega. Nimeta fail ümber või kustuta esmalt puhverdatud font.', + nameTaken: 'Selle printeri nimega on juba puhvris font teistsuguste baitidega. Kasuta teist printeri failinime või kustuta esmalt puhverdatud font.', nameUnusable: 'Failinimi ei anna tähtedeks ja numbriteks taandades ühtegi printeri nime. Nimeta fail kõigepealt ümber.', inUse: 'Seda kasutab veel tekstiväli või selle kujunduse alias.', inProfile: 'Seda pakub veel printeriprofiil. Eemalda see esmalt jaotisest Objektid.', diff --git a/src/locales/fa.ts b/src/locales/fa.ts index 8d0ad15d..7a52af3c 100644 --- a/src/locales/fa.ts +++ b/src/locales/fa.ts @@ -1708,6 +1708,13 @@ const fa = { printerFilename: 'نام فایل چاپگر', printerFilenamePlaceholder: 'مثلاً ARIAL.TTF', upload: 'بارگذاری', + fromComputer: 'از این کامپیوتر', + filterFonts: 'فیلتر فونت‌ها', + noSystemFonts: 'هیچ فونت نصب‌شده‌ای مطابقت ندارد. فونت‌های بالای 4 MiB و مجموعه‌فونت‌ها کنار گذاشته می‌شوند.', + noFilterMatch: 'هیچ فونت نصب‌شده‌ای با فیلتر مطابقت ندارد.', + systemFontsFailed: 'خواندن فونت‌های نصب‌شده ممکن نبود.', + restrictedLicense: 'مجوز فونت تعبیه را ممنوع می‌کند.', + variableFont: 'فونت متغیر، چاپگر فقط از نمونهٔ پیش‌فرض آن استفاده می‌کند.', cancel: 'لغو', delete: 'حذف', deleteConfirm: 'این فونت از مرورگر حذف شود؟ طرح‌هایی که پیش‌تر ذخیره کرده‌اید و از آن استفاده می‌کنند آن را از دست می‌دهند.', @@ -1715,7 +1722,7 @@ const fa = { deleteHistoryManyFmt: 'این کار {n} مرحله از تاریخچهٔ واگرد را که از آن استفاده می‌کنند نیز حذف می‌کند.', uploadError: 'بارگذاری فایل فونت ممکن نبود', faceRejected: 'مرورگر نمی‌تواند این فونت را در پیش‌نمایش نمایش دهد. ممکن است چاپگر همچنان آن را بپذیرد.', - nameTaken: 'فونتی با این نام چاپگر و بایت‌های متفاوت از قبل در حافظهٔ پنهان ذخیره شده است. نام فایل را تغییر دهید یا ابتدا فونت ذخیره‌شده در حافظهٔ پنهان را حذف کنید.', + nameTaken: 'فونتی با این نام چاپگر و بایت‌های متفاوت از قبل در حافظهٔ پنهان ذخیره شده است. از نام فایل چاپگر دیگری استفاده کنید، یا ابتدا فونت ذخیره‌شده در حافظهٔ پنهان را حذف کنید.', nameUnusable: 'نام فایل پس از تقلیل به حروف و ارقام هیچ نام چاپگری باقی نمی‌گذارد. ابتدا نام فایل را تغییر دهید.', inUse: 'هنوز یک فیلد متنی یا نام مستعار این طراحی از آن استفاده می‌کند.', inProfile: 'هنوز توسط نمایهٔ چاپگر تأمین می‌شود. ابتدا آن را زیر «اشیاء» حذف کنید.', diff --git a/src/locales/fi.ts b/src/locales/fi.ts index 45117c7a..69a5fa21 100644 --- a/src/locales/fi.ts +++ b/src/locales/fi.ts @@ -1708,6 +1708,13 @@ const fi = { printerFilename: 'Tulostimen tiedostonimi', printerFilenamePlaceholder: 'esim. ARIAL.TTF', upload: 'Lataa', + fromComputer: 'Tästä tietokoneesta', + filterFonts: 'Suodata fontteja', + noSystemFonts: 'Mikään asennettu fontti ei sovi. Fontit, joiden koko on yli 4 MiB, ja fonttikokoelmat jätetään pois.', + noFilterMatch: 'Mikään asennettu fontti ei vastaa suodatinta.', + systemFontsFailed: 'Asennettuja fontteja ei voitu lukea.', + restrictedLicense: 'Fontin lisenssi kieltää upottamisen.', + variableFont: 'Muuttuva fontti, tulostin käyttää vain sen oletusversiota.', cancel: 'Peruuta', delete: 'Poista', deleteConfirm: 'Poistetaanko tämä fontti selaimesta? Aiemmin tallennetut rakenteet, jotka käyttävät sitä, menettävät sen.', @@ -1715,7 +1722,7 @@ const fi = { deleteHistoryManyFmt: 'Tämä poistaa myös {n} vaihetta kumoamishistoriassa, jotka käyttävät sitä.', uploadError: 'Fonttitiedostoa ei voitu ladata', faceRejected: 'Selain ei pysty näyttämään tätä fonttia esikatselussa. Tulostin saattaa silti hyväksyä sen.', - nameTaken: 'Tällä tulostimen nimellä on jo välimuistissa fontti, jolla on eri tavut. Nimeä tiedosto uudelleen, tai poista ensin välimuistissa oleva fontti.', + nameTaken: 'Tällä tulostimen nimellä on jo välimuistissa fontti, jolla on eri tavut. Käytä toista tulostimen tiedostonimeä, tai poista ensin välimuistissa oleva fontti.', nameUnusable: 'Tiedostonimi ei jätä jäljelle mitään tulostimen nimeä, kun se supistetaan kirjaimiin ja numeroihin. Nimeä tiedosto ensin uudelleen.', inUse: 'Tekstikenttä tai tämän suunnittelun alias käyttää sitä yhä.', inProfile: 'Tulostinprofiili tarjoaa sen yhä. Poista se ensin kohdasta Objektit.', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 7e7d77e0..02e40fae 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -1708,6 +1708,13 @@ const fr = { printerFilename: 'Nom de fichier imprimante', printerFilenamePlaceholder: 'ex. ARIAL.TTF', upload: 'Téléverser', + fromComputer: 'Depuis cet ordinateur', + filterFonts: 'Filtrer les polices', + noSystemFonts: 'Aucune police installée ne correspond. Les polices de plus de 4 MiB et les collections de polices sont exclues.', + noFilterMatch: 'Aucune police installée ne correspond au filtre.', + systemFontsFailed: 'Impossible de lire les polices installées.', + restrictedLicense: "La licence de la police interdit l'intégration.", + variableFont: "Police variable, l'imprimante utilise uniquement son instance par défaut.", cancel: 'Annuler', delete: 'Supprimer', deleteConfirm: "Supprimer cette police du navigateur ? Les designs enregistrés précédemment qui l'utilisent la perdront.", @@ -1715,7 +1722,7 @@ const fr = { deleteHistoryManyFmt: "Cela supprime aussi {n} étapes de l'historique d'annulation qui l'utilisent.", uploadError: 'Impossible de charger le fichier de police', faceRejected: "Le navigateur ne peut pas afficher cette police dans l'aperçu. L'imprimante peut tout de même l'accepter.", - nameTaken: "Une police avec ce nom d'imprimante est déjà en cache avec des octets différents. Renommez le fichier, ou supprimez d'abord la police en cache.", + nameTaken: "Une police avec ce nom d'imprimante est déjà en cache avec des octets différents. Utilisez un autre nom de fichier imprimante, ou supprimez d'abord la police en cache.", nameUnusable: "Le nom du fichier ne laisse aucun nom d'imprimante une fois réduit aux lettres et aux chiffres. Renommez d'abord le fichier.", inUse: 'Encore utilisée par un champ de texte ou un alias de ce design.', inProfile: "Encore fournie par le profil d'imprimante. Retirez-la d'abord dans Objets.", diff --git a/src/locales/he.ts b/src/locales/he.ts index 2cc94b39..6a631ef0 100644 --- a/src/locales/he.ts +++ b/src/locales/he.ts @@ -1708,6 +1708,13 @@ const he = { printerFilename: 'שם קובץ המדפסת', printerFilenamePlaceholder: 'לדוגמה: ARIAL.TTF', upload: 'העלה', + fromComputer: 'מהמחשב הזה', + filterFonts: 'סינון גופנים', + noSystemFonts: 'אין גופן מותקן שמתאים. גופנים מעל 4 MiB ואוספי גופנים אינם נכללים.', + noFilterMatch: 'אין גופן מותקן שמתאים למסנן.', + systemFontsFailed: 'לא ניתן לקרוא את הגופנים המותקנים.', + restrictedLicense: 'הרישיון של הגופן אוסר על הטבעה.', + variableFont: 'גופן משתנה, המדפסת משתמשת רק במופע המחדל שלו.', cancel: 'ביטול', delete: 'מחק', deleteConfirm: 'למחוק את הגופן הזה מהדפדפן? עיצובים ששמרת בעבר ומשתמשים בו יאבדו אותו.', @@ -1715,7 +1722,7 @@ const he = { deleteHistoryManyFmt: 'פעולה זו גם תמחק {n} שלבים בהיסטוריית הביטולים שמשתמשים בו.', uploadError: 'לא ניתן לטעון את קובץ הגופן', faceRejected: 'הדפדפן אינו יכול להציג גופן זה בתצוגה המקדימה. ייתכן שהמדפסת תקבל אותו בכל זאת.', - nameTaken: 'גופן עם שם מדפסת זה כבר שמור במטמון עם בייטים שונים. שנה את שם הקובץ או מחק תחילה את הגופן השמור במטמון.', + nameTaken: 'גופן עם שם מדפסת זה כבר שמור במטמון עם בייטים שונים. השתמש בשם קובץ מדפסת אחר, או מחק תחילה את הגופן השמור במטמון.', nameUnusable: 'שם הקובץ אינו משאיר שם מדפסת לאחר צמצום לאותיות ולספרות. שנה תחילה את שם הקובץ.', inUse: 'עדיין בשימוש על ידי שדה טקסט או כינוי של עיצוב זה.', inProfile: 'עדיין מסופק על ידי פרופיל המדפסת. הסר אותו תחילה תחת אובייקטים.', diff --git a/src/locales/hr.ts b/src/locales/hr.ts index bbb2c885..81529292 100644 --- a/src/locales/hr.ts +++ b/src/locales/hr.ts @@ -1708,6 +1708,13 @@ const hr = { printerFilename: 'Naziv datoteke pisača', printerFilenamePlaceholder: 'npr. ARIAL.TTF', upload: 'Prenesi', + fromComputer: 'S ovog računala', + filterFonts: 'Filtriraj fontove', + noSystemFonts: 'Nijedan instalirani font ne odgovara. Fontovi veći od 4 MiB i zbirke fontova su izostavljeni.', + noFilterMatch: 'Nijedan instalirani font ne odgovara filtru.', + systemFontsFailed: 'Instalirani fontovi nisu mogli biti pročitani.', + restrictedLicense: 'Licenca fonta ne dopušta ugrađivanje.', + variableFont: 'Varijabilni font, pisač koristi samo njegovu zadanu instancu.', cancel: 'Odustani', delete: 'Izbriši', deleteConfirm: 'Izbrisati ovaj font iz preglednika? Ranije spremljeni dizajni koji ga koriste izgubit će ga.', @@ -1715,7 +1722,7 @@ const hr = { deleteHistoryManyFmt: 'Time će se izbrisati i {n} koraka u povijesti poništavanja koji ga koriste.', uploadError: 'Datoteka fonta nije mogla biti učitana', faceRejected: 'Preglednik ne može prikazati ovaj font u pregledu. Pisač ga ipak može prihvatiti.', - nameTaken: 'Font s ovim nazivom pisača već je u predmemoriji s drugačijim bajtovima. Preimenujte datoteku ili prvo izbrišite predmemorirani font.', + nameTaken: 'Font s ovim nazivom pisača već je u predmemoriji s drugačijim bajtovima. Upotrijebite drugi naziv datoteke pisača ili prvo izbrišite predmemorirani font.', nameUnusable: 'Naziv datoteke ne ostavlja nijedan naziv pisača kad se svede na slova i znamenke. Najprije preimenujte datoteku.', inUse: 'Još ga koristi tekstualno polje ili alias ovog dizajna.', inProfile: 'Još ga osigurava profil pisača. Ukloni ga prvo u odjeljku Objekti.', diff --git a/src/locales/hu.ts b/src/locales/hu.ts index e399180b..2223f1dc 100644 --- a/src/locales/hu.ts +++ b/src/locales/hu.ts @@ -1708,6 +1708,13 @@ const hu = { printerFilename: 'Nyomtató fájlnév', printerFilenamePlaceholder: 'pl. ARIAL.TTF', upload: 'Feltöltés', + fromComputer: 'Erről a számítógépről', + filterFonts: 'Betűtípusok szűrése', + noSystemFonts: 'Nincs megfelelő telepített betűtípus. A 4 MiB-nál nagyobb betűtípusok és a betűtípus-gyűjtemények ki vannak hagyva.', + noFilterMatch: 'Nincs a szűrőnek megfelelő telepített betűtípus.', + systemFontsFailed: 'A telepített betűtípusok nem olvashatók be.', + restrictedLicense: 'A betűtípus licence tiltja a beágyazást.', + variableFont: 'Variábilis betűtípus, a nyomtató csak az alapértelmezett változatát használja.', cancel: 'Mégse', delete: 'Törlés', deleteConfirm: 'Törli ezt a betűtípust a böngészőből? A korábban mentett tervek, amelyek használják, elveszítik.', @@ -1715,7 +1722,7 @@ const hu = { deleteHistoryManyFmt: 'Ez egyben töröl {n} lépést a visszavonási előzményekben, amelyek ezt használják.', uploadError: 'A betűtípus-fájl nem tölthető be', faceRejected: 'A böngésző nem tudja megjeleníteni ezt a betűtípust az előnézetben. A nyomtató ennek ellenére elfogadhatja.', - nameTaken: 'Ezzel a nyomtatónévvel már van gyorsítótárazott betűtípus, más bájtokkal. Nevezd át a fájlt, vagy előbb töröld a gyorsítótárazott betűtípust.', + nameTaken: 'Ezzel a nyomtatónévvel már van gyorsítótárazott betűtípus, más bájtokkal. Használj másik nyomtató fájlnevet, vagy előbb töröld a gyorsítótárazott betűtípust.', nameUnusable: 'A fájlnév betűkre és számjegyekre csökkentve nem hagy meg nyomtatónevet. Először nevezd át a fájlt.', inUse: 'Még használja egy szövegmező vagy ennek a tervnek egy aliasa.', inProfile: 'Még mindig a nyomtatóprofil biztosítja. Előbb távolítsd el az Objektumok alatt.', diff --git a/src/locales/it.ts b/src/locales/it.ts index 4767d585..6521772d 100644 --- a/src/locales/it.ts +++ b/src/locales/it.ts @@ -1708,6 +1708,13 @@ const it = { printerFilename: 'Nome file stampante', printerFilenamePlaceholder: 'es. ARIAL.TTF', upload: 'Carica', + fromComputer: 'Da questo computer', + filterFonts: 'Filtra i caratteri', + noSystemFonts: 'Nessun carattere installato corrisponde. I caratteri superiori a 4 MiB e le raccolte di caratteri vengono esclusi.', + noFilterMatch: 'Nessun carattere installato corrisponde al filtro.', + systemFontsFailed: 'Impossibile leggere i caratteri installati.', + restrictedLicense: "La licenza del carattere non consente l'incorporamento.", + variableFont: 'Carattere variabile, la stampante usa solo la sua istanza predefinita.', cancel: 'Annulla', delete: 'Elimina', deleteConfirm: 'Eliminare questo font dal browser? I design salvati in precedenza che lo usano lo perderanno.', @@ -1715,7 +1722,7 @@ const it = { deleteHistoryManyFmt: 'Questo elimina anche {n} passaggi della cronologia di annullamento che lo usano.', uploadError: 'Impossibile caricare il file del carattere', faceRejected: "Il browser non riesce a visualizzare questo carattere nell'anteprima. La stampante potrebbe comunque accettarlo.", - nameTaken: 'È già presente in cache un font con questo nome stampante ma con byte diversi. Rinomina il file, oppure elimina prima il font in cache.', + nameTaken: 'È già presente in cache un font con questo nome stampante ma con byte diversi. Usa un nome file stampante diverso, oppure elimina prima il font in cache.', nameUnusable: 'Il nome del file non lascia alcun nome stampante una volta ridotto a lettere e cifre. Rinomina prima il file.', inUse: 'Ancora usato da un campo di testo o da un alias di questo design.', inProfile: 'Ancora fornito dal profilo stampante. Rimuovilo prima da Oggetti.', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 0fa11014..7834a108 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -1708,6 +1708,13 @@ const ja = { printerFilename: 'プリンターファイル名', printerFilenamePlaceholder: '例: ARIAL.TTF', upload: 'アップロード', + fromComputer: 'このコンピューターから', + filterFonts: 'フォントを絞り込み', + noSystemFonts: 'インストールされたフォントで一致するものがありません。4 MiBを超えるフォントとフォントコレクションは対象外です。', + noFilterMatch: '絞り込み条件に一致するインストール済みのフォントがありません。', + systemFontsFailed: 'インストールされたフォントを読み込めませんでした。', + restrictedLicense: 'このフォントのライセンスは埋め込みを禁止しています。', + variableFont: '可変フォント、プリンターは既定のインスタンスのみを使用します。', cancel: 'キャンセル', delete: '削除', deleteConfirm: 'このフォントをブラウザから削除しますか? それを使用している以前保存したデザインはフォントを失います。', @@ -1715,7 +1722,7 @@ const ja = { deleteHistoryManyFmt: 'これにより、それを使用する元に戻す履歴内の操作も{n}件削除されます。', uploadError: 'フォントファイルを読み込めませんでした', faceRejected: 'ブラウザはこのフォントをプレビューに表示できません。プリンターは受け付ける場合があります。', - nameTaken: 'このプリンター名のフォントは、異なるバイト列で既にキャッシュされています。ファイル名を変更するか、先にキャッシュされたフォントを削除してください。', + nameTaken: 'このプリンター名のフォントは、異なるバイト列で既にキャッシュされています。別のプリンターファイル名を使用するか、先にキャッシュされたフォントを削除してください。', nameUnusable: 'ファイル名は英字と数字に縮小すると、プリンター名が残りません。先にファイル名を変更してください。', inUse: 'まだテキストフィールドまたはこのデザインのエイリアスが使用しています。', inProfile: 'まだプリンタープロファイルから供給されています。先にオブジェクトで削除してください。', diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 18ea6f3a..af73a8fc 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -1708,6 +1708,13 @@ const ko = { printerFilename: '프린터 파일명', printerFilenamePlaceholder: '예: ARIAL.TTF', upload: '업로드', + fromComputer: '이 컴퓨터에서', + filterFonts: '글꼴 필터링', + noSystemFonts: '일치하는 설치된 글꼴이 없습니다. 4 MiB를 초과하는 글꼴과 글꼴 모음은 제외됩니다.', + noFilterMatch: '필터와 일치하는 설치된 글꼴이 없습니다.', + systemFontsFailed: '설치된 글꼴을 불러올 수 없습니다.', + restrictedLicense: '이 글꼴의 라이선스는 포함을 금지합니다.', + variableFont: '가변 글꼴, 프린터는 기본 인스턴스만 사용합니다.', cancel: '취소', delete: '삭제', deleteConfirm: '이 글꼴을 브라우저에서 삭제할까요? 이를 사용하는 이전에 저장한 디자인은 글꼴을 잃게 됩니다.', @@ -1715,7 +1722,7 @@ const ko = { deleteHistoryManyFmt: '이렇게 하면 이를 사용하는 실행 취소 기록의 단계 {n}개도 함께 삭제됩니다.', uploadError: '글꼴 파일을 불러올 수 없습니다', faceRejected: '브라우저가 이 글꼴을 미리보기에 표시할 수 없습니다. 프린터는 그래도 이를 받아들일 수 있습니다.', - nameTaken: '이 프린터 이름의 글꼴이 다른 바이트로 이미 캐시되어 있습니다. 파일 이름을 바꾸거나 먼저 캐시된 글꼴을 삭제하세요.', + nameTaken: '이 프린터 이름의 글꼴이 다른 바이트로 이미 캐시되어 있습니다. 다른 프린터 파일명을 사용하거나, 먼저 캐시된 글꼴을 삭제하세요.', nameUnusable: '파일 이름을 영문자와 숫자로 줄이면 프린터 이름이 남지 않습니다. 먼저 파일 이름을 바꾸세요.', inUse: '아직 텍스트 필드 또는 이 디자인의 별칭에서 사용 중입니다.', inProfile: '아직 프린터 프로필에서 제공 중입니다. 먼저 개체에서 제거하세요.', diff --git a/src/locales/lt.ts b/src/locales/lt.ts index 2ed8f4da..a253cca4 100644 --- a/src/locales/lt.ts +++ b/src/locales/lt.ts @@ -1708,6 +1708,13 @@ const lt = { printerFilename: 'Spausdintuvo failo pavadinimas', printerFilenamePlaceholder: 'pvz. ARIAL.TTF', upload: 'Įkelti', + fromComputer: 'Iš šio kompiuterio', + filterFonts: 'Filtruoti šriftus', + noSystemFonts: 'Nėra tinkamo įdiegto šrifto. Šriftai didesni nei 4 MiB ir šriftų rinkiniai yra praleidžiami.', + noFilterMatch: 'Nėra įdiegto šrifto, atitinkančio filtrą.', + systemFontsFailed: 'Nepavyko nuskaityti įdiegtų šriftų.', + restrictedLicense: 'Šrifto licencija draudžia įterpimą.', + variableFont: 'Kintamasis šriftas, spausdintuvas naudoja tik jo numatytąjį egzempliorių.', cancel: 'Atšaukti', delete: 'Ištrinti', deleteConfirm: 'Ištrinti šį šriftą iš naršyklės? Anksčiau išsaugoti dizainai, kurie jį naudoja, jo neteks.', @@ -1715,7 +1722,7 @@ const lt = { deleteHistoryManyFmt: 'Tai taip pat pašalins {n} žingsnius atšaukimo istorijoje, kurie jį naudoja.', uploadError: 'Nepavyko įkelti šrifto failo', faceRejected: 'Naršyklė negali atvaizduoti šio šrifto peržiūroje. Spausdintuvas jį vis tiek gali priimti.', - nameTaken: 'Su šiuo spausdintuvo pavadinimu podėlyje jau yra šriftas su kitais baitais. Pervadinkite failą arba pirmiausia ištrinkite podėlyje saugomą šriftą.', + nameTaken: 'Su šiuo spausdintuvo pavadinimu podėlyje jau yra šriftas su kitais baitais. Naudokite kitą spausdintuvo failo pavadinimą arba pirmiausia ištrinkite podėlyje saugomą šriftą.', nameUnusable: 'Sumažinus failo pavadinimą iki raidžių ir skaitmenų, spausdintuvo pavadinimo nelieka. Pirmiausia pervadinkite failą.', inUse: 'Jį vis dar naudoja teksto laukas arba šio dizaino alias.', inProfile: 'Jį vis dar teikia spausdintuvo profilis. Pirmiausia pašalinkite jį skiltyje Objektai.', diff --git a/src/locales/lv.ts b/src/locales/lv.ts index df006f73..eba65b45 100644 --- a/src/locales/lv.ts +++ b/src/locales/lv.ts @@ -1708,6 +1708,13 @@ const lv = { printerFilename: 'Printera faila nosaukums', printerFilenamePlaceholder: 'piem. ARIAL.TTF', upload: 'Augšupielādēt', + fromComputer: 'No šī datora', + filterFonts: 'Filtrēt fontus', + noSystemFonts: 'Nav piemērota instalēta fonta. Fonti, lielāki par 4 MiB, un fontu kolekcijas tiek izlaisti.', + noFilterMatch: 'Nav instalēta fonta, kas atbilst filtram.', + systemFontsFailed: 'Neizdevās nolasīt instalētos fontus.', + restrictedLicense: 'Fonta licence aizliedz iegulšanu.', + variableFont: 'Mainīgais fonts, printeris izmanto tikai tā noklusējuma versiju.', cancel: 'Atcelt', delete: 'Dzēst', deleteConfirm: 'Dzēst šo fontu no pārlūkprogrammas? Iepriekš saglabāti dizaini, kas to izmanto, to zaudēs.', @@ -1715,7 +1722,7 @@ const lv = { deleteHistoryManyFmt: 'Tas arī dzēsīs {n} soļus atsaukšanas vēsturē, kas to izmanto.', uploadError: 'Neizdevās ielādēt fontu failu', faceRejected: 'Pārlūkprogramma nevar attēlot šo fontu priekšskatījumā. Printeris to tomēr var pieņemt.', - nameTaken: 'Ar šo printera nosaukumu kešatmiņā jau ir fonts ar citiem baitiem. Pārdēvējiet failu vai vispirms dzēsiet kešatmiņā saglabāto fontu.', + nameTaken: 'Ar šo printera nosaukumu kešatmiņā jau ir fonts ar citiem baitiem. Izmantojiet citu printera faila nosaukumu vai vispirms dzēsiet kešatmiņā saglabāto fontu.', nameUnusable: 'Faila nosaukums, samazinot to līdz burtiem un cipariem, neatstāj nevienu printera nosaukumu. Vispirms pārdēvējiet failu.', inUse: 'To joprojām izmanto teksta lauks vai šī dizaina aliass.', inProfile: 'To joprojām nodrošina printera profils. Vispirms noņemiet to sadaļā Objekti.', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index ff113031..252d586d 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -1708,6 +1708,13 @@ const nl = { printerFilename: 'Printerbestandsnaam', printerFilenamePlaceholder: 'bijv. ARIAL.TTF', upload: 'Uploaden', + fromComputer: 'Vanaf deze computer', + filterFonts: 'Lettertypen filteren', + noSystemFonts: 'Geen geïnstalleerd lettertype past. Lettertypen boven 4 MiB en lettertypeverzamelingen worden weggelaten.', + noFilterMatch: 'Geen geïnstalleerd lettertype komt overeen met het filter.', + systemFontsFailed: 'De geïnstalleerde lettertypen konden niet worden gelezen.', + restrictedLicense: 'De licentie van het lettertype verbiedt insluiten.', + variableFont: 'Variabel lettertype, de printer gebruikt alleen de standaardinstantie.', cancel: 'Annuleren', delete: 'Verwijderen', deleteConfirm: 'Dit lettertype uit de browser verwijderen? Eerder opgeslagen ontwerpen die dit gebruiken, raken het kwijt.', @@ -1715,7 +1722,7 @@ const nl = { deleteHistoryManyFmt: 'Dit verwijdert ook {n} stappen in de geschiedenis van ongedaan maken die het gebruiken.', uploadError: 'Lettertypebestand kon niet worden geladen', faceRejected: 'De browser kan dit lettertype niet weergeven in de voorvertoning. De printer accepteert het mogelijk toch.', - nameTaken: 'Er staat al een lettertype met deze printernaam in de cache, met andere bytes. Hernoem het bestand, of verwijder eerst het gecachete lettertype.', + nameTaken: 'Er staat al een lettertype met deze printernaam in de cache, met andere bytes. Gebruik een andere printerbestandsnaam, of verwijder eerst het gecachete lettertype.', nameUnusable: 'De bestandsnaam levert geen printernaam meer op zodra deze wordt teruggebracht tot letters en cijfers. Hernoem eerst het bestand.', inUse: 'Wordt nog door een tekstveld of een alias van dit ontwerp gebruikt.', inProfile: 'Wordt nog geleverd door het printerprofiel. Verwijder het eerst onder Objecten.', diff --git a/src/locales/no.ts b/src/locales/no.ts index 59720fda..868bccd6 100644 --- a/src/locales/no.ts +++ b/src/locales/no.ts @@ -1708,6 +1708,13 @@ const no = { printerFilename: 'Skriverfilnavn', printerFilenamePlaceholder: 'f.eks. ARIAL.TTF', upload: 'Last opp', + fromComputer: 'Fra denne datamaskinen', + filterFonts: 'Filtrer skrifter', + noSystemFonts: 'Ingen installert skrift passer. Skrifter over 4 MiB og skriftsamlinger utelates.', + noFilterMatch: 'Ingen installert skrift samsvarer med filteret.', + systemFontsFailed: 'Klarte ikke lese de installerte skriftene.', + restrictedLicense: 'Skriftens lisens forbyr innbygging.', + variableFont: 'Variabel skrift, skriveren bruker bare standardinstansen.', cancel: 'Avbryt', delete: 'Slett', deleteConfirm: 'Slette denne skriften fra nettleseren? Design du har lagret tidligere og som bruker den, mister den.', @@ -1715,7 +1722,7 @@ const no = { deleteHistoryManyFmt: 'Dette fjerner også {n} trinn i angre-historikken som bruker det.', uploadError: 'Klarte ikke laste inn skriftfilen', faceRejected: 'Nettleseren kan ikke vise denne skriften i forhåndsvisningen. Skriveren godtar den kanskje likevel.', - nameTaken: 'Det finnes allerede en skrift bufret med dette skrivernavnet, med andre byte. Gi filen nytt navn, eller slett den bufrede skriften først.', + nameTaken: 'Det finnes allerede en skrift bufret med dette skrivernavnet, med andre byte. Bruk et annet skriverfilnavn, eller slett den bufrede skriften først.', nameUnusable: 'Filnavnet gir ikke noe skrivernavn igjen når det reduseres til bokstaver og siffer. Gi filen nytt navn først.', inUse: 'Fortsatt brukt av et tekstfelt eller et alias for dette designet.', inProfile: 'Leveres fortsatt av skriverprofilen. Fjern den først under Objekter.', diff --git a/src/locales/pl.ts b/src/locales/pl.ts index eeaebf04..8d03ba92 100644 --- a/src/locales/pl.ts +++ b/src/locales/pl.ts @@ -1708,6 +1708,13 @@ const pl = { printerFilename: 'Nazwa pliku drukarki', printerFilenamePlaceholder: 'np. ARIAL.TTF', upload: 'Prześlij', + fromComputer: 'Z tego komputera', + filterFonts: 'Filtruj czcionki', + noSystemFonts: 'Żadna zainstalowana czcionka nie odpowiada. Czcionki powyżej 4 MiB i kolekcje czcionek są pomijane.', + noFilterMatch: 'Żadna zainstalowana czcionka nie odpowiada filtrowi.', + systemFontsFailed: 'Nie można odczytać zainstalowanych czcionek.', + restrictedLicense: 'Licencja czcionki zabrania osadzania.', + variableFont: 'Czcionka zmienna, drukarka używa tylko jej domyślnej instancji.', cancel: 'Anuluj', delete: 'Usuń', deleteConfirm: 'Usunąć tę czcionkę z przeglądarki? Wcześniej zapisane projekty, które jej używają, ją utracą.', @@ -1715,7 +1722,7 @@ const pl = { deleteHistoryManyFmt: 'To usunie też {n} kroków w historii cofania, które jej używają.', uploadError: 'Nie można załadować pliku czcionki', faceRejected: 'Przeglądarka nie może wyświetlić tej czcionki w podglądzie. Drukarka może mimo to ją zaakceptować.', - nameTaken: 'Czcionka o tej nazwie drukarki jest już w pamięci podręcznej z innymi bajtami. Zmień nazwę pliku lub najpierw usuń czcionkę z pamięci podręcznej.', + nameTaken: 'Czcionka o tej nazwie drukarki jest już w pamięci podręcznej z innymi bajtami. Użyj innej nazwy pliku drukarki lub najpierw usuń czcionkę z pamięci podręcznej.', nameUnusable: 'Nazwa pliku po zredukowaniu do liter i cyfr nie pozostawia żadnej nazwy drukarki. Najpierw zmień nazwę pliku.', inUse: 'Nadal używana przez pole tekstowe lub alias tego projektu.', inProfile: 'Nadal dostarcza ją profil drukarki. Najpierw usuń ją w sekcji Obiekty.', diff --git a/src/locales/pt.ts b/src/locales/pt.ts index 0083d598..399a9e35 100644 --- a/src/locales/pt.ts +++ b/src/locales/pt.ts @@ -1708,6 +1708,13 @@ const pt = { printerFilename: 'Nome do arquivo da impressora', printerFilenamePlaceholder: 'ex. ARIAL.TTF', upload: 'Carregar', + fromComputer: 'Deste computador', + filterFonts: 'Filtrar fontes', + noSystemFonts: 'Nenhuma fonte instalada corresponde. Fontes acima de 4 MiB e coleções de fontes são deixadas de fora.', + noFilterMatch: 'Nenhuma fonte instalada corresponde ao filtro.', + systemFontsFailed: 'Não foi possível ler as fontes instaladas.', + restrictedLicense: 'A licença da fonte proíbe a incorporação.', + variableFont: 'Fonte variável, a impressora usa apenas a instância padrão.', cancel: 'Cancelar', delete: 'Excluir', deleteConfirm: 'Eliminar esta fonte do browser? Os designs guardados anteriormente que a usam vão perdê-la.', @@ -1715,7 +1722,7 @@ const pt = { deleteHistoryManyFmt: 'Isto também remove {n} passos do histórico de desfazer que a usam.', uploadError: 'Não foi possível carregar o arquivo de fonte', faceRejected: 'O navegador não consegue renderizar esta fonte na pré-visualização. A impressora pode aceitá-la mesmo assim.', - nameTaken: 'Já existe uma fonte em cache com este nome de impressora mas com bytes diferentes. Mude o nome do ficheiro, ou elimine primeiro a fonte em cache.', + nameTaken: 'Já existe uma fonte em cache com este nome de impressora mas com bytes diferentes. Utilize um nome de ficheiro de impressora diferente, ou elimine primeiro a fonte em cache.', nameUnusable: 'O nome do ficheiro não deixa nenhum nome de impressora depois de reduzido a letras e algarismos. Mude primeiro o nome do ficheiro.', inUse: 'Ainda é usada por um campo de texto ou um alias deste design.', inProfile: 'Ainda é fornecida pelo perfil de impressora. Remova-a primeiro em Objetos.', diff --git a/src/locales/ro.ts b/src/locales/ro.ts index fd055a4f..58a1795c 100644 --- a/src/locales/ro.ts +++ b/src/locales/ro.ts @@ -1708,6 +1708,13 @@ const ro = { printerFilename: 'Nume fișier imprimantă', printerFilenamePlaceholder: 'ex. ARIAL.TTF', upload: 'Încarcă', + fromComputer: 'De pe acest computer', + filterFonts: 'Filtrează fonturile', + noSystemFonts: 'Niciun font instalat nu se potrivește. Fonturile peste 4 MiB și colecțiile de fonturi sunt excluse.', + noFilterMatch: 'Niciun font instalat nu corespunde filtrului.', + systemFontsFailed: 'Fonturile instalate nu au putut fi citite.', + restrictedLicense: 'Licența fontului interzice încorporarea.', + variableFont: 'Font variabil, imprimanta folosește doar instanța implicită.', cancel: 'Anulează', delete: 'Șterge', deleteConfirm: 'Ștergeți acest font din browser? Design-urile salvate anterior care îl folosesc îl vor pierde.', @@ -1715,7 +1722,7 @@ const ro = { deleteHistoryManyFmt: 'Asta va elimina și {n} pași din istoricul de anulare care îl folosesc.', uploadError: 'Fisierul de font nu a putut fi incarcat', faceRejected: 'Browserul nu poate reda acest font în previzualizare. Imprimanta îl poate accepta totuși.', - nameTaken: 'Există deja un font stocat în cache cu acest nume de imprimantă, dar cu octeți diferiți. Redenumește fișierul sau șterge mai întâi fontul stocat în cache.', + nameTaken: 'Există deja un font stocat în cache cu acest nume de imprimantă, dar cu octeți diferiți. Folosește un alt nume de fișier imprimantă sau șterge mai întâi fontul stocat în cache.', nameUnusable: 'Numele fișierului nu lasă niciun nume de imprimantă odată redus la litere și cifre. Redenumește mai întâi fișierul.', inUse: 'Încă folosit de un câmp de text sau de un alias al acestui design.', inProfile: 'Încă este furnizat de profilul imprimantei. Elimină-l mai întâi din Obiecte.', diff --git a/src/locales/sk.ts b/src/locales/sk.ts index e611bd58..c9313107 100644 --- a/src/locales/sk.ts +++ b/src/locales/sk.ts @@ -1708,6 +1708,13 @@ const sk = { printerFilename: 'Názov súboru tlačiarne', printerFilenamePlaceholder: 'napr. ARIAL.TTF', upload: 'Nahrať', + fromComputer: 'Z tohto počítača', + filterFonts: 'Filtrovať písma', + noSystemFonts: 'Žiadne nainštalované písmo nevyhovuje. Písma nad 4 MiB a kolekcie písem sú vynechané.', + noFilterMatch: 'Žiadne nainštalované písmo nezodpovedá filtru.', + systemFontsFailed: 'Nainštalované písma sa nepodarilo načítať.', + restrictedLicense: 'Licencia písma zakazuje vkladanie.', + variableFont: 'Variabilné písmo, tlačiareň používa iba jeho predvolenú inštanciu.', cancel: 'Zrušiť', delete: 'Odstrániť', deleteConfirm: 'Odstrániť toto písmo z prehliadača? Skôr uložené návrhy, ktoré ho používajú, oň prídu.', @@ -1715,7 +1722,7 @@ const sk = { deleteHistoryManyFmt: 'Tým sa zároveň odstráni v histórii krokov späť aj {n} krokov, ktoré ho používajú.', uploadError: 'Súbor písma sa nepodarilo načítať', faceRejected: 'Prehliadač nedokáže zobraziť toto písmo v náhľade. Tlačiareň ho napriek tomu môže prijať.', - nameTaken: 'S týmto názvom tlačiarne je vo vyrovnávacej pamäti už písmo s inými bajtmi. Premenujte súbor, alebo najprv odstráňte uložené písmo z vyrovnávacej pamäte.', + nameTaken: 'S týmto názvom tlačiarne je vo vyrovnávacej pamäti už písmo s inými bajtmi. Použite iný názov súboru tlačiarne, alebo najprv odstráňte uložené písmo z vyrovnávacej pamäte.', nameUnusable: 'Názov súboru po zredukovaní na písmená a číslice neponecháva žiadny názov tlačiarne. Najprv premenujte súbor.', inUse: 'Stále ho používa textové pole alebo alias tohto návrhu.', inProfile: 'Stále ho poskytuje profil tlačiarne. Najprv ho odstráňte v časti Objekty.', diff --git a/src/locales/sl.ts b/src/locales/sl.ts index ab60acfc..bf0fd126 100644 --- a/src/locales/sl.ts +++ b/src/locales/sl.ts @@ -1708,6 +1708,13 @@ const sl = { printerFilename: 'Ime datoteke tiskalnika', printerFilenamePlaceholder: 'npr. ARIAL.TTF', upload: 'Naloži', + fromComputer: 'S tega računalnika', + filterFonts: 'Filtriraj pisave', + noSystemFonts: 'Nobena nameščena pisava ne ustreza. Pisave nad 4 MiB in zbirke pisav so izpuščene.', + noFilterMatch: 'Nobena nameščena pisava ne ustreza filtru.', + systemFontsFailed: 'Nameščenih pisav ni bilo mogoče prebrati.', + restrictedLicense: 'Licenca pisave prepoveduje vdelavo.', + variableFont: 'Spremenljiva pisava, tiskalnik uporablja samo njeno privzeto različico.', cancel: 'Prekliči', delete: 'Izbriši', deleteConfirm: 'Izbrišem to pisavo iz brskalnika? Prej shranjeni dizajni, ki jo uporabljajo, jo bodo izgubili.', @@ -1715,7 +1722,7 @@ const sl = { deleteHistoryManyFmt: 'S tem bo izbrisanih tudi {n} korakov v zgodovini razveljavitev, ki jo uporabljajo.', uploadError: 'Datoteke pisave ni bilo mogoče naložiti', faceRejected: 'Brskalnik ne more prikazati te pisave v predogledu. Tiskalnik jo lahko kljub temu sprejme.', - nameTaken: 'S tem imenom tiskalnika je v predpomnilniku že pisava z drugačnimi bajti. Preimenujte datoteko ali najprej izbrišite predpomnjeno pisavo.', + nameTaken: 'S tem imenom tiskalnika je v predpomnilniku že pisava z drugačnimi bajti. Uporabite drugo ime datoteke tiskalnika ali najprej izbrišite predpomnjeno pisavo.', nameUnusable: 'Ime datoteke po skrčitvi na črke in številke ne pusti nobenega imena tiskalnika. Najprej preimenujte datoteko.', inUse: 'Še vedno jo uporablja besedilno polje ali vzdevek tega oblikovanja.', inProfile: 'Še vedno jo zagotavlja profil tiskalnika. Najprej jo odstranite pod Predmeti.', diff --git a/src/locales/sr.ts b/src/locales/sr.ts index 72b9e8f2..228bf9ca 100644 --- a/src/locales/sr.ts +++ b/src/locales/sr.ts @@ -1708,6 +1708,13 @@ const sr = { printerFilename: 'Назив датотеке штампача', printerFilenamePlaceholder: 'нпр. ARIAL.TTF', upload: 'Отпреми', + fromComputer: 'Са овог рачунара', + filterFonts: 'Филтрирај фонтове', + noSystemFonts: 'Ниједан инсталирани фонт не одговара. Фонтови већи од 4 MiB и колекције фонтова су изостављени.', + noFilterMatch: 'Ниједан инсталирани фонт не одговара филтеру.', + systemFontsFailed: 'Инсталирани фонтови нису могли бити прочитани.', + restrictedLicense: 'Лиценца фонта не дозвољава уграђивање.', + variableFont: 'Променљиви фонт, штампач користи само његову подразумевану инстанцу.', cancel: 'Откажи', delete: 'Избриши', deleteConfirm: 'Обрисати овај фонт из претраживача? Раније сачувани дизајни који га користе изгубиће га.', @@ -1715,7 +1722,7 @@ const sr = { deleteHistoryManyFmt: 'Ово ће уклонити и {n} корака у историји опозивања који га користе.', uploadError: 'Датотека фонта није могла бити учитана', faceRejected: 'Прегледач не може да прикаже овај фонт у прегледу. Штампач га ипак може прихватити.', - nameTaken: 'Фонт са овим називом штампача већ је у кешу са другачијим бајтовима. Преименујте датотеку или прво избришите кеширани фонт.', + nameTaken: 'Фонт са овим називом штампача већ је у кешу са другачијим бајтовима. Користите други назив датотеке штампача или прво избришите кеширани фонт.', nameUnusable: 'Назив датотеке не оставља ниједан назив штампача када се сведе на слова и цифре. Прво преименујте датотеку.', inUse: 'Још увек га користи текстуално поље или алијас овог дизајна.', inProfile: 'Још увек га обезбеђује профил штампача. Прво га уклоните под Објекти.', diff --git a/src/locales/sv.ts b/src/locales/sv.ts index 04381c50..4840175e 100644 --- a/src/locales/sv.ts +++ b/src/locales/sv.ts @@ -1708,6 +1708,13 @@ const sv = { printerFilename: 'Skrivarfilnamn', printerFilenamePlaceholder: 't.ex. ARIAL.TTF', upload: 'Ladda upp', + fromComputer: 'Från den här datorn', + filterFonts: 'Filtrera typsnitt', + noSystemFonts: 'Inget installerat typsnitt passar. Typsnitt över 4 MiB och typsnittssamlingar utelämnas.', + noFilterMatch: 'Inget installerat typsnitt matchar filtret.', + systemFontsFailed: 'Det gick inte att läsa de installerade typsnitten.', + restrictedLicense: 'Typsnittets licens förbjuder inbäddning.', + variableFont: 'Variabelt typsnitt, skrivaren använder bara standardinstansen.', cancel: 'Avbryt', delete: 'Ta bort', deleteConfirm: 'Ta bort det här typsnittet från webbläsaren? Tidigare sparade designer som använder det förlorar det.', @@ -1715,7 +1722,7 @@ const sv = { deleteHistoryManyFmt: 'Detta tar också bort {n} steg i ångra-historiken som använder det.', uploadError: 'Det gick inte att ladda typsnittsfilen', faceRejected: 'Webbläsaren kan inte visa detta typsnitt i förhandsgranskningen. Skrivaren kan ändå acceptera det.', - nameTaken: 'Det finns redan ett typsnitt cachat med det här skrivarnamnet men med andra byte. Byt namn på filen, eller ta bort det cachade typsnittet först.', + nameTaken: 'Det finns redan ett typsnitt cachat med det här skrivarnamnet men med andra byte. Använd ett annat skrivarfilnamn, eller ta bort det cachade typsnittet först.', nameUnusable: 'Filnamnet lämnar inget skrivarnamn kvar när det reduceras till bokstäver och siffror. Byt namn på filen först.', inUse: 'Används fortfarande av ett textfält eller ett alias för denna design.', inProfile: 'Tillhandahålls fortfarande av skrivarprofilen. Ta bort det först under Objekt.', diff --git a/src/locales/tr.ts b/src/locales/tr.ts index b3a9a836..36fe9870 100644 --- a/src/locales/tr.ts +++ b/src/locales/tr.ts @@ -1708,6 +1708,13 @@ const tr = { printerFilename: 'Yazıcı dosya adı', printerFilenamePlaceholder: 'örn. ARIAL.TTF', upload: 'Yükle', + fromComputer: 'Bu bilgisayardan', + filterFonts: 'Yazı tiplerini filtrele', + noSystemFonts: 'Uygun yüklü yazı tipi yok. 4 MiB üzerindeki yazı tipleri ve yazı tipi koleksiyonları hariç tutulur.', + noFilterMatch: 'Filtreyle eşleşen yüklü yazı tipi yok.', + systemFontsFailed: 'Yüklü yazı tipleri okunamadı.', + restrictedLicense: 'Yazı tipinin lisansı gömmeyi yasaklıyor.', + variableFont: 'Değişken yazı tipi, yazıcı yalnızca varsayılan örneğini kullanır.', cancel: 'İptal', delete: 'Sil', deleteConfirm: 'Bu yazı tipi tarayıcıdan silinsin mi? Onu kullanan, daha önce kaydettiğiniz tasarımlar yazı tipini kaybeder.', @@ -1715,7 +1722,7 @@ const tr = { deleteHistoryManyFmt: 'Bu işlem onu kullanan geri alma geçmişindeki {n} adımı da siler.', uploadError: 'Yazı tipi dosyası yüklenemedi', faceRejected: 'Tarayıcı bu yazı tipini önizlemede gösteremiyor. Yazıcı yine de kabul edebilir.', - nameTaken: 'Bu yazıcı adına sahip bir yazı tipi farklı baytlarla zaten önbelleğe alınmış. Dosyayı yeniden adlandırın veya önce önbellekteki yazı tipini silin.', + nameTaken: 'Bu yazıcı adına sahip bir yazı tipi farklı baytlarla zaten önbelleğe alınmış. Farklı bir yazıcı dosya adı kullanın, veya önce önbellekteki yazı tipini silin.', nameUnusable: 'Dosya adı harf ve rakamlara indirgendiğinde hiçbir yazıcı adı bırakmıyor. Önce dosyayı yeniden adlandırın.', inUse: 'Hâlâ bir metin alanı veya bu tasarımın bir takma adı tarafından kullanılıyor.', inProfile: 'Hâlâ yazıcı profili tarafından sağlanıyor. Önce Nesneler altından kaldırın.', diff --git a/src/locales/zh-hans.ts b/src/locales/zh-hans.ts index b6a8ab08..5bce3462 100644 --- a/src/locales/zh-hans.ts +++ b/src/locales/zh-hans.ts @@ -1708,6 +1708,13 @@ const zhHans = { printerFilename: '打印机文件名', printerFilenamePlaceholder: '例如 ARIAL.TTF', upload: '上传', + fromComputer: '从此计算机', + filterFonts: '筛选字体', + noSystemFonts: '没有匹配的已安装字体。大于 4 MiB 的字体和字体集合将被排除。', + noFilterMatch: '没有与筛选条件匹配的已安装字体。', + systemFontsFailed: '无法读取已安装的字体。', + restrictedLicense: '该字体的许可证禁止嵌入。', + variableFont: '可变字体,打印机仅使用其默认实例。', cancel: '取消', delete: '删除', deleteConfirm: '要从浏览器中删除此字体吗?之前保存的、使用它的设计将失去它。', @@ -1715,7 +1722,7 @@ const zhHans = { deleteHistoryManyFmt: '这还会移除撤销历史记录中使用它的 {n} 步。', uploadError: '无法加载字体文件', faceRejected: '浏览器无法在预览中渲染此字体。打印机可能仍会接受它。', - nameTaken: '已有一个字体以不同的字节缓存在此打印机名称下。请重命名文件,或先删除缓存的字体。', + nameTaken: '已有一个字体以不同的字节缓存在此打印机名称下。请使用不同的打印机文件名,或先删除缓存的字体。', nameUnusable: '文件名简化为字母和数字后不会留下任何打印机名称。请先重命名文件。', inUse: '仍被文本字段或此设计的别名使用。', inProfile: '仍由打印机配置文件提供。请先在对象中移除它。', diff --git a/src/locales/zh-hant.ts b/src/locales/zh-hant.ts index ee332e3e..fc005dc0 100644 --- a/src/locales/zh-hant.ts +++ b/src/locales/zh-hant.ts @@ -1708,6 +1708,13 @@ const zhHant = { printerFilename: '印表機檔名', printerFilenamePlaceholder: '例如 ARIAL.TTF', upload: '上傳', + fromComputer: '從此電腦', + filterFonts: '篩選字體', + noSystemFonts: '沒有符合的已安裝字體。大於 4 MiB 的字體和字體集合將被排除。', + noFilterMatch: '沒有符合篩選條件的已安裝字體。', + systemFontsFailed: '無法讀取已安裝的字型。', + restrictedLicense: '該字體的授權禁止嵌入。', + variableFont: '可變字體,印表機僅使用其預設實例。', cancel: '取消', delete: '刪除', deleteConfirm: '要從瀏覽器中刪除這個字型嗎?先前儲存並使用它的設計將會失去它。', @@ -1715,7 +1722,7 @@ const zhHant = { deleteHistoryManyFmt: '這還會移除復原歷史記錄中使用它的 {n} 步。', uploadError: '無法載入字體檔案', faceRejected: '瀏覽器無法在預覽中顯示此字體。印表機可能仍會接受它。', - nameTaken: '已有一個字型以不同的位元組快取在此印表機名稱下。請重新命名檔案,或先刪除快取的字型。', + nameTaken: '已有一個字型以不同的位元組快取在此印表機名稱下。請使用不同的印表機檔名,或先刪除快取的字型。', nameUnusable: '檔案名稱簡化為字母和數字後不會留下任何印表機名稱。請先重新命名檔案。', inUse: '仍被文字欄位或此設計的別名使用。', inProfile: '仍由印表機設定檔提供。請先在物件中移除它。', diff --git a/src/registry/text.panel.tsx b/src/registry/text.panel.tsx index f46dacbd..7d736da8 100644 --- a/src/registry/text.panel.tsx +++ b/src/registry/text.panel.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from "react"; import type { ObjectTypeUi } from "./panelTypes"; import { useT } from "../hooks/useT"; import { buttonCls, inputCls, labelCls } from "../components/Properties/styles"; -import { getFontFamily, hasFontBytes, loadFontFile } from "@zplab/core/lib/fontCache"; +import { FONT_FILE_ACCEPT, getFontFamily, hasFontBytes, loadFontFile } from "@zplab/core/lib/fontCache"; import { useFontCacheVersion } from "../hooks/useFontCacheVersion"; import { useLabelStore } from "../store/labelStore"; import { currentObjects, currentPageLabel } from "../store/labelStore.selectors"; @@ -190,7 +190,7 @@ export const textPanel: ObjectTypeUi = { { const file = e.target.files?.[0]; diff --git a/src/test/systemFontCap.test.ts b/src/test/systemFontCap.test.ts new file mode 100644 index 00000000..6c913443 --- /dev/null +++ b/src/test/systemFontCap.test.ts @@ -0,0 +1,15 @@ +import { readFileSync } from "node:fs"; +import { describe, it, expect } from "vitest"; +import { MAX_FONT_BYTES } from "@zplab/core/lib/fontCache"; +import { LOCALE_CODES, loadLocale } from "../locales"; + +describe("the system font cap", () => { + it("is the cache's limit in Rust and in every hint text", async () => { + const rust = readFileSync("src-tauri/src/system_fonts.rs", "utf8").match(/const MAX_FONT_BYTES: u64 = ([^;]+);/)?.[1] ?? ""; + const product = rust.split("*").map((factor) => Number(factor.trim())).reduce((a, b) => a * b, 1); + expect(product).toBe(MAX_FONT_BYTES); + for (const code of LOCALE_CODES) { + expect((await loadLocale(code)).fonts.noSystemFonts, code).toContain(`${MAX_FONT_BYTES / 1024 / 1024} MiB`); + } + }); +});