diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f176b90..d305c09 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,7 +11,7 @@ - [ ] `dotnet build Rotula.sln -c Release` and `dotnet test Rotula.sln -c Release` pass - [ ] New behavior has tests; `Rotula.Core` stays at or above 80% line coverage - [ ] Public members have XML docs and non-obvious logic has a "why" comment (in English) -- [ ] New user-visible text exists in both `Strings.en.resx` and `Strings.es.resx` +- [ ] New user-visible text exists in all four languages: `Strings.en.resx`, `Strings.es.resx`, `Strings.pt-BR.resx` and `Strings.fr-CA.resx` - [ ] No network calls or telemetry added; light theme only - [ ] New dependencies are permissively licensed and listed in `THIRD-PARTY-NOTICES.md` - [ ] `CHANGELOG.md` updated under "Unreleased" when users would notice the change diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8443ad8..9d2d578 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,15 +79,24 @@ jobs: -p:Version=${{ steps.ver.outputs.version }} -o publish/win-x64-mcp + # The print engine (rotula.exe: command line and REST API), published the same way. + - name: Publish print engine single-file win-x64 + run: > + dotnet publish src/Rotula.Cli/Rotula.Cli.csproj -c Release -r win-x64 --self-contained true + -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true + -p:EnableCompressionInSingleFile=true -p:DebugType=none + -p:Version=${{ steps.ver.outputs.version }} + -o publish/win-x64-engine + # The zip is an UNSIGNED CI artifact for inspection only; the signed portable zip comes from the local build. - name: Stage files and create the portable zip shell: pwsh run: | $v = '${{ steps.ver.outputs.version }}' New-Item -ItemType Directory -Force artifacts, stage | Out-Null - Copy-Item publish/win-x64/Rotula.App.exe, publish/win-x64-mcp/Rotula.Mcp.exe stage/ + Copy-Item publish/win-x64/Rotula.App.exe, publish/win-x64-mcp/Rotula.Mcp.exe, publish/win-x64-engine/rotula.exe stage/ New-Item -ItemType Directory -Force stage/docs | Out-Null - Copy-Item docs/mcp.md stage/docs/ + Copy-Item docs/mcp.md, docs/print-engine.md stage/docs/ Copy-Item LICENSE, THIRD-PARTY-NOTICES.md, README.md stage/ Compress-Archive -Path stage/* -DestinationPath "artifacts/Rotula-$v-win-x64-portable-unsigned-ci.zip" -Force @@ -100,6 +109,10 @@ jobs: $alive = -not $p.HasExited if ($alive) { $p.Kill() } if (-not $alive) { throw "Rotula.App.exe exited early with code $($p.ExitCode)." } + # The print engine must start too: "version" prints the version and exits 0. + $out = & publish/win-x64-engine/rotula.exe version + if ($LASTEXITCODE -ne 0) { throw "rotula.exe version failed with exit code $LASTEXITCODE." } + Write-Host "rotula.exe version: $out" - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index ed4083e..81d2492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,19 @@ and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] ### Added +- **Print engine for scripts and warehouse systems** (`rotula.exe`): print `.rotula` labels with no window. `rotula print` takes a label, field values (`--set`) or records from a CSV, Excel or JSON file, and a Windows printer or network address, and prints through the designer's own print path: resolution conversion, darkness and speed, variables, questions (`--answer`, otherwise the designer default; never a sample value), copies, and serial numbers used once. Before every job it checks the printer status (`~HS`) and holds the job back on a fault; after the last job it waits until the printer reports it finished. `rotula status` and `rotula printers` report printers, `--json` gives a machine-readable result, and exit codes tell a script what happened. `rotula serve` offers the same as a REST API (`/api/v1/jobs`, printer status and lists, OpenAPI at `/openapi/v1.json`) that can run as a Windows service: off until started, on `127.0.0.1` by default, an API key on every call (only its hash is stored), and only the label folders and printers its settings file lists. One job at a time per printer. Messages in English, Spanish, Portuguese (Brazil) and French (Canada). A new `PrintEngine` policy lets an administrator switch it off. The setup program, the MSI and the portable zip include `rotula.exe` beside the app. See [docs/print-engine.md](docs/print-engine.md). +- **Label sheets for office printers** (File > Export > Label sheets (PDF), or Label sheets in the Print dialog): put the label on sheets for a laser or inkjet printer and save them as a PDF at the sheet's real size, each label at its own physical size. Pick a generic template (Letter 30-up, 20-up, 14-up, 10-up, 6-up, 80-up and half sheets; A4 3 x 8, 21-up, 65-up, 16-up, 14-up, 8-up and 2-up) or type your own layout, start at any position of a partly used sheet, and add crop marks or label outlines. With data loaded the ticked records go on the sheets with their copies, and serial numbers are used once, exactly as when printing. The layout is saved with the label; older versions of Rotula open such a file and ignore it. Sending the sheets straight to a Windows printer is not offered: print the PDF at actual size. +- **Machine-wide MSI for IT departments**: every release now also has `Rotula-X.Y.Z-win-x64.msi`, signed like the setup program. It installs to Program Files for every user with a Start menu entry and the `.rotula` association, upgrades older versions in place, and is made for silent deployment with Intune, Group Policy or Configuration Manager. The setup program can now also install for all users silently with `/ALLUSERS`. Silent install and uninstall commands, exit codes, Intune detection rules and Group Policy steps are in the new [docs/deployment.md](docs/deployment.md). +- **Settings your organization can manage**: administrators can fix the language and switch the update check on or off for everyone, suggest a default printer, keep add-ons in the user's own folder from loading, and switch printing or database access of the MCP server off, through Group Policy or Intune. Administrative templates (English and Spanish) are in `packaging/policies`. A setting fixed this way is greyed out in Rotula with "Managed by your organization". +- **Optional update check** (Help > Check for updates once a day): off unless you switch it on. Then Rotula asks GitHub once a day, with one anonymous request that carries nothing about you or your PC, whether a newer version exists, and shows a notice with a link to the release page. It never downloads or installs anything. +- **winget**: the manifests use schema 1.10, offer the MSI for `--scope machine` next to the per-user installer, and have a Spanish (es-MX) description. The submission steps are in docs/releasing.md. +- **Print station** for the warehouse floor: a locked, print-only screen started with `Rotula.exe --station ` (without a path it uses `%ProgramData%\Rotula\station.json`) or from **File > Open print station**. Labels show as large tiles with thumbnails and a search box; scanning a label's code opens it. Its questions are large fields that take keyboard-wedge scanner input: Enter or Tab moves to the next one, the last scan can print at once, and the label's own rules (required, length, pattern) are checked before anything is sent. Printing goes through the same print path as the Print dialog with the printer status check, and the result is shown in large plain words (ready, held back with the reason, printed and verified, not confirmed); the questions then reset for the next item, keeping the answers the station is told to keep. A `station.json` sets the labels and their scan codes, the printer of each label or a default, copy limits, whether the label's data may be printed, and an exit PIN stored only as a salted PBKDF2 hash (made with **File > Create print station PIN** or `Rotula.exe --station-pin`); five wrong PINs lock the PIN box for 30 seconds. Every setting is checked and a mistake is explained with the setting and line. See [docs/print-station.md](docs/print-station.md). +- **Printer status before and after printing** (ZPL `~HS`). Before each job Rotula asks the printer for its status and does not send while it reports it is out of labels, its printhead is open, the ribbon is out, it is paused, too hot, in diagnostics mode or its buffer is full; the Print dialog says which and what to do. After the job it waits (up to 30 seconds) until the printer reports it finished, then says "printed and verified", names a fault that appeared after the job (the labels still count as possibly printed, so no serial number is reused), or says it could not confirm. A printer that cannot answer never blocks printing. On by default for network printers and off for Windows printers, switchable in the Print dialog; "Check printer status" shows the decoded answer at any time. The MCP server has a matching `get_printer_status` tool. - **A gallery of eight showcase labels** in the README (GS1-128 pallet, parcel, food, wine, lab specimen, asset tag, shelf tags with data merge, print conditions). Their `.rotula` files and the CSV data are in `docs/showcase`, and the pictures are rendered with Rotula's own PNG export by a test that only runs when `ROTULA_RENDER_SHOWCASE=1` is set. - **Text fit for wrapped text** (Properties panel, Text fit): choose what a text block does when its text is longer than the box. "Shrink to fit" uses the largest font size, between a smallest and a largest size you set, at which all the text fits; "Grow height" adds lines, up to a limit you set, so all the text shows. With data merge every record gets the size or the lines its own text needs, the canvas shows the same size that prints, and "Do not warn" hides the does-not-fit warning when cutting the text off is what you want. Labels that do not use it are saved exactly as before; a label that does cannot be opened by older versions of Rotula. - **Print an object only when you want it**: in the new Printing section of the properties, mark an object "Do not print this object" (a note or guide that never reaches the printer or an export), set "Print only when" a field equals, does not equal, contains, is empty or is not empty a value (so a warning can appear on some labels only), and "Lock position" so nothing moves it by accident. The Objects list and the canvas show small No print, Hidden and Locked badges. +- **The Preview window shows a label the way it prints when it is set to print upside down (`^POI`) or mirrored (`^PMY`).** It opens on "As printed", says in one sentence why the picture is turned, and has an "As designed" switch to see it upright. The canvas stays upright for editing and the status bar says "Prints upside down" (or "Prints mirrored"). Imported ZPL with `^POI` / `^PON` and `^PMY` sets the label's setting, so the preview follows it. PNG, JPEG and PDF exports stay as designed. +- **Portuguese (Brazil) and French (Canada)**: the whole interface is now also available in Brazilian Portuguese and Canadian French (View > Language, where each language is listed in its own name), and the installer offers both. On first run Rotula follows Windows: a Portuguese desktop gets Portuguese (Brazil), a French one French (Canada), Spanish stays Spanish and anything else English. Numbers and dates follow the chosen language. A language you picked before stays as it was. The "Language of Rotula" policy (Group Policy / Intune) also accepts `pt-BR` and `fr-CA`. ### Fixed - **Windows fonts that are named after a style now draw in that style.** "Arial Black", "Segoe UI Semibold", "Segoe UI Black", "Bahnschrift Condensed" and similar names used to be replaced by the default font in the preview, PNG/PDF export and print picture; they now resolve to their parent family at the weight and width the name says. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8b13b0..b6da1ff 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,11 +10,18 @@ in [SECURITY.md](SECURITY.md), not a public issue. These are product rules; changes that break them will not be merged. -- **Offline-first**: no network calls, no telemetry, no update checks, no analytics. Printing to the printer address the user - entered is the only network traffic. +- **Offline-first**: no telemetry, no analytics, and no network traffic unless the user asks for it: printing to the printer + address the user entered and the databases the user connects. The one exception is the **opt-in update check**: it sends + nothing unless the user (Help menu) or an administrator (policy) explicitly switches it on, and then it is at most one + anonymous `GET` of the public GitHub "latest release" API a day, with nothing about the user or the computer in it. It never + downloads or installs anything. Do not add other network calls, and do not add anything to that request. The print engine's + REST server (`rotula serve`) is not a call out: it listens only when someone starts it, only on the addresses in its settings + file (`127.0.0.1` by default), and talks only to the printers that file allows. - **Light theme only.** -- **Spanish and English** through `.resx` resources (`src/Rotula.App/Resources/Strings.en.resx` and `Strings.es.resx`). Every new - user-visible string needs both. Core returns message keys, not text. +- **English, Spanish, Portuguese (Brazil) and French (Canada)** through `.resx` resources in `src/Rotula.App/Resources` + (`Strings.en.resx`, `Strings.es.resx`, `Strings.pt-BR.resx`, `Strings.fr-CA.resx`). Every new user-visible string needs all + four: en, es, pt-BR, fr-CA, with the same `{0}` placeholders and `_` access keys. French follows Canadian (OQLF) usage, with + a non-breaking space before `:` `;` `!` `?` and inside « ». Core returns message keys, not text. - **Permissive dependencies only** (MIT, Apache-2.0, BSD and similar). Add any new package to [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) in the same pull request. Discuss new dependencies in an issue first. - **Plain-language messages**: say what happened and what the user can do next. diff --git a/README.es.md b/README.es.md index bcf2c95..26f6e54 100644 --- a/README.es.md +++ b/README.es.md @@ -41,6 +41,7 @@ Diséñala, previsualízala, combina tus datos e imprímela. Sin cuenta, sin nub - [Etiquetas y rollos de la biblioteca](#etiquetas-y-rollos-de-la-biblioteca) - [Fuentes de bases de datos](#fuentes-de-bases-de-datos) - [Impresión](#impresión) +- [Puesto de impresión](#puesto-de-impresión) - [Servidor MCP](#servidor-mcp) - [Códigos de barras compatibles](#códigos-de-barras-compatibles) - [Idiomas](#idiomas) @@ -74,8 +75,12 @@ Siempre la versión más reciente: [github.com/P4Software/Rotula/releases/latest |---|---|---| | **Instalador** | `Rotula--win-x64-setup.exe` | Instalación por usuario, acceso en el menú Inicio e icono de escritorio opcional. Firmado digitalmente | | **Portátil** | `Rotula--win-x64-portable.zip` | Descomprime en cualquier carpeta y haz doble clic en `Rotula.App.exe`. No instala nada. No necesita .NET | +| **MSI (para TI)** | `Rotula--win-x64.msi` | Instalación para todo el equipo en Archivos de programa, para todos los usuarios, con Intune, directiva de grupo o Configuration Manager. Firmado. Consulta la [instalación en empresas](docs/deployment.md) (en inglés) | | **winget** | `winget install P4Software.Rotula` | Próximamente. El manifiesto del paquete está preparado, pero aún no fue aceptado en el catálogo de winget | +Para instalar Rotula en toda una organización (instalación silenciosa, el MSI, Intune, configuración por directiva de grupo): +[docs/deployment.md](docs/deployment.md) (en inglés). + Cada versión incluye además un archivo `SHA256SUMS.txt`.
@@ -134,7 +139,10 @@ QR, cuadros, líneas, círculos e imágenes monocromáticas (con umbral o tramad ### Vista previa e impresión -La vista previa muestra la etiqueta más grande, tal como se dibujará, y dice con claridad que es aproximada. El cuadro de +La vista previa muestra la etiqueta más grande, tal como se dibujará, y dice con claridad que es aproximada. Una etiqueta +configurada para imprimirse al revés (`^POI`) o en espejo (`^PMY`) se muestra "Como se imprime", con una frase que explica +por qué; cambia a "Como se diseñó" para verla derecha. El lienzo siempre queda derecho para editar, y la barra de estado +indica "Se imprime al revés". El cuadro de impresión lee la resolución y el tamaño de papel desde el controlador de la impresora, puede enviar una etiqueta de prueba e imprime en una impresora de Windows o en una impresora de red. @@ -169,9 +177,10 @@ salida es determinista y en ASCII puro, así que puedes versionarla, compararla | **Bases de datos** | SQL Server, PostgreSQL, MySQL/MariaDB, SQLite y ODBC, más ajustes predefinidos para servicios en la nube comunes ([detalles](#fuentes-de-bases-de-datos)) | | **Cobertura ZPL** | 222 comandos ZPL revisados uno por uno en una [página de cobertura generada](docs/zpl-coverage.md) (en inglés) | | **Servidor MCP** | Un programa aparte que permite a los clientes compatibles con MCP diseñar y revisar etiquetas ([detalles](#servidor-mcp)) | -| **Impresión** | Cola de Windows (RAW, incluido USB) y puerto TCP 9100, detección de DPI y papel, perfiles de impresora, etiqueta de prueba | +| **Impresión** | Cola de Windows (RAW, incluido USB) y puerto TCP 9100, detección de DPI y papel, perfiles de impresora, etiqueta de prueba, comprobación del estado de la impresora antes y después de cada trabajo | +| **Puesto de impresión** | Una pantalla bloqueada, solo para imprimir, pensada para el almacén: elige o escanea una etiqueta, responde a sus preguntas con un lector de códigos, imprime y lee la respuesta de la impresora en letra grande y palabras sencillas ([detalles](#puesto-de-impresión)) | | **Resolución** | Convierte una etiqueta entre 203, 300 y 600 dpi conservando su tamaño físico | -| **Idiomas** | Español e inglés, con cambio en vivo | +| **Idiomas** | Español, inglés, portugués (Brasil) y francés (Canadá), con cambio en vivo | --- @@ -210,8 +219,9 @@ El software de etiquetas ha significado casi siempre una licencia de pago, una c es un único programa de escritorio que hace el trabajo y no estorba. - **Gratis y con licencia MIT.** Úsalo en casa, en la oficina, en un almacén o dentro de un producto. Sin puestos ni planes. -- **Sin conexión por diseño.** Sin analítica, sin búsqueda de actualizaciones, sin sincronización en la nube. El único tráfico - de red que Rotula puede generar es la etiqueta que envías a la dirección de impresora que escribiste. +- **Sin conexión por diseño.** Sin analítica, sin telemetría, sin sincronización en la nube. El único tráfico de red que + Rotula puede generar es la etiqueta que envías a la dirección de impresora que escribiste, las bases de datos que conectas + y, solo si la activas, una búsqueda de actualizaciones anónima al día. - **ZPL honesto.** La misma etiqueta con los mismos datos siempre da ZPL idéntico byte a byte, en ASCII puro. - **Errores que puedes resolver.** Un dígito verificador EAN-13 inválido o un carácter que no existe en Code 39 se reporta en lenguaje sencillo, con lo que debes hacer a continuación. @@ -333,7 +343,8 @@ transferencia térmica) y el seguimiento. - **Qué sabe.** Tamaño en milímetros y pulgadas, radio de las esquinas, etiquetas a lo ancho, tamaño del núcleo, etiquetas por rollo y de dónde sale el dato. Un medio cuyo tamaño no está contrastado aparece marcado y, al aplicarlo, se muestra un aviso. - **Sin inventar.** La separación entre etiquetas casi siempre se desconoce, así que Rotula indica que calibres la impresora en - lugar de adivinar. En medios con varias etiquetas por fila o en hojas solo se aplica el tamaño de una etiqueta, y Rotula lo dice. + lugar de adivinar. En rollos con varias etiquetas por fila solo se aplica el tamaño de una etiqueta, y Rotula lo dice. Un medio en hojas abre la + exportación de [hojas de etiquetas](#hojas-de-etiquetas-impresoras-de-oficina) con el diseño que le corresponde. - **Tus propios medios.** Guarda un tamaño personalizado como medio, o importa una lista de suministros (`.csv` o `.xlsx`) y revisa las filas antes de conservarlas. Los precios nunca se leen. Tus medios se guardan en `%AppData%\Rotula\stocks`. - **Favoritos y recientes** se recuerdan en tu PC. @@ -379,11 +390,51 @@ Rotula envía el ZPL como datos RAW, así que el controlador lo transmite sin to - **Perfiles de impresora.** La resolución, el seguimiento de medios, la oscuridad (0 a 30) y la velocidad (1 a 14) se recuerdan junto con la impresora. - **Etiqueta de prueba.** Una pequeña etiqueta de muestra comprueba la conexión, el ancho y los caracteres con tilde. +- **Estado de la impresora.** Antes de un trabajo Rotula puede preguntar a la impresora su estado (el comando ZPL `~HS`) y + retener el trabajo mientras no tenga etiquetas, esté abierta, sin cinta, en pausa o demasiado caliente. Después espera a que + la impresora indique que terminó y dice "impreso y verificado", nombra el fallo o avisa de que no pudo confirmarlo. Viene + activado para impresoras de red; para una impresora de Windows actívalo en el cuadro Imprimir (muchos controladores USB no + pueden responder y entonces se imprime sin la comprobación). "Comprobar estado de la impresora" muestra la respuesta cuando + quieras. - **Fallos en lenguaje sencillo.** Impresora desconectada, conexión rechazada, host inalcanzable, tiempo agotado o un carácter que el juego de caracteres no puede imprimir: cada caso dice qué pasó y qué hacer. Rotula no incluye controladores de impresora. Más información en [docs/printing.md](docs/printing.md) (en inglés). +### Hojas de etiquetas (impresoras de oficina) + +**Archivo > Exportar > Hojas de etiquetas (PDF)** (o **Hojas de etiquetas** en el cuadro Imprimir) coloca la etiqueta en hojas para +una impresora láser o de inyección de tinta: Carta de 30 (2-5/8 x 1 in), 20, 14, 10 de envío (4 x 2 in), 6, 80 y media hoja (2 por hoja), +A4 de 3 x 8 (70 x 37 mm), 21, 65, 16, 14, 8 y 2 por hoja, o un diseño personalizado (papel, márgenes, tamaño de la etiqueta, etiquetas +a lo ancho y a lo alto, distancia entre etiquetas). + +- **Empezar en la posición** aprovecha una hoja ya empezada: haz clic en la primera posición libre de la vista previa. +- **La combinación de datos y los números de serie** funcionan como al imprimir: los registros marcados con sus copias, y cada + número de serie se usa una sola vez. +- **Marcas de corte** en los márgenes, o un **contorno fino** de cada etiqueta para probar en papel normal. +- El resultado es un PDF al tamaño real de la hoja, con cada etiqueta a su propio tamaño físico. Imprímelo a **tamaño real (100 %)**. + El diseño de la hoja se guarda con la etiqueta. Las plantillas se describen por sus medidas; las hojas de distintos proveedores + pueden variar un milímetro, así que imprime primero una página de prueba en papel normal. + +--- + +## Puesto de impresión + +Una pantalla solo para imprimir, para operarios, que se abre con `Rotula.exe --station ` (o **Archivo > Abrir puesto de +impresión**). Muestra las etiquetas de una carpeta como mosaicos grandes, sin menús ni herramientas de diseño. + +- **Elegir o escanear.** Toca un mosaico, escribe parte de su nombre o escanea el código que el puesto asigna a la etiqueta. +- **Pensado para lectores.** Las preguntas de la etiqueta son campos grandes; un lector de códigos de tipo teclado rellena uno y su + Intro o Tab pasa al siguiente. Con "imprimir al último escaneo" el último escaneo imprime en el acto. Antes se comprueban las + reglas de la propia etiqueta (obligatorio, longitud, patrón), y un escaneo rechazado se queda en su campo. +- **Resultado claro.** Lista, retenida (por ejemplo "el cabezal está abierto"), impresa y verificada, o sin confirmar, en letra + grande y palabras sencillas. Después las preguntas se vacían para la siguiente pieza; se pueden conservar las respuestas elegidas. +- **Bloqueado.** Para salir hace falta un PIN cuando se ha definido (solo se guarda un hash con sal). Las impresoras, los límites + de copias, la impresión de datos y el PIN vienen de un `station.json` junto a las etiquetas o en `%ProgramData%\Rotula`, así que + el puesto se puede configurar por directiva. + +Más información en [docs/print-station.md](docs/print-station.md) (en inglés). + --- ## Códigos de barras compatibles @@ -418,16 +469,21 @@ confirmación en dos pasos. El instalador lo coloca junto a la aplicación, por ## Idiomas -La interfaz está disponible en **español** y en **inglés**. De forma predeterminada Rotula sigue el idioma de Windows, y -puedes cambiarlo en cualquier momento desde **Ver > Idioma**, sin reiniciar. El instalador también está en ambos idiomas. +La interfaz está disponible en **español**, **inglés**, **portugués (Brasil)** y **francés (Canadá)**. De forma +predeterminada Rotula sigue el idioma de Windows (cualquier portugués usa portugués de Brasil, cualquier francés usa francés +de Canadá, y un idioma que Rotula no tiene usa inglés), y puedes cambiarlo en cualquier momento desde **Ver > Idioma**, sin +reiniciar. Los números y las fechas siguen el idioma elegido. El instalador también está en los cuatro idiomas. --- ## Privacidad -Rotula funciona totalmente sin conexión. No tiene telemetría, ni analítica, ni búsqueda de actualizaciones, ni cuentas, y no -hace llamadas de red. El único tráfico de red que puede generar es la etiqueta que envías a la dirección de impresora de red -que indicaste. Tus preferencias (idioma, unidades, archivos recientes, última impresora) son un pequeño archivo JSON en +Rotula funciona totalmente sin conexión. No tiene telemetría, ni analítica, ni cuentas, y no hace llamadas de red por su +cuenta. El único tráfico de red que puede generar es la etiqueta que envías a la dirección de impresora de red que indicaste +y las bases de datos a las que te conectas. **Ayuda > Buscar actualizaciones una vez al día** está desactivado hasta que lo +activas: entonces Rotula pregunta a GitHub una vez al día cuál es la versión más reciente (una sola solicitud anónima, no se +envía nada sobre ti ni sobre tu PC) y muestra un enlace cuando hay una más nueva. Nunca descarga ni instala nada. Un +administrador puede activarla o desactivarla para todos ([instalación en empresas](docs/deployment.md), en inglés). Tus preferencias (idioma, unidades, archivos recientes, última impresora) son un pequeño archivo JSON en `%AppData%\Rotula`. Si el programa falla, escribe un registro en `%AppData%\Rotula\rotula.log` en tu PC y no lo envía a ningún lado. --- @@ -498,7 +554,7 @@ flowchart LR | `src/Rotula.Mcp` | El servidor MCP (un ejecutable aparte) | | `src/Rotula.App` | La aplicación WPF | | `tests/` | Pruebas xUnit y los archivos ZPL de referencia | -| `packaging/` | Script del instalador y plantillas del manifiesto de winget | +| `packaging/` | Script del instalador, proyecto del MSI, plantillas de directiva de grupo y plantillas del manifiesto de winget | | `docs/` | [Cobertura ZPL](docs/zpl-coverage.md), [catálogo de medios](docs/stock-catalog.md), [proveedores de datos](docs/data-providers.md), [servidor MCP](docs/mcp.md), [impresión](docs/printing.md), [publicación de versiones](docs/releasing.md), [decisiones de diseño](docs/decisions.md) (en inglés) | --- @@ -525,7 +581,6 @@ No es compatible hoy, y está abierto a discusión: - Un mercado de plantillas de etiquetas - Sincronización en la nube - macOS y Linux -- Diseños de hojas con varias etiquetas ¿Tienes un caso de uso que te importa? [Abre una solicitud de función](https://github.com/P4Software/Rotula/issues/new/choose). @@ -540,8 +595,9 @@ Léelas antes de depender de Rotula para etiquetas de producción, e **imprime s impresa. La aplicación lo avisa. - **Fuentes de mapa de bits y `FontWidth`.** Las fuentes de mapa de bits integradas de la impresora no se reproducen; el escalado de ancho solo se aproxima en la vista previa. -- **`^PO` (orientación de impresión) no se previsualiza.** Se escribe en el ZPL, pero la vista previa siempre muestra la - orientación normal. +- **La orientación de impresión (`^PO`) y el espejo (`^PM`) solo se muestran en la ventana Vista previa.** El lienzo y las + exportaciones PNG, JPEG y PDF muestran la etiqueta como se diseñó (derecha); las exportaciones son el diseño, no una imagen + de lo que sale de la impresora. - **Las variables no sobreviven a un viaje de ida y vuelta por ZPL.** ZPL no puede expresar `{{nombre}}`, así que el ZPL importado es texto simple hasta que lo edites. - **El ZPL sin procesar no se escala al convertir el DPI.** Los comandos desconocidos conservados como ZPL sin procesar diff --git a/README.md b/README.md index 3746db9..2eeb9d0 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,9 @@ Design it, preview it, merge your data, print it. No account, no cloud, no telem - [Label stocks](#label-stocks) - [Database sources](#database-sources) - [Printing](#printing) +- [Print station](#print-station) - [MCP server](#mcp-server) +- [Print engine (command line and REST API)](#print-engine-command-line-and-rest-api) - [Barcode support](#barcode-support) - [Languages](#languages) - [Privacy](#privacy) @@ -74,8 +76,11 @@ Always the newest version: [github.com/P4Software/Rotula/releases/latest](https: |---|---|---| | **Installer** | `Rotula--win-x64-setup.exe` | Per-user install, Start menu entry, optional desktop icon. Code-signed | | **Portable** | `Rotula--win-x64-portable.zip` | Unzip anywhere and double-click `Rotula.App.exe`. Nothing is installed. No .NET needed | +| **MSI (for IT)** | `Rotula--win-x64.msi` | Machine-wide install to Program Files for every user, for Intune, Group Policy or Configuration Manager. Code-signed. See [deployment](docs/deployment.md) | | **winget** | `winget install P4Software.Rotula` | Coming soon. The package manifest is prepared but not yet accepted into the winget catalog | +Installing Rotula for a whole organization (silent install, the MSI, Intune, Group Policy settings): [docs/deployment.md](docs/deployment.md). + Every release also carries a `SHA256SUMS.txt` file.
@@ -132,7 +137,9 @@ dithering). ### Preview and print -A live preview shows the label larger, as it will be drawn, and says plainly that it is approximate. The print dialog +A live preview shows the label larger, as it will be drawn, and says plainly that it is approximate. A label set to print +upside down (`^POI`) or as a mirror image (`^PMY`) is shown "As printed" with a sentence saying why; switch to "As designed" +to see it upright. The canvas always stays upright for editing, and the status bar says "Prints upside down". The print dialog reads the resolution and paper size from the printer driver, can send a test label, and prints to a Windows printer or a network printer. @@ -166,9 +173,10 @@ deterministic and plain ASCII, so you can version it, diff it or send it with an | **Database sources** | SQL Server, PostgreSQL, MySQL/MariaDB, SQLite and ODBC, plus presets for common cloud services ([details](#database-sources)) | | **ZPL coverage** | 222 ZPL commands tracked one by one on a [generated coverage page](docs/zpl-coverage.md) | | **MCP server** | A separate program that lets MCP-capable clients design and check labels ([details](#mcp-server)) | -| **Printing** | Windows spooler (RAW, including USB) and TCP port 9100, DPI and paper detection, printer profiles, test label | +| **Printing** | Windows spooler (RAW, including USB) and TCP port 9100, DPI and paper detection, printer profiles, test label, printer status check before and after a job | +| **Print station** | A locked, print-only screen for the warehouse floor: pick or scan a label, answer its questions with a barcode scanner, print, and read the printer's answer in large plain words ([details](#print-station)) | | **Resolution** | Convert a label between 203, 300 and 600 dpi while keeping its physical size | -| **Languages** | English and Spanish, switchable live | +| **Languages** | English, Spanish, Portuguese (Brazil) and French (Canada), switchable live | --- @@ -207,8 +215,9 @@ Label software has long meant a paid license, an account, a subscription or a se does the job and stays out of the way. - **Free and MIT licensed.** Use it at home, at work, in a warehouse or inside a product. No seats, no tiers. -- **Offline by design.** No analytics, no update checks, no cloud sync. The only network traffic Rotula can produce is the - label you send to the printer address you typed. +- **Offline by design.** No analytics, no telemetry, no cloud sync. The only network traffic Rotula can produce is the + label you send to the printer address you typed, the databases you connect, and, only if you switch it on, one anonymous + daily update check. - **Honest ZPL.** The same label and data always give byte-identical ZPL, in plain ASCII. - **Errors you can act on.** An invalid EAN-13 check digit or a Code 39 character that does not exist is reported in plain language, with what to do next. @@ -327,7 +336,8 @@ type. Pick a result and Rotula sets the label size, media type (direct thermal o - **What it knows.** Size in millimetres and inches, die-cut corner radius, labels across, core size, labels per roll and where the data comes from. A stock whose size is not cross-checked is marked, and applying it shows a warning. - **Honest about gaps.** The gap between labels is usually unknown, so Rotula says to calibrate the printer instead of guessing. - Multi-up and sheet stocks apply the label size only, and Rotula says so. + Multi-up roll stocks apply the label size only, and Rotula says so. A sheet stock opens the [label sheets](#label-sheets-office-printers) + export on the matching layout. - **Your own stocks.** Save a custom size as a stock, or import a supplies list (`.csv` or `.xlsx`) and review the rows before they are kept. Prices are never read. User stocks live in `%AppData%\Rotula\stocks`. - **Favourites and recent** stocks are remembered on your PC. @@ -371,11 +381,47 @@ Rotula sends ZPL as RAW data, so the driver passes it through untouched. open design to the printer's resolution and undo it with `Ctrl+Z`. - **Printer profiles.** Resolution, media tracking, darkness (0 to 30) and speed (1 to 14) are remembered with the printer. - **Test label.** A small sample label checks the connection, the width and accented characters. +- **Printer status.** Before a job Rotula can ask the printer for its status (the ZPL `~HS` command) and hold the job back + while the printer is out of labels, open, out of ribbon, paused or too hot. Afterwards it waits until the printer reports it + finished and says "printed and verified", names the fault, or says it could not confirm. On by default for network printers; + for a Windows printer switch it on in the Print dialog (many USB drivers cannot answer, and printing then goes ahead without + the check). "Check printer status" shows the answer at any time. - **Plain-language failures.** Offline, refused connection, unreachable host, timeout or a character the character set cannot print: each says what happened and what to do. Rotula bundles no printer drivers. More in [docs/printing.md](docs/printing.md). +### Label sheets (office printers) + +**File > Export > Label sheets (PDF)** (or **Label sheets** in the Print dialog) puts the label on sheets for a laser or inkjet +printer: Letter 30-up (2-5/8 x 1 in), 20-up, 14-up, 10-up shipping (4 x 2 in), 6-up, 80-up, 2-up half sheets, A4 3 x 8 (70 x 37 mm), +21-up, 65-up, 16-up, 14-up, 8-up, 2-up, or a custom layout (paper, margins, label size, labels across and down, distance between labels). + +- **Start at position** uses up a partly used sheet: click the first free position in the preview. +- **Data merge and serial numbers** work as when printing: the ticked records with their copies, and every serial number used once. +- **Crop marks** in the margins, or a **thin outline** of each label for a test on plain paper. +- The result is a PDF at the sheet's real size with every label at its own physical size. Print it at **actual size (100 %)**. The + layout is saved with the label. Templates are described by their dimensions; sheets of different suppliers can differ by a + millimetre, so print one test page on plain paper first. + +--- + +## Print station + +A print-only screen for operators, started with `Rotula.exe --station ` (or **File > Open print station**). It shows +the labels of one folder as large tiles, with no menus and no design tools. + +- **Pick or scan.** Tap a tile, type part of its name, or scan the code the station gives the label. +- **Scanner friendly.** The label's questions are large fields; a keyboard-wedge scanner fills one and its Enter or Tab moves to + the next. With "print on last scan" the last scan prints at once. The label's own rules (required, length, pattern) are + checked first, and a refused scan stays in its field. +- **Clear result.** Ready, held back (for example "the printhead is open"), printed and verified, or could not confirm, in + large plain words. The questions then reset for the next item; chosen answers can be kept. +- **Locked.** Leaving needs a PIN when one is set (stored only as a salted hash). Printers, copy limits, data printing and the + PIN come from a `station.json` next to the labels or in `%ProgramData%\Rotula`, so the station can be set up by policy. + +More in [docs/print-station.md](docs/print-station.md). + --- ## Barcode support @@ -407,19 +453,49 @@ on in `rotula-mcp.settings.json`. Printing needs a two-step confirmation. The in --- +## Print engine (command line and REST API) + +`rotula.exe` prints Rotula labels with no window, for scripts, scheduled tasks and warehouse systems (WMS, ERP, shipping software). +It uses the designer's own print path: resolution conversion, darkness and speed, variables and questions, data merge, and serial +numbers that are reserved before a job and used up only for labels that reached the printer. Before every job it asks the printer +for its status (ZPL `~HS`) and holds the job back while the printer is out of labels, open, paused or out of ribbon; after the last +job it waits until the printer reports it finished. Results come back as exit codes or JSON. + +```bat +rotula print carton.rotula --printer "Dock 1" --data wave-1207.csv --reference WAVE-1207 --json +rotula status --host 10.20.0.31 +``` + +`rotula serve` exposes the same engine as a REST API (`POST /api/v1/jobs`, printer lists and status, an OpenAPI description) that +can run as a Windows service. It is off until you start it, listens on `127.0.0.1` unless you configure otherwise, needs an API +key on every call, and prints only labels from the folders and to the printers its settings file lists. Messages follow the +caller's language (English, Spanish, Portuguese (Brazil) or French (Canada)). Commands, API, settings and exit codes: +[docs/print-engine.md](docs/print-engine.md). The installer and the MSI place `rotula.exe` beside the app (by default +`%LOCALAPPDATA%\Programs\Rotula\rotula.exe`, or `C:\Program Files\Rotula\rotula.exe` for all users). + +--- + ## Languages -The interface is available in **English** and **Spanish**. By default Rotula follows the Windows language, and you can -switch at any time from **View > Language**, with no restart. The installer is also available in both languages. +The interface is available in **English**, **Spanish**, **Portuguese (Brazil)** and **French (Canada)**. By default Rotula +follows the Windows language (any Portuguese gets Portuguese (Brazil), any French gets French (Canada), and a language +Rotula does not have gets English), and you can switch at any time from **View > Language**, with no restart. Numbers and +dates follow the language you choose. The installer is also available in all four languages. --- ## Privacy -Rotula works fully offline. It has no telemetry, no analytics, no update checks, no account and makes no network calls. The -only network traffic it can produce is the label you send to the network printer address you entered. Your settings +Rotula works fully offline. It has no telemetry, no analytics, no account and makes no network calls of its own. The +only network traffic it can produce is the label you send to the network printer address you entered and the databases you +connect to. **Help > Check for updates once a day** is off until you switch it on: then Rotula asks GitHub once a day which +version is the newest (one anonymous request, nothing about you or your PC is sent) and shows a link when there is a newer +one. It never downloads or installs anything. An administrator can switch the check on or off for everyone +([deployment](docs/deployment.md)). Your settings (language, units, recent files, last printer) are a small JSON file in `%AppData%\Rotula`. If the program ever fails, it writes a log to `%AppData%\Rotula\rotula.log` on your PC and sends it nowhere. +The print engine's REST server (`rotula serve`) listens +for calls only when you start it, only on the addresses in its settings file, and makes no outgoing calls except to printers. --- @@ -487,9 +563,11 @@ flowchart LR | `src/Rotula.Printing` | Windows spooler and TCP 9100 printing, printer detection | | `src/Rotula.Data` | Database providers, presets, the read-only query guard and the connection store | | `src/Rotula.Mcp` | The MCP server (a separate executable) | +| `src/Rotula.Engine` | The headless print engine: print requests, printer status, settings and API keys. No UI | +| `src/Rotula.Cli` | `rotula.exe`: the print engine's command line and REST server | | `src/Rotula.App` | The WPF application | | `tests/` | xUnit tests and the golden ZPL fixtures | -| `packaging/` | Installer script and winget manifest templates | +| `packaging/` | Installer script, MSI project, Group Policy templates and winget manifest templates | | `docs/` | [ZPL coverage](docs/zpl-coverage.md), [stock catalogue](docs/stock-catalog.md), [database providers](docs/data-providers.md), [MCP server](docs/mcp.md), [printing](docs/printing.md), [releasing](docs/releasing.md), [design decisions](docs/decisions.md) | --- @@ -516,7 +594,6 @@ Not supported today, and open for discussion: - A label template marketplace - Cloud sync - macOS and Linux -- Multi-label sheet layouts Have a use case that matters to you? [Open a feature request](https://github.com/P4Software/Rotula/issues/new/choose). @@ -530,7 +607,8 @@ Read these before relying on Rotula for production labels, and **always print a shapes and line breaks can differ slightly from the printed label. The app says so. - **Bitmap fonts and `FontWidth`.** The printer's built-in bitmap fonts are not reproduced; width scaling is only approximated in the preview. -- **`^PO` (print orientation) is not previewed.** It is written to the ZPL, but the preview always shows the normal orientation. +- **Print orientation (`^PO`) and mirror (`^PM`) are shown only in the Preview window.** The canvas and the PNG, JPEG and PDF + exports show the label as designed (upright); the exports are artwork, not a picture of the printer's output. - **Variables do not survive a ZPL round trip.** ZPL cannot express `{{name}}`, so imported ZPL is plain text until you edit it. - **Raw ZPL is not scaled on DPI conversion.** Unknown commands kept as raw ZPL stay in their original dots when a label is converted to another resolution, and Rotula warns you. diff --git a/Rotula.sln b/Rotula.sln index ae37ced..2f6de85 100644 --- a/Rotula.sln +++ b/Rotula.sln @@ -47,6 +47,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rotula.Data.Pack.ClickHouse EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rotula.Data.Packs.Tests", "tests\Rotula.Data.Packs.Tests\Rotula.Data.Packs.Tests.csproj", "{3DC421B4-0B47-4308-8791-4225E9A0CCAD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rotula.Engine", "src\Rotula.Engine\Rotula.Engine.csproj", "{AEEF329C-5A0F-42F6-BE98-2D444BFA3904}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rotula.Cli", "src\Rotula.Cli\Rotula.Cli.csproj", "{1CB2C052-D923-4083-A606-C1E4B81D36C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -285,6 +289,30 @@ Global {3DC421B4-0B47-4308-8791-4225E9A0CCAD}.Release|x64.Build.0 = Release|Any CPU {3DC421B4-0B47-4308-8791-4225E9A0CCAD}.Release|x86.ActiveCfg = Release|Any CPU {3DC421B4-0B47-4308-8791-4225E9A0CCAD}.Release|x86.Build.0 = Release|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|x64.ActiveCfg = Debug|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|x64.Build.0 = Debug|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|x86.ActiveCfg = Debug|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Debug|x86.Build.0 = Debug|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|Any CPU.Build.0 = Release|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|x64.ActiveCfg = Release|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|x64.Build.0 = Release|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|x86.ActiveCfg = Release|Any CPU + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904}.Release|x86.Build.0 = Release|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|x64.Build.0 = Debug|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|x86.ActiveCfg = Debug|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Debug|x86.Build.0 = Debug|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|Any CPU.Build.0 = Release|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|x64.ActiveCfg = Release|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|x64.Build.0 = Release|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|x86.ActiveCfg = Release|Any CPU + {1CB2C052-D923-4083-A606-C1E4B81D36C7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -309,5 +337,7 @@ Global {A93A2F2F-9C16-49E9-B046-5A0CC310AB2F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {12DDAC38-D696-4C2E-A2E4-7DADB7D08D28} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {3DC421B4-0B47-4308-8791-4225E9A0CCAD} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {AEEF329C-5A0F-42F6-BE98-2D444BFA3904} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {1CB2C052-D923-4083-A606-C1E4B81D36C7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} EndGlobalSection EndGlobal diff --git a/SECURITY.md b/SECURITY.md index a57463d..484f95b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,7 +19,8 @@ changelog unless you prefer otherwise. ## What is in scope Rotula is an offline desktop application. It makes no network calls except the printing you ask for (a TCP connection to the -printer address you enter). Reports of interest include: +printer address you enter), the databases you connect, and, only when you or your administrator switch it on, one anonymous +daily request to the GitHub "latest release" API (see [docs/deployment.md](docs/deployment.md#network-traffic-and-the-update-check)). Reports of interest include: - Crashes, hangs or memory exhaustion when opening a crafted `.rotula`, ZPL, CSV or Excel file. - Anything that causes Rotula to write outside the folders you chose, or to send data anywhere other than the printer you selected. diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index a3b12cd..35bea7d 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -16,12 +16,20 @@ read from the project files and the license metadata in each package's `.nuspec` | ModelContextProtocol.Core (via ModelContextProtocol) | 2.2.0 | Apache-2.0 | Rotula.Mcp | https://github.com/modelcontextprotocol/csharp-sdk | | Microsoft.Extensions.Hosting and the Microsoft.Extensions.* packages it pulls in (Configuration, Configuration.*, DependencyInjection, DependencyInjection.Abstractions, Diagnostics, Diagnostics.Abstractions, FileProviders.Abstractions, FileProviders.Physical, FileSystemGlobbing, Hosting.Abstractions, Logging, Logging.Abstractions, Logging.Configuration, Logging.Console, Logging.Debug, Logging.EventLog, Logging.EventSource, Options, Options.ConfigurationExtensions, Primitives, Caching.Abstractions) | 10.0.10 | MIT | Rotula.Mcp | https://github.com/dotnet/runtime | | Microsoft.Extensions.AI.Abstractions (via ModelContextProtocol.Core) | 10.8.3 | MIT | Rotula.Mcp | https://github.com/dotnet/extensions | -| System.Configuration.ConfigurationManager, System.Security.Cryptography.Pkcs (via the Microsoft.Extensions.* packages) | 9.0.18 | MIT | Rotula.Mcp | https://github.com/dotnet/runtime | +| System.Configuration.ConfigurationManager (via the Microsoft.Extensions.* packages) | 9.0.18 | MIT | Rotula.Mcp | https://github.com/dotnet/runtime | +| System.Security.Cryptography.Pkcs (pinned to the patched version in Rotula.Data) | 10.0.13 | MIT | Rotula.Mcp | https://github.com/dotnet/runtime | | System.Diagnostics.EventLog (via Microsoft.Extensions.Logging.EventLog) | 10.0.10 | MIT | Rotula.Mcp | https://github.com/dotnet/runtime | +| ASP.NET Core shared framework (Microsoft.AspNetCore.App, part of .NET; copied into a self-contained `rotula.exe`) | 10.0 | MIT | Rotula.Cli | https://github.com/dotnet/aspnetcore | +| Microsoft.AspNetCore.OpenApi (OpenAPI description of the print engine's REST API) | 10.0.13 | MIT | Rotula.Cli | https://github.com/dotnet/aspnetcore | +| Microsoft.OpenApi (via Microsoft.AspNetCore.OpenApi) | 2.12.2 | MIT | Rotula.Cli | https://github.com/microsoft/OpenAPI.NET | +| Microsoft.Extensions.Hosting.WindowsServices (runs `rotula serve` as a Windows service) | 10.0.13 | MIT | Rotula.Cli | https://github.com/dotnet/runtime | +| System.ServiceProcess.ServiceController (via Microsoft.Extensions.Hosting.WindowsServices) | 10.0.13 | MIT | Rotula.Cli | https://github.com/dotnet/runtime | | Liberation Sans Regular / Bold and Liberation Mono Regular (embedded stand-in fonts) | 2.x | SIL Open Font License 1.1 | Rotula.Rendering | https://github.com/liberationfonts/liberation-fonts | The MCP server (`Rotula.Mcp`) is a separate executable from the app. It uses the packages marked `Rotula.Mcp` above; its license expressions were read from each package's `.nuspec` in the restored NuGet cache (the SDK is Apache-2.0, used unmodified). +The print engine (`rotula.exe`, `Rotula.Cli` and `Rotula.Engine`) is another separate executable; it uses the packages marked +`Rotula.Cli` above besides Rotula.Rendering's and Rotula.Core's. The Liberation fonts are embedded unmodified in `Rotula.Rendering`; their license text is in `src/Rotula.Rendering/Fonts/LICENSE-LiberationFonts.txt` and is also embedded in the assembly. Apache-2.0 components are @@ -46,7 +54,8 @@ each package's `.nuspec` in the NuGet cache. | Microsoft.Extensions.DependencyInjection.Abstractions, Logging.Abstractions | 10.0.0 | MIT | SqlClient dependencies. | | Microsoft.IdentityModel.Abstractions, JsonWebTokens, Logging, Protocols, Protocols.OpenIdConnect, Tokens | 8.16.0 | MIT | SqlClient dependencies (token handling inside the driver). | | System.IdentityModel.Tokens.Jwt | 8.16.0 | MIT | SqlClient dependency. | -| System.Configuration.ConfigurationManager, System.Diagnostics.EventLog, System.Security.Cryptography.Pkcs, System.Threading.RateLimiting | 9.0.18 | MIT | SqlClient dependencies. | +| System.Configuration.ConfigurationManager, System.Diagnostics.EventLog, System.Threading.RateLimiting | 9.0.18 | MIT | SqlClient dependencies. | +| System.Security.Cryptography.Pkcs | 10.0.13 | MIT | SqlClient dependency, pinned to the patched version in Rotula.Data. | | Microsoft.Data.Sqlite, Microsoft.Data.Sqlite.Core | 10.0.12 | MIT | SQLite files. https://github.com/dotnet/efcore | | SQLitePCLRaw.bundle_e_sqlite3, .core, .provider.e_sqlite3, .lib.e_sqlite3 (native `e_sqlite3` library) | 2.1.12 | Apache-2.0 (the bundled SQLite engine itself is in the public domain) | SQLite files. https://github.com/ericsink/SQLitePCL.raw | | MySqlConnector | 2.6.2 | MIT | MySQL and MariaDB. https://github.com/mysql-net/MySqlConnector | @@ -67,7 +76,7 @@ rows are completed when those packs are built. The decisions and conditions are | Pack | Component | License | Status | |---|---|---|---| -| Oracle | Oracle.ManagedDataAccess.Core 23.26.301 (`Oracle.ManagedDataAccess.dll`), plus the MIT libraries it needs: System.Configuration.ConfigurationManager, System.Diagnostics.EventLog and System.Diagnostics.PerformanceCounter 8.0.0, System.DirectoryServices.Protocols 8.0.2, System.Security.Cryptography.Pkcs 8.0.1, System.Security.Cryptography.ProtectedData 10.0.12 | Oracle Free Distribution, Hosting, and Use Terms and Conditions (not MIT; copyright Oracle) | Accepted 2026-10-10, as an optional pack only. Built by `scripts\build-packs.ps1` (`src\Rotula.Data.Pack.Oracle`); the full licence is in the pack's `NOTICES.txt`; shipped unmodified. | +| Oracle | Oracle.ManagedDataAccess.Core 23.26.301 (`Oracle.ManagedDataAccess.dll`), plus the MIT libraries it needs: System.Configuration.ConfigurationManager, System.Diagnostics.EventLog and System.Diagnostics.PerformanceCounter 8.0.0, System.DirectoryServices.Protocols 8.0.2, System.Security.Cryptography.Pkcs 10.0.13, System.Security.Cryptography.ProtectedData 10.0.12 | Oracle Free Distribution, Hosting, and Use Terms and Conditions (not MIT; copyright Oracle) | Accepted 2026-10-10, as an optional pack only. Built by `scripts\build-packs.ps1` (`src\Rotula.Data.Pack.Oracle`); the full licence is in the pack's `NOTICES.txt`; shipped unmodified. | | Firebird | FirebirdSql.Data.FirebirdClient 10.3.4 | Initial Developer's Public License 1.0 (file-level copyleft; source https://github.com/FirebirdSQL/NETProvider) | Accepted 2026-10-10. Built by `scripts\build-packs.ps1` (`src\Rotula.Data.Pack.Firebird`); shipped unmodified; licence in the pack's `NOTICES.txt`. | | DuckDB | DuckDB.NET.Data.Full and DuckDB.NET.Bindings.Full 1.5.6 (with the native `duckdb.dll`, DuckDB 1.5.6), Apache.Arrow and Apache.Arrow.Scalars 23.0.0 | MIT (DuckDB.NET, DuckDB), Apache-2.0 (Apache Arrow) | Built by `scripts\build-packs.ps1` (`src\Rotula.Data.Pack.DuckDb`); licences in the pack's `NOTICES.txt`. | | ClickHouse | ClickHouse.Driver 1.5.0 (MIT, with vendored K4os.Compression.LZ4, K4os.Hash.xxHash and ZstdSharp), NodaTime 3.2.3 (Apache-2.0), Microsoft.IO.RecyclableMemoryStream 3.0.1 (MIT), Microsoft.Extensions.* 10.0.8 (MIT) | MIT, Apache-2.0 | Built by `scripts\build-packs.ps1` (`src\Rotula.Data.Pack.ClickHouse`); licences and the driver's own third-party notices in the pack's `NOTICES.txt`. | @@ -99,7 +108,8 @@ Specifications. | Newtonsoft.Json (via the test SDK) | 13.0.3 | MIT | | coverlet.collector | 6.0.4 | MIT | | dotnet-reportgenerator-globaltool (CI only, installed on demand) | latest | Apache-2.0 | -| Inno Setup (CI only, builds the installer) | 6.x | Inno Setup License (free, redistribution of the tool is not part of Rotula) | +| Inno Setup (release build machine, builds the per-user installer) | 6.x | Inno Setup License (free, redistribution of the tool is not part of Rotula) | +| WiX Toolset SDK (`WixToolset.Sdk`, release build machine, builds the MSI; nothing of it is inside the MSI or the app) | 5.0.2 | MS-RL (Microsoft Reciprocal License); the tool is not redistributed | ## Data diff --git a/docs/decisions.md b/docs/decisions.md index 1beea9e..2a9624c 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -3,13 +3,22 @@ ## App (Rotula.App) ### Language switch: live update -The in-app language switch (View > Language: Same as Windows / English / Espanol) updates open windows immediately; no restart. +The in-app language switch (View > Language: Same as Windows / English / Español / Português (Brasil) / Français (Canada)) +updates open windows immediately; no restart. Each language is listed in its own language. All XAML text is bound to the indexer of `Loc` (`{loc:T Key}`), which raises `Item[]` on a change. View-models that cache translated text (property panel rows, toolbox, layers, status text) rebuild on `Loc.LanguageChanged`. The default follows the -Windows display language (Spanish if it is Spanish, English otherwise). The choice is stored in `%AppData%\Rotula\settings.json`. -Only `Strings.en.resx` and `Strings.es.resx` exist (satellite assemblies, no neutral resources), so `Loc` always asks for "en" -or "es" explicitly and falls back to English per key. `NeutralResourcesLanguage(..., Satellite)` must NOT be used: it also -redirects WPF's own XAML lookup and the application fails to start. +Windows display language: any Spanish is `es`, any Portuguese `pt-BR`, any French `fr-CA`, everything else English. The choice +is stored in `%AppData%\Rotula\settings.json` as `auto` or the culture name; settings saved as `en` or `es` keep working unchanged. +Only `Strings.en.resx`, `Strings.es.resx`, `Strings.pt-BR.resx` and `Strings.fr-CA.resx` exist (satellite assemblies, no neutral +resources), so `Loc` always asks for one of `Loc.SupportedLanguages` by its full culture name and falls back to English per key. +Numbers and dates in UI text are formatted with that culture (decimal comma in es, pt-BR and fr-CA). +`NeutralResourcesLanguage(..., Satellite)` must NOT be used: it also redirects WPF's own XAML lookup and the application fails +to start. +- **One variant per language**: Brazilian Portuguese and Canadian French are the ones shipped; a Portugal or France desktop gets + them rather than English. The French follows Quebec usage (OQLF): "courriel", and a non-breaking space before `:` `;` `!` `?` + and inside « ». Technical tokens (ZPL commands, GS1 AIs, file extensions, the product name) are never translated. +- **Parity is tested**: every `Strings.*.resx` must have exactly the English keys, no empty value and the same `{n}` + placeholders per key, so a new language or a new key cannot ship half done. ### Core messages Core only returns `ValidationIssue.MessageKey` + args. The UI looks up `Issue.`; args that are keywords @@ -150,6 +159,14 @@ host that matches a preset offers its settings. The password stays a separate pr placeholder name used for an unnamed form can never pick up the password of a real connection. When the store asks for the password again the user is told and the password box takes the focus. +### Label sheets (office printers) +File > Export > Label sheets (PDF) and a button in the Print dialog open `SheetLayoutViewModel`. Output is a PDF only: sending to a Windows +printer was left out because driver margins and "fit to page" scaling move the labels, while a PDF printed at actual size keeps them in place. +The labels come from the print paths (`MergeBatch.Labels` with data, `LabelPrintRun.LabelValues` without), so copies, skipped records and serial +numbers behave as when printing; numbers are confirmed only when the PDF is complete. Templates are generic and named from their dimensions. +The chosen layout is saved in the optional `sheet` object of the .rotula file without a version change (an older build ignores it); changing it +marks the label as changed but is not an undo step. Details in [printing.md](printing.md#label-sheets-for-office-printers). + ### Tests `tests/Rotula.Tests` targets `net10.0-windows` (it references the WPF app). All App tests share the xUnit collection "App" because the language is process-wide state. @@ -163,6 +180,56 @@ because the language is process-wide state. satellite resource assemblies and the SkiaSharp native library all load from the single file. The native library is unpacked to a temporary folder on first start, which is why the first launch can take a second or two longer. +### Machine-wide MSI (WiX Toolset), next to the Inno Setup installer +Approved by the owner (Glenn) as part of the enterprise deployment work: administrators need a per-machine MSI for Intune, +Group Policy software installation and Configuration Manager, which handle MSI packages natively (product code detection, +upgrades, return codes). The per-user Inno Setup installer stays the default for people who install Rotula themselves. +- **Tool**: WiX Toolset v5 (`WixToolset.Sdk/5.0.2`, MS-RL), restored by `dotnet build` on the build machine. It is a build-time + tool only: nothing of it is inside the MSI or the app, so the "permissive licences for what ships" rule is unaffected. It is + listed under "Build and test only" in THIRD-PARTY-NOTICES.md. The WiX project asks organizations that earn money from it to + pay its Open Source Maintenance Fee; **whether that applies to P4 Software is for the owner to check** before the first MSI + release. +- **Not in Rotula.sln**: the WiX build needs Windows Installer and only runs on Windows; the solution must keep building on any + runner. `packaging/msi/Rotula.Msi.wixproj` is built by `build-installer.ps1` from the stage folder after the programs are + signed, and the MSI is signed and verified like the setup exe. An unsigned MSI is renamed `-UNSIGNED` and never published. +- **Shape**: per-machine, `Program Files\Rotula`, advertised Start menu shortcut, `.rotula` association in HKLM (property + `ASSOCIATE=0` skips it), ARP links and icon, no Modify. No wizard pages (silent deployment is the use case); deploying it + means accepting `LICENSE-THIRD-PARTY.txt` for the users, as `docs/deployment.md` says. +- **Upgrades**: fixed UpgradeCode `{4B2C6076-5B82-411C-B751-EC4B70BEE9C6}` (never change it), a new ProductCode per build, + `MajorUpgrade` with same-version upgrades allowed (MSI versions are numeric, so 0.4.0 must be able to replace 0.4.0-rc1) and + downgrades refused. The UpgradeCode differs from the Inno AppId on purpose: they are two products, and a computer should + have only one of them. + +### Organization policies (Group Policy / Intune) +`Rotula.Files.Policy` reads `SOFTWARE\Policies\P4Software\Rotula` from HKLM and HKCU once at start-up, value by value with +the computer winning (`LayeredPolicySource` over two `RegistryPolicySource`s, behind `IPolicySource` so tests use a dictionary). +It lives in Rotula.Files because both the app and the MCP server need it. `RotulaPolicy.Read` ignores values it does not +understand, so a typo cannot force a wrong value. +- **What a policy may do**: choose a value for the user and lock it (language, update check), suggest a default (printer, not + locked), or take something away (add-ons from the user's folder; MCP printing and database access). It never switches a risky + feature on: the MCP policies only block, and the MCP settings file still has to allow printing and databases. +- **Locked settings in the UI**: the menu entries are disabled (the language commands' CanExecute, the update entry's + IsEnabled) and a disabled "Managed by your organization" line appears under them. The user's own value stays in + settings.json and comes back when the policy is removed; `AppSettings.LanguageIn` / `UpdateCheckIn` give the value in force. +- **Templates**: `packaging/policies/Rotula.admx` with en-US and es-ES ADML. A test checks that every registry value name of the + template is a `PolicyNames` constant (and the other way round) and that both ADML files define the same strings. + +### Update check: opt-in, anonymous, never installs +The owner approved an update check on the condition that it sends nothing unless explicitly enabled. Rules: +- Off by default. It runs only when the user ticks Help > Check for updates once a day, or the `UpdateCheck` policy is 1. The + policy at 0 forces it off and the user cannot switch it on. +- At most once a day (`UpdateSchedule.IsDue`, 24 hours; a last-check time in the future counts as due so a wrong clock cannot + stop it). The time is stored before asking, so an offline computer tries once a day, not at every start. +- One `GET https://api.github.com/repos/P4Software/Rotula/releases/latest`, User-Agent `Rotula` without a version, no cookies, + no identifiers; the Windows proxy with default credentials. Failures are silent. +- `tag_name` is parsed as SemVer (`SemanticVersion`, pre-release ordering per SemVer 2.0); drafts and pre-releases are ignored. + The link of the notice is built from the parsed version (`https://github.com/P4Software/Rotula/releases/tag/vX.Y.Z`), never + taken from the answer, and the launcher opens https addresses only. +- A newer version shows a non-modal strip under the toolbar ("View release", close). Closing it remembers that version so it + does not come back for it. Nothing is downloaded or installed, ever. +- Testable: version compare, schedule, parsing and the checker are UI-free (`Services/UpdateCheck.cs`); HTTP is behind + `IReleaseFeed`, and `GitHubReleaseFeed` takes a handler so a test sees the exact request. + ### Installer: Inno Setup, not MSIX An MSIX package must be signed with a certificate the machine trusts before Windows will install it, and the project has no signing certificate (and the CI has no secrets). An unsigned MSIX cannot be installed by double-clicking it, which @@ -174,9 +241,11 @@ documents. The script lives in `packaging/installer/Rotula.iss`; the CI installs certificate becomes available, signing both the exe and the installer is the only change needed. ### Winget -`packaging/winget/` holds templates for the version, installer and en-US locale manifests (schema 1.9.0). -`scripts/New-WingetManifest.ps1` fills version, installer URL, SHA-256 and date from the tag, and the release workflow attaches -the result as a zip. Submitting it to the winget-pkgs repository is a manual step because it needs a personal token. +`packaging/winget/` holds templates for the version, installer, en-US default locale and es-MX locale manifests (schema 1.10.0). +The installer manifest lists two installers: the Inno Setup exe (`Scope: user`, the default) and the MSI (`Scope: machine`, +`InstallerType: wix`, `ElevationRequirement: elevationRequired`, its ProductCode and the fixed UpgradeCode). +`scripts/New-WingetManifest.ps1` fills version, URLs, SHA-256 hashes, the MSI's ProductCode (read from the signed MSI) and the +date; `build-installer.ps1` runs it and zips the result. Submitting it to winget-pkgs is a manual step (docs/releasing.md). ### Coverage gate CI fails when line coverage of `Rotula.Core` drops below 80% (`scripts/check-coverage.ps1`, reading the Cobertura report from @@ -210,6 +279,15 @@ that export, does not add the printer set-up commands. The copies typed in the d sets copies per record (fixed or from a column); the dialog says so then. With records but nothing ticked, Print is disabled and the reason is shown. With no data loaded the dialog is the single-label dialog it always was. The dialog subscribes to the data view-model and unsubscribes when it closes. +### Printer status (~HS) around a job +Approved by the owner (assessment item 4). The `~HS` reply is decoded in Rotula.Printing (`HostStatusParser`), not Core: it is about the printer, +not the label, and Core's catalogue keeps `~HS` out of scope for label files. The check is opt-in per `PrinterTarget` so library callers and +the MCP `print_label` path send exactly what they did before; the app turns it on per transport (on for TCP, off for Windows queues, because +many USB drivers cannot read back and a non-ZPL queue would print the query). An unknown status never blocks. A blocking condition is a +`PrintError` kind of its own (`HeadOpen`, `PaperOut`, ...) with 0 bytes sent, so `LabelPrintRun` gives the numbers back; the verification after +the job runs after the numbers were confirmed and is a notice, so a fault after the job never gives a number back. Batches check every job and +verify once after the last one. + ### Data merge `DataMergeViewModel` (window opened with Ctrl+M, not modal so the canvas can be watched) wraps Core.Merge: CSV (delimiter auto detected and shown; UTF-8 with fallback to Windows-1252 as Excel writes it) or .xlsx (sheet picker), read off the UI thread with @@ -530,3 +608,58 @@ emitted or validated, so a barcode invalid only on a record where it is hidden d prints and PNG/JPEG/PDF; only the design canvas draws them dimmed (`includeNonPrinting`), and the Preview window shows the printed result. Lock is enforced in `EditorCommands` (move, nudge, resize, rotate, align, distribute, delete, cut) and never touches ZPL; property edits stay allowed. Condition fields are listed as merge placeholders so they can be mapped to columns. ZPL import never creates conditions or locks. + +## Print station (Rotula.Files/Station, StationViewModel) + +### One print path, a locked screen on top +The station is a separate window over the same services as the designer: it builds the printer target and profile through +`PrinterSettingsViewModel`, asks the label's questions through `PromptSet`, and prints through `IPrintService` with the status check on. So a +label prints the same from the station as from the Print dialog, and nothing in the print path knows about stations. + +### The configuration is strict and the PIN is only a hash +`station.json` refuses unknown settings (`UnmappedMemberHandling.Disallow`), so a misspelt `maxCopies` is an error with its line, not a silently +unlimited station. The exit PIN is stored as `pbkdf2-sha256$$$` (600,000 iterations, 16-byte salt) and checked with +`CryptographicOperations.FixedTimeEquals`, from the framework only. Five wrong PINs lock the box for 30 seconds. The PIN keeps an operator from +leaving by accident; it is not a security boundary, because anyone who can edit `station.json` can remove it, and Windows keys cannot be blocked. + +### Scanners are read by their terminator +Keyboard-wedge scanners end a code with Enter or Tab, which `ScanSequencer` turns into "stay", "next field", "focus Print" or "print". Timing +heuristics (telling a scan from typing by speed) were left out: they misfire on slow PCs and remote sessions, and the terminator already works for +both people and scanners. +## Print orientation preview (Rotula.Rendering) + +### ^PO and ^PM are shown "as printed" only in the Preview window +`RenderLabel.Inverted` (^POI) and `RenderLabel.Mirrored` (^PMY) flip the whole drawing about the label's centre inside `LabelRenderer.Draw`, so +bitmaps and PDF pages get the same transform. `LabelDocumentAdapter.ToRenderLabel` never sets them; only `LabelDocumentAdapter.AsPrinted` does, +and only the Preview window asks for it (it opens on "As printed" with an "As designed" switch and a sentence saying why the picture is turned). +The design canvas stays upright because editing an upside-down label (handles, rulers, snapping, typing) would be confusing; the status bar says +"Prints upside down" / "Prints mirrored" instead. PNG, JPEG and PDF exports and the MCP preview stay as designed: they are artwork for documents, +proofs and screens, not a picture of the media leaving the printer, and there is no "as printed" export option to honour. The two flips commute +(inverted and mirrored together is a top-to-bottom flip), so their order is not a question. + +## Print engine (Rotula.Engine, Rotula.Cli) + +### A library and a thin executable, on the app's print path +`Rotula.Engine` (net10.0, no UI) turns a request into a print through `IPrintService`, `LabelPrintRun` / `VariableJob`, `PromptSet`, +`LabelDpiConverter` and `PrinterSetup`, the same pieces the Print dialog and the data merge use, so a label prints byte for byte the same from +the designer, the station and the engine. `Rotula.Cli` (`rotula.exe`) is only argument parsing, the REST host and the console. Results are values +(`PrintJobResult` with an outcome, an error code and localized notices), never exceptions, so the command line and the API answer the same thing. +Messages come from the app's own `.resx` files, embedded in the engine, so the engine needs no WPF and every string exists in all four languages. + +### Questions take their default, never the sample value +An unattended job cannot ask. An unanswered question takes the default set in the designer; a required question with no default stops the job +with every question listed. The sample value is design-time data and printing it on a real carton would be silent, wrong output. + +### One job at a time per printer, status before every job +Requests for the same printer (a queue name, or host:port) wait on one `SemaphoreSlim`, so two callers never interleave labels on one roll. The +`~HS` check runs before each job and verification once after the last, as in the app's data merge. Counters are reserved before and confirmed +only for the labels sent, so a held-back or failed job gives its serial numbers back unless part of it may have printed. + +### The REST server is opt-in and allow-listed +Offline-first stays true: the server listens only when started, on `127.0.0.1` unless configured, and calls out only to printers. Every route but +health and the OpenAPI document needs a key; keys are 32 random bytes shown once and stored as SHA-256 hashes, compared with `FixedTimeEquals` +(a slow hash is not needed for 256-bit random keys). Labels are named relative to configured folders (no rooted paths, `..`, drives, UNC or +wildcards), printers must be listed, inline label content is off by default, and request bodies and label files have size limits. The settings +file and request bodies refuse unknown names, so a typo never widens what is allowed. The audit log carries an 8-character key id from the hash, +never the key or label data. `PrintEngine = 0` (policy) switches it all off. Kestrel and ASP.NET Core come with .NET; the only packages added are +Microsoft's OpenAPI and Windows service hosting (MIT). diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..a7503cf --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,254 @@ +# Deploying Rotula in an organization + +This page is for administrators who install Rotula on many computers: silent installs, the machine-wide MSI, Intune, +Group Policy and Configuration Manager, and the policy settings that fix Rotula's settings for users. + +## Which package to use + +Every release has two installers, both signed by **Barrdega Sistemas NA LLC** (check with `Get-AuthenticodeSignature`, see +[releasing.md](releasing.md#verifying-a-signature)) and listed in `SHA256SUMS.txt`: + +| Package | Installs to | Rights | Best for | +|---|---|---|---| +| `Rotula-X.Y.Z-win-x64.msi` | `C:\Program Files\Rotula`, for every user | Administrator (SYSTEM when deployed) | Intune, Group Policy software installation, Configuration Manager, any tool that deploys MSI packages | +| `Rotula-X.Y.Z-win-x64-setup.exe` (Inno Setup) | `%LocalAppData%\Programs\Rotula` for the current user, or `C:\Program Files\Rotula` with `/ALLUSERS` | None (per user) or administrator (`/ALLUSERS`) | People installing it themselves; per-user deployment scripts | + +Use **one** package per computer. The MSI and the Inno Setup installer are separate products in Apps & features (different +product codes), so a computer that has both shows Rotula twice. To move a computer from the per-user installer to the MSI, +uninstall the per-user copy first (command below). Settings, saved connections and counters live in `%AppData%\Rotula` and +are kept either way. + +Both packages install the same files: `Rotula.App.exe` (the designer), `Rotula.Mcp.exe` (the MCP server for AI assistants, +see [mcp.md](mcp.md)), `rotula.exe` (the print engine's command line and REST API, see [print-engine.md](print-engine.md)), the +licence files, `docs\mcp.md`, `docs\print-engine.md`, the document icon and an empty `packs` folder for data source +add-ons. Both register `.rotula` files to open with Rotula and add a Start menu entry. Neither installs a service, a +scheduled task, a driver or a browser extension, and neither needs a reboot. + +**Licence terms.** Rotula is MIT, but it ships the SQL Server networking component under Microsoft's own licence terms. The +Inno Setup installer shows those terms and the user accepts them; the MSI has no wizard, so by deploying it you accept +`LICENSE-THIRD-PARTY.txt` (installed next to the program and attached to every release) on behalf of your users. See +[releasing.md](releasing.md#the-licence-page-and-the-sql-server-networking-component). + +## The MSI + +### Install + +``` +msiexec /i Rotula-X.Y.Z-win-x64.msi /qn /norestart /l*v "%TEMP%\Rotula-install.log" +``` + +`/qn` is completely silent; `/qb!` shows a small progress bar without a Cancel button. The command must run elevated +(Intune, Group Policy and Configuration Manager run it as SYSTEM). Public properties: + +| Property | Default | Meaning | +|---|---|---| +| `INSTALLFOLDER` | `C:\Program Files\Rotula` | Where the program goes, for example `INSTALLFOLDER="D:\Apps\Rotula"` | +| `ASSOCIATE` | `1` | `0` leaves the `.rotula` file association alone | + +Example: `msiexec /i Rotula-X.Y.Z-win-x64.msi /qn ASSOCIATE=0`. + +### Upgrade + +Install the newer MSI with the same command. The package has a fixed **UpgradeCode** +`{4B2C6076-5B82-411C-B751-EC4B70BEE9C6}` and a new **ProductCode** for every version (read it from the MSI with Orca, or from +the winget manifest of the release). The newer package removes the older one in the same transaction. Installing an older +version over a newer one is refused (error 1603 with "A newer version of Rotula is already installed" in the log). + +### Uninstall + +``` +msiexec /x {ProductCode} /qn /norestart +``` + +or `msiexec /x Rotula-X.Y.Z-win-x64.msi /qn`. Uninstalling removes the program folder, the Start menu entry and the +file association. It never touches users' settings in `%AppData%\Rotula`. + +### Exit codes + +| Code | Meaning | +|---|---| +| 0 | Success | +| 3010 | Success, a restart is required (Rotula itself never asks for one; another pending install can cause it) | +| 1602 | The user cancelled | +| 1603 | Fatal error: read the log (`/l*v`) | +| 1618 | Another installation is already running; try again later | +| 1619 | The MSI file could not be opened (wrong path, blocked download) | +| 1625 | Installation blocked by policy | +| 1925 | Not elevated: run the command as administrator | + +## The Inno Setup installer + +### Install silently + +``` +Rotula-X.Y.Z-win-x64-setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- /LOG="%TEMP%\Rotula-setup.log" +``` + +That installs for the **current user** without administrator rights. For **all users** (Program Files, needs elevation) add +`/ALLUSERS`: + +``` +Rotula-X.Y.Z-win-x64-setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- /ALLUSERS +``` + +Other switches: + +| Switch | Meaning | +|---|---| +| `/SILENT` | Progress window only, no questions (instead of `/VERYSILENT`) | +| `/DIR="D:\Apps\Rotula"` | Install folder | +| `/TASKS="associate"` | Choose the tasks; leave `associate` out to skip the `.rotula` association | +| `/MERGETASKS="desktopicon"` | Also add a desktop icon (off by default) | +| `/LANG=spanish` | Language of the installer itself (`english`, `spanish`, `brazilianportuguese` or `french`) | +| `/CURRENTUSER` | Force a per-user install even when run elevated | + +The licence page is accepted automatically by `/VERYSILENT`, so the same note about the licence terms applies as for the MSI. + +### Uninstall silently + +The uninstaller is `unins000.exe` in the install folder: + +``` +"%LocalAppData%\Programs\Rotula\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART +"C:\Program Files\Rotula\unins000.exe" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART +``` + +The first line is a per-user install, the second an `/ALLUSERS` one. The uninstall entry is +`{6B0E5C1A-3F7D-4C52-9A1E-2D8F4B7A9C31}_is1` under `...\CurrentVersion\Uninstall` (HKCU for per-user, HKLM for all users); +its `QuietUninstallString` value holds the exact command. + +### Exit codes + +| Code | Meaning | +|---|---| +| 0 | Success | +| 1 | Setup could not start | +| 2 | Cancelled before the installation began | +| 3 | Fatal error while preparing | +| 4 | Fatal error during the installation | +| 5 | Cancelled during the installation | +| 7 | Preparing to install failed (for example a file in use) | +| 8 | Preparing to install failed and a restart is needed first | + +## Microsoft Intune + +### As a Win32 app (recommended) + +1. Put `Rotula-X.Y.Z-win-x64.msi` alone in a folder and wrap it with the + [Microsoft Win32 Content Prep Tool](https://learn.microsoft.com/intune/intune-service/apps/apps-win32-prepare): + `IntuneWinAppUtil.exe -c -s Rotula-X.Y.Z-win-x64.msi -o `. +2. In the Intune admin center: **Apps > Windows > Add > Windows app (Win32)** and upload the `.intunewin` file. Intune + reads the name, publisher, version and ProductCode from the MSI and fills the install and uninstall commands: + - Install: `msiexec /i "Rotula-X.Y.Z-win-x64.msi" /qn /norestart` + - Uninstall: `msiexec /x {ProductCode} /qn /norestart` + - Install behavior: **System**. Device restart behavior: **Determine behavior based on return codes** (the defaults map + 0, 1707, 3010, 1641 and 1618 correctly). +3. **Detection rule**: choose *Manually configure detection rules > MSI*, with the ProductCode Intune filled in. To detect + "this version or newer" instead, use a *File* rule: path `%ProgramFiles%\Rotula`, file `Rotula.App.exe`, + *String (version)*, *Greater than or equal to* `X.Y.Z`. +4. For a later version, add it as a new app and set **Supersedence** to the previous one with *Uninstall previous version* + off: the MSI upgrades in place. + +The per-user Inno Setup installer can also be packaged as a Win32 app with install behavior **User**, the command +`Rotula-X.Y.Z-win-x64-setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-`, and a file detection rule on +`%LocalAppData%\Programs\Rotula\Rotula.App.exe`. + +### As a line-of-business app + +**Apps > Windows > Add > Line-of-business app** accepts the MSI directly. It is simpler, but Win32 apps give you detection +rules, return codes and supersedence, so prefer Win32 when mixing with other app types. + +### Policy settings with Intune + +Either way below writes the same registry values as Group Policy, so Rotula behaves the same. + +- **Imported administrative templates** (recommended): **Devices > Configuration > Import ADMX**, upload + `packaging/policies/Rotula.admx` with `packaging/policies/en-US/Rotula.adml`. Then create a profile of type + **Templates > Imported Administrative templates** and set the policies under *P4 Software > Rotula*, in Computer + Configuration (for every user of the device) or User Configuration. +- **Custom OMA-URI (ADMX ingestion)**, for tenants without ADMX import: + 1. Ingest the template: OMA-URI `./Device/Vendor/MSFT/Policy/ConfigOperations/ADMXInstall/Rotula/Policy/RotulaAdmx`, data type + *String*, value: the whole content of `Rotula.admx`. + 2. Set a policy: OMA-URI `./Device/Vendor/MSFT/Policy/Config/Rotula~Policy~P4Software~Rotula/`, data type + *String*, value ``, ``, or for a policy with a value + `` (Language) or ``. + Use `./User/...` instead of `./Device/...` for a per-user policy. +- **Settings catalog / remediation script**: where neither is possible, a PowerShell script can write the values in the + table below directly under `HKLM\SOFTWARE\Policies\P4Software\Rotula`. + +## Group Policy + +### Software installation + +1. Copy the MSI to a share that the computer accounts can read, for example `\\fileserver\Software$\Rotula\`. +2. In the Group Policy Management Editor: **Computer Configuration > Policies > Software Settings > Software installation > + New > Package**, choose the MSI through its UNC path and pick **Assigned**. It installs at the next restart. +3. For a new version, add the new MSI to the same GPO and, on its **Upgrades** tab, mark it as upgrading the previous + package (*Package can upgrade over the existing package*). + +### Administrative templates + +Copy `packaging/policies/Rotula.admx` to `\\\SYSVOL\\Policies\PolicyDefinitions\` (the central store) and +`en-US\Rotula.adml` and `es-ES\Rotula.adml` to the language folders below it. For one computer, use +`C:\Windows\PolicyDefinitions` instead. The settings appear under **Administrative Templates > P4 Software > Rotula** in both +Computer Configuration and User Configuration. + +## Configuration Manager + +Create an application with the deployment type **Windows Installer (\*.msi file)** and point it at the MSI. Configuration +Manager reads the ProductCode for the detection method and fills +`msiexec /i "Rotula-X.Y.Z-win-x64.msi" /qn` and the uninstall command. Set the installation behavior to **Install for system**, +the logon requirement to **Whether or not a user is logged on**, and add the new version's application with a +**supersedence** relationship to the old one (without uninstalling it first: the MSI upgrades in place). + +## Policy settings + +Rotula reads the policies once at start-up from `HKLM\SOFTWARE\Policies\P4Software\Rotula` (the computer) and +`HKCU\SOFTWARE\Policies\P4Software\Rotula` (the user). When both set the same value, **the computer's value wins**. A +setting fixed by a policy is shown disabled in Rotula with the note "Managed by your organization", and the user's own +choice is kept in `settings.json` for the day the policy is removed. The MCP server (`Rotula.Mcp.exe`) and the print engine (`rotula.exe`) read the same key. + +| Policy (template name) | Registry value | Type | Values | Effect | +|---|---|---|---|---| +| Language of Rotula | `Language` | REG_SZ | `auto`, `en`, `es`, `pt-BR`, `fr-CA` | Sets the language and disables **View > Language**. `auto` follows the Windows display language (any Portuguese gets `pt-BR`, any French `fr-CA`). Case does not matter | +| Check for updates | `UpdateCheck` | REG_DWORD | `1` on, `0` off | Switches the daily update check on or off and disables **Help > Check for updates once a day**. Not set: each user decides (off until they switch it on) | +| Default printer | `DefaultPrinter` | REG_SZ | A Windows printer name | Preselected in the Print dialog until the user picks a printer. Not locked; ignored quietly if the printer is not installed | +| Allow data source add-ons from the user's folder | `UserAddOns` | REG_DWORD | `0` blocks | Add-ons in `%AppData%\Rotula\packs` are neither loaded nor offered for approval; the installed `packs` folder still works ([data-providers.md](data-providers.md)) | +| Allow the MCP server to print | `McpPrinting` | REG_DWORD | `0` blocks | The MCP server refuses every print request, whatever its settings file says | +| Allow the MCP server to read databases | `McpDatabase` | REG_DWORD | `0` blocks | The MCP server refuses every database query | +| Allow the print engine to print | `PrintEngine` | REG_DWORD | `0` blocks | `rotula.exe` refuses to print or query printers (exit code 5) and its REST server answers 403, whatever its settings file says ([print-engine.md](print-engine.md)) | + +A value that Rotula does not understand (a language other than the five values above, a number other than 0 or 1) is ignored, so +a typo never forces a wrong setting on users. The MCP policies can only take something away: setting them to `1` does not +switch printing or database access on; the MCP server's own settings file still has to allow it ([mcp.md](mcp.md)). + +Example with PowerShell (elevated), for a whole computer: + +```powershell +$key = 'HKLM:\SOFTWARE\Policies\P4Software\Rotula' +New-Item -Path $key -Force | Out-Null +Set-ItemProperty -Path $key -Name Language -Value 'es' -Type String +Set-ItemProperty -Path $key -Name UpdateCheck -Value 0 -Type DWord +Set-ItemProperty -Path $key -Name McpPrinting -Value 0 -Type DWord +``` + +Restart Rotula after a policy change. + +## Network traffic and the update check + +Rotula makes no network request of its own unless the update check is switched on, by the user (**Help > Check for updates +once a day**) or by the *Check for updates* policy. Otherwise the only traffic is what the user asks for: a label sent to a +network printer, and queries to the databases the user connects. + +When the update check is on, Rotula sends at most one request a day: an anonymous HTTPS `GET` of +`https://api.github.com/repos/P4Software/Rotula/releases/latest`, with the User-Agent `Rotula` (no version) and no cookies, +no identifiers and nothing about the user or the computer. It uses the Windows proxy settings, with the signed-in user's +credentials for a proxy that asks for them. When a newer version exists, a notice under the toolbar links to its release page +on GitHub. Rotula never downloads or installs anything itself: updating stays with you (a new MSI through your deployment +tool, or `winget upgrade P4Software.Rotula`). To allow the check through a firewall, allow `api.github.com` on port 443. +A failed check is silent and is tried again the next day. + +## winget + +`winget install P4Software.Rotula` installs the per-user package; `winget install P4Software.Rotula --scope machine` installs +the MSI for all users (winget asks for administrator rights). The package is not in the winget catalogue yet; see the README. diff --git a/docs/mcp.md b/docs/mcp.md index 0b9a458..ffaa035 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -56,6 +56,11 @@ a folder you own. A damaged settings file never switches anything on: the server falls back to the defaults above, leaves the file untouched and logs what is wrong. Values outside their range are clamped and mentioned in the log. +An organization can switch printing and database access off for everyone with the `McpPrinting` and `McpDatabase` policies +(Group Policy or Intune, see [deployment.md](deployment.md#policy-settings)). A policy can only take them away: when it +allows them, the settings file above still decides. Installed in Program Files (the MSI or an all-users install) the +settings file cannot be created by a normal user; the server then runs with the safe defaults and says so in its log. + ## Tools Positions and sizes are in the label's unit (millimetres unless the label was created in inches or dots), measured from the @@ -107,6 +112,7 @@ and what to do. |---|---| | `list_printers` | Printers installed in Windows. Read-only. | | `get_printer_info` | Resolution and paper size from the Windows driver. Read-only; no job is created. | +| `get_printer_status` | Asks the printer whether it can print now (out of labels, printhead open, ribbon out, paused, too hot) and how many labels it still has to print, with the ZPL `~HS` command. It sends that command through the Windows queue, so it needs the same permission as printing (`allowPrinting` and the printer in `allowedPrinters`) but no confirmation; a ZPL compatible printer prints nothing. Many USB drivers cannot pass the answer back; the result then says the status is unavailable. | | `print_label` | See "Printing" below. | | `list_database_connections` | Names of connections saved in the Rotula app (never hosts, users or passwords). Needs `allowDatabase`. | | `run_query` | One read-only `SELECT` on a saved connection. Needs `allowDatabase`. | diff --git a/docs/print-engine.md b/docs/print-engine.md new file mode 100644 index 0000000..1bd45ed --- /dev/null +++ b/docs/print-engine.md @@ -0,0 +1,239 @@ +# Print engine: command line and REST API + +`rotula.exe` prints Rotula labels without the designer. A script, a scheduled task or a warehouse system (WMS, ERP, shipping +software) hands it a `.rotula` label, the data and a printer; it prints exactly as the designer's Print dialog would, checks the +printer before every job and says how it went in a form a program can act on. + +It has two faces over one engine: + +- **`rotula print`, `rotula status`, `rotula printers`**: a command line for scripts and schedulers. It runs with the rights of the + person or service that starts it. +- **`rotula serve`**: a REST API for systems on this computer or the network. It prints only what its settings file allows (API + keys, label folders, printers) and can run as a Windows service. + +What it shares with the designer: + +- **The same print path.** Resolution conversion to the printer, darkness and speed, variables (dates, counters, questions, computed + values), data merge with the counters, copies and "skip or stop" settings saved with the label, and Windows fonts drawn as + pictures. +- **Serial numbers are used once.** Counters that remember their value use the same counter file as the designer + (`%AppData%\Rotula\counters.json`, or `--counter-file` / `counterFile`). Numbers are reserved before a run and confirmed only for + labels that reached the printer: a printer that was offline gives the numbers back; a send that broke halfway keeps them used, so a + number is never printed twice. +- **The printer status check (ZPL `~HS`).** Before every job the engine asks the printer whether it can print and holds the job back + while it reports it is out of labels, its printhead is open, the ribbon is out, it is paused, too hot, in diagnostics mode or its + buffer is full. After the last job it waits (up to 30 seconds) until the printer reports it finished. A printer that cannot answer + (many USB drivers) never blocks printing; the result says the status was not checked. `--no-status` and `--no-verify` (or + `"checkStatus": false` and `"verify": false`) switch either part off. +- **One job at a time per printer.** Requests for the same printer wait for each other, so two callers never interleave their + labels. Different printers print at the same time. +- **The messages of the app**, in English, Spanish, Portuguese (Brazil) or French (Canada). + +## Command line + +```text +rotula print LABEL.rotula --printer NAME | --host ADDRESS [options] +rotula status --printer NAME | --host ADDRESS [--port N] +rotula printers +rotula serve [--settings FILE] [--urls URL] rotula serve --init [--label-folder FOLDER] +rotula new-key +rotula help | version +``` + +| Option | Meaning | +|---|---| +| `--printer NAME` | An installed Windows printer (USB, shared or network queue). | +| `--host ADDRESS`, `--port N` | A network printer reached directly over TCP (port 9100 when not given). | +| `--copies N` | Copies of the label, or of every record. Without it, the quantity saved in the label (or the data's copies column) is used. | +| `--set NAME=VALUE` | The value of a field (`{{NAME}}` in the label). Repeat for more fields. | +| `--data FILE` | Records from a `.csv` / `.txt` (delimiter detected), `.xlsx` (`--sheet NAME`) or `.json` file; one label per record. A JSON object is one label's values. | +| `--answer NAME=VALUE` | The answer to a question the label asks at print time (a prompt variable). | +| `--dpi 203\|300\|600` | Printhead resolution. Without it, a Windows printer's driver is asked; a network printer prints at the label's own resolution. | +| `--darkness 0-30`, `--speed 1-14` | Sent to the printer before the labels. | +| `--no-status`, `--no-verify` | Skip the status check before every job, or the wait for "finished" after the last one. | +| `--dry-run` | Builds the ZPL and prints it to the output (or to `--output FILE`) without sending anything and without using up serial numbers. | +| `--reference TEXT` | Your own reference (order, wave, shipment), repeated in the result and the server log. | +| `--counter-file FILE` | Where remembering counters keep their position. | +| `--json` | The result as JSON, in the same shape as the REST API's answer. | +| `--lang en\|es\|pt-BR\|fr-CA` | Language of the messages. Without it the Language policy, then the `ROTULA_LANG` environment variable, then Windows decides. | + +Questions a label asks at print time take their **designer default** when no `--answer` is given. A required question without +a default and without an answer stops the job and lists every question; a sample value is never printed in place of an answer. + +Examples: + +```bat +rotula print "C:\Labels\carton.rotula" --printer "Dock 1" --set sku=4006381333931 --set lot=L2410 --copies 2 +rotula print "C:\Labels\carton.rotula" --host 10.20.0.31 --data "C:\Exports\wave-1207.csv" --reference WAVE-1207 +rotula print "C:\Labels\carton.rotula" --printer "Dock 1" --data picks.json --dry-run --output wave.zpl +rotula status --host 10.20.0.31 --json +``` + +### Exit codes + +| Code | Meaning | +|---|---| +| 0 | Printed (or the dry run worked, or the printer is ready). | +| 1 | Sending failed (printer not found, offline, unreachable...), or the status could not be read. | +| 2 | The command line or the request is wrong (unknown option, missing printer, unreadable data or label file). | +| 3 | The label has errors, or the answers to its questions are missing. Nothing was sent. | +| 4 | The printer reported a fault (out of labels, head open, paused...) and nothing was sent; for `status`, the printer cannot print now. | +| 5 | Switched off by the organization's `PrintEngine` policy. | +| 6 | Stopped with Ctrl+C. Labels already sent are counted; serial numbers of labels not sent go back. | + +## REST API + +### Set up + +```bat +rotula serve --init --settings "C:\ProgramData\Rotula\rotula-engine.settings.json" --label-folder "D:\Labels" +``` + +This writes a starter settings file and prints a new API key **once**. Give the key to the calling system; the file keeps only its +SHA-256 hash. Add the printers, then start the server: + +```bat +rotula serve --settings "C:\ProgramData\Rotula\rotula-engine.settings.json" +``` + +```jsonc +{ + // Where the server listens. 127.0.0.1 accepts calls from this computer only. + "urls": ["http://127.0.0.1:9180"], + // SHA-256 hashes of the API keys ("rotula new-key" makes a key and its hash). Several keys can be listed, one per caller. + "apiKeyHashes": ["3f1c...e9"], + // A request names a label relative to one of these folders: "outbound/carton.rotula". + "labelFolders": ["D:\\Labels"], + // Windows printer names, or host / host:port of network printers. Nothing else can be printed to or queried. + "allowedPrinters": ["Dock 1", "10.20.0.31", "10.20.0.32:6101"], + // Accept the content of a .rotula file in the request (labelContent). Off: only labels from the folders print. + "allowInlineLabels": false, + "maxCopies": 1000, + "maxRecords": 10000, + "maxRequestBytes": 10485760, + "maxLabelBytes": 5242880, + // Language of the messages when a request has no Accept-Language header. + "language": "en", + // Optional: the counter file shared with the designer, for example on a server where the designer also runs. + "counterFile": null +} +``` + +Unknown settings are an error (a misspelt `allowedPrinter` must not silently allow nothing or everything), and the server does not +start without at least one valid key. Everything a caller can reach is listed: there is no "allow all". + +**HTTPS.** Listening on another address than `127.0.0.1` over plain `http` works but the server warns, because keys and label data +would cross the network unencrypted. Use an `https://` URL with a certificate configured through the standard ASP.NET Core +settings, for example the environment variables `ASPNETCORE_Kestrel__Certificates__Default__Path` and +`ASPNETCORE_Kestrel__Certificates__Default__Password`, or put the server behind your reverse proxy. + +**As a Windows service** (elevated prompt): + +```bat +sc.exe create RotulaEngine binPath= "\"C:\Program Files\Rotula\rotula.exe\" serve --settings \"C:\ProgramData\Rotula\rotula-engine.settings.json\"" start= auto +sc.exe start RotulaEngine +``` + +Run the service as an account that may use the printers and read the label folders. A service account has its own +`%AppData%`; set `counterFile` when serial numbers must continue from the designer's file. + +### Calls + +Every route except `GET /api/v1/health` and the OpenAPI document needs the key in the `X-Api-Key` header (or +`Authorization: Bearer KEY`). The OpenAPI 3 description is at `/openapi/v1.json`. Messages follow the `Accept-Language` header +(or `?lang=`). + +| Route | What it does | +|---|---| +| `GET /api/v1/health` | `{"status":"ok","version":"0.4.0","printingBlocked":false}`. No key needed. | +| `GET /api/v1/printers` | The Windows printers, each with `allowed` (on `allowedPrinters`). | +| `GET /api/v1/printers/{name}/status` | Status of an allowed Windows printer. | +| `POST /api/v1/printers/status` | Status of an allowed printer: `{"host":"10.20.0.31"}` or `{"printer":"Dock 1"}`. | +| `POST /api/v1/jobs` | Prints. | + +A print request: + +```json +{ + "label": "outbound/carton.rotula", + "printer": "Dock 1", + "reference": "WAVE-1207", + "copies": 1, + "records": [ + { "sku": "4006381333931", "lot": "L2410", "qty": 12 }, + { "sku": "4006381333948", "lot": "L2411", "qty": 6 } + ], + "answers": { "operator": "M. Rivera" } +} +``` + +| Field | Meaning | +|---|---| +| `label` / `labelContent` | A label file relative to a label folder, or the content of a `.rotula` file (only with `allowInlineLabels`). Exactly one. | +| `printer` / `host`, `port` | A Windows printer, or a network printer and its port. Exactly one, and it must be on `allowedPrinters`. | +| `values` | The field values of one label. | +| `records` | Several records; one label (times the copies) each. Not together with `values`. Values may be text, numbers or true/false. | +| `copies`, `answers`, `dpi`, `darkness`, `speed` | As the command-line options. | +| `checkStatus`, `verify` | `true` by default. | +| `dryRun` | `true` returns the ZPL in `zpl` without printing. | +| `reference` | Your own reference, repeated in the answer and the log. | + +Unknown fields are refused with 400, so a misspelt `copeis` never prints the label's own quantity by accident. + +The answer is the same JSON as `rotula print --json`: + +```json +{ + "outcome": "printed", + "message": "Sent 2 record(s), 2 label(s), to Dock 1 in 1 job(s).", + "printer": "Dock 1", + "records": 2, + "labels": 2, + "jobs": 1, + "bytesSent": 1874, + "notices": [ { "code": "PrinterStatus.Verified", "severity": "warning", "message": "The printer reported that it finished the job." } ], + "reference": "WAVE-1207", + "succeeded": true +} +``` + +| `outcome` | HTTP | Meaning | +|---|---|---| +| `printed` | 200 | Every label was handed to the printer. Read `notices` for the verification. | +| `dryRun` | 200 | Nothing was sent; the ZPL is in `zpl`. | +| `invalid` | 400 | The request is wrong (copies out of range, values and records together...). | +| `invalidLabel` | 422 | The label has errors or questions are unanswered; `notices` say which. Nothing was sent. | +| `heldBack` | 409 | The printer reported a fault before a job; `errorCode` is `PaperOut`, `HeadOpen`, `RibbonOut`, `Paused`... Nothing more was sent. | +| `failed` | 502 | Sending failed (`errorCode`: `Offline`, `HostUnreachable`, `Timeout`...). `jobs` says how many went out before. | +| `cancelled` | 503 | The call was cancelled. | + +Refusals before the engine runs (no key 401, printer or label folder not allowed 403, label not found 404, unreadable body 400) +answer `{"errorCode":"Engine.Error.PrinterNotAllowed","message":"..."}`. + +### Log + +The server writes one line per job to its console (and to the Windows event log when it runs as a service): the reference, an +8-character id of the API key (a piece of its hash, useless for calling the server), the caller's address, the outcome, the printer, +the number of labels and jobs, and the error code. It never logs label data. + +## Policy + +An administrator can switch the engine off for a computer with the **Allow the print engine to print** policy (`PrintEngine`, +REG_DWORD `0`, in `packaging/policies`). The command line then refuses to print and to query printers (exit code 5), and the +server answers 403 to jobs and status requests; the health check reports `"printingBlocked": true`. The policy cannot switch +anything on; the server's own settings still decide what it allows. See [deployment.md](deployment.md). + +## Where it is + +The installer and the MSI put `rotula.exe` beside the designer: `%LOCALAPPDATA%\Programs\Rotula\rotula.exe` for a per-user +install, `C:\Program Files\Rotula\rotula.exe` for all users (MSI or `/ALLUSERS`). The portable zip has it too. It is not added to +`PATH`; call it by its full path or add the folder yourself. For a Windows service, use the all-users install so the service +account can reach the program. + +## Building it yourself + +`rotula.exe` is built from `src/Rotula.Cli` (the engine itself is `src/Rotula.Engine`, a library with no UI): + +```bat +dotnet publish src\Rotula.Cli\Rotula.Cli.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=none -o publish\win-x64-engine +``` diff --git a/docs/print-station.md b/docs/print-station.md new file mode 100644 index 0000000..6f59c2b --- /dev/null +++ b/docs/print-station.md @@ -0,0 +1,187 @@ +# Print station + +The print station is a locked, print-only screen for operators: on a packing line, at a goods-in desk or next to a +shared printer. It shows a set of labels as large tiles, asks the label's questions in large fields that take scanner +input, prints, and tells the operator in plain words what the printer did. It has no menus, no design tools and no +file dialogs. + +## Starting it + +| How | What opens | +|---|---| +| `Rotula.exe --station C:\Labels\Line2` | The labels of that folder, with the `station.json` in it when there is one | +| `Rotula.exe --station C:\Labels\Line2\station.json` | That configuration file (any file name works) | +| `Rotula.exe --station C:\Labels\pallet.rotula` | Only that label, with the `station.json` next to it when there is one | +| `Rotula.exe --station` | `%ProgramData%\Rotula\station.json`, so the station can be set up by policy | +| **File > Open print station...** in the designer | A folder you pick, as with `--station ` | + +`/station` and `--station=` work too. A shortcut with `--station` in the Startup folder (or a Windows +Assigned Access / kiosk setup) turns a PC into a dedicated station. + +When the configuration has a mistake, Rotula says which setting and line before the station opens; nothing is +guessed. A label that is listed but missing is only left out, with a warning at the top of the station. + +## Using it + +1. **Pick a label.** Tap its tile, or type part of its name in the search box and press Enter. When a label has a + `key`, scanning that code in the search box opens it directly. With a single label, the station opens it at once. +2. **Answer the questions.** Every question of the label (its prompted variables) is a large field, with the label's + own hint and default. A keyboard-wedge scanner types the code and ends it with Enter or Tab: the station checks the + answer and moves to the next field. Shift+Tab goes back. +3. **Print.** With `printOnLastScan` on (the default), the scan into the last field prints straight away. Otherwise + the focus moves to the big Print button, and Enter prints. The quantity box accepts 1 up to the label's + `maxCopies`. +4. **Read the result** in the large status area. The questions then empty for the next item; values the station is + told to keep stay filled in. + +The label's rules for each question (required, minimum and maximum length, pattern) are checked when a scan ends +with Enter or Tab, and again before printing. A refused scan stays in its field with the reason under it, so the operator scans +again instead of printing a wrong label. + +### What the status means + +| Status | Meaning | +|---|---| +| **Printer ready** | The printer answered and reports no problem. | +| **The printer needs attention** | The printer reports a problem (for example the printhead is open or the paper is out). The reason is shown below. | +| **Printer status unknown** | The printer could not be asked (a Windows printer that does not answer, or the check is off). Printing still works. | +| **Not printed: the printer needs attention** | Rotula asked the printer first and held the job back. Fix what it says and press Print again; nothing was sent. | +| **Printed and verified** | The job was sent and the printer then reported that it finished without a problem. | +| **Sent, but not confirmed** | The job was sent, but the printer could not confirm it. Look at the label before moving on. | +| **Sent, but the printer reports a problem** | The job was sent and the printer reported a problem straight after. Check the label that came out. | +| **Sent to the printer** | The job was sent; the printer's answer was not checked. | +| **Not printed** | Something else stopped the job (for example an unreachable printer). The reason is shown below. | + +The status check uses the same print path as the designer's Print dialog (see [printing.md](printing.md)). It is +on for every station printer unless that printer sets `"checkStatus": false`. + +### Printing the label's data + +When a label has data printing set up (a spreadsheet, CSV or database) and the station allows it +(`allowDataMerge`), the label page also shows the records with a **Print all records** button. Files and queries are +the ones saved with the label; the station never opens a file dialog. A label whose data source cannot be reached +says so and still prints single labels. + +### Leaving the station + +With an `exitPinHash`, the Exit button (and closing the window) asks for the PIN. Five wrong PINs lock the PIN box +for 30 seconds. Without a PIN, Exit leaves straight away. + +In full screen (`fullScreen`, the default) the window covers the task bar and has no title bar. Rotula cannot block +keys that Windows keeps for itself, such as the Windows key or Ctrl+Alt+Del; for a fully locked PC, use Windows +Assigned Access (kiosk mode) with `Rotula.exe --station` as the app. + +## station.json + +The file is JSON; comments (`//`) and trailing commas are allowed. Every setting is optional. Setting names are +checked exactly: a misspelt name is an error rather than being ignored, so a typo never silently changes what the +station does. + +```jsonc +{ + "formatVersion": 1, + "title": "Packing line 2", + "labelFolder": "labels", + "printers": { + "line": { "host": "192.168.1.50", "dpi": 300 }, + "desk": { "windowsPrinter": "Desk printer", "checkStatus": false } + }, + "defaultPrinter": "line", + "defaultCopies": 1, + "maxCopies": 50, + "allowDataMerge": false, + "keepValues": false, + "exitPinHash": "pbkdf2-sha256$600000$...$...", + "printOnLastScan": true, + "scanToSelect": true, + "fullScreen": true, + "labels": [ + { "file": "pallet.rotula", "name": "Pallet", "key": "PAL-01", "maxCopies": 5, "keepFields": ["lot"] }, + { "file": "case.rotula", "printer": "desk", "allowDataMerge": true } + ] +} +``` + +### Station settings + +| Setting | Default | Meaning | +|---|---|---| +| `formatVersion` | `1` | The format of the file. A newer number than this Rotula knows is refused. | +| `title` | "Print station" | Shown at the top of the station. | +| `labelFolder` | the file's folder | Where the labels are, relative to `station.json` or absolute. | +| `labels` | every `.rotula` in `labelFolder` | The labels to offer, in this order (see below). Without it, every label of the folder is offered by file name. | +| `printers` | none | Named printers (see below). | +| `defaultPrinter` | the only printer | The printer of labels that do not name one. With no printers at all, the printer last used in the designer on this PC is used. | +| `defaultCopies` | `1` | The quantity a label opens with. | +| `maxCopies` | `100` | The highest quantity an operator may print at once (1 to 99999999). | +| `allowDataMerge` | `false` | Whether labels may print their saved data (all records). | +| `keepValues` | `false` | Keep every answer after printing instead of emptying the fields. | +| `exitPinHash` | none | The protected code of the exit PIN (see below). Without it, leaving needs no PIN. | +| `printOnLastScan` | `true` | The scan into the last field prints. Off: it moves to the Print button instead. | +| `scanToSelect` | `true` | Scanning a label's `key` in the search box opens that label. | +| `fullScreen` | `true` | Open full screen without a title bar. | +| `$schema` | | Ignored; allowed so an editor can point at a schema. | + +### Label settings + +| Setting | Meaning | +|---|---| +| `file` | **Required.** The `.rotula` file, relative to `labelFolder` or absolute. | +| `name` | The name on the tile. Default: the file name. | +| `key` | The code that opens the label when scanned. Codes are compared without case and must be unique. | +| `printer` | One of the names under `printers`. | +| `defaultCopies`, `maxCopies`, `allowDataMerge`, `keepValues` | Override the station setting of the same name for this label. | +| `keepFields` | Answers (by variable name) to keep after printing, for example a lot number that stays the same all shift. | + +### Printer settings + +| Setting | Meaning | +|---|---| +| `windowsPrinter` | The name of a printer installed in Windows (USB, shared or network queue). | +| `host` | A network address; printed to directly over TCP. Give either `windowsPrinter` or `host`, not both. | +| `port` | The TCP port, 1 to 65535. Default `9100`. | +| `dpi` | 203, 300 or 600. Default: the label's own resolution. | +| `darkness` | 0 to 30. Default: the printer's own setting. | +| `speed` | 1 to 14 (inches per second). Default: the printer's own setting. | +| `checkStatus` | Ask the printer before and after each job. Default `true`. | + +### Problems Rotula reports + +Every problem has a stable message key (`StationConfig.*` in `Rotula.Files.Station.StationKeys`) and is shown in plain +words in the UI language: + +| Key | When | +|---|---| +| `NotFound` | The path, the configuration or the `labelFolder` does not exist. | +| `ReadFailed` | The file exists but cannot be read. | +| `InvalidJson` | A JSON mistake, a misspelt setting or a value of the wrong kind; the line and setting are named. | +| `NewerVersion` | `formatVersion` is newer than this Rotula. | +| `NoLabels` | There is no label left to offer. | +| `LabelMissing` | (warning) A listed label file does not exist; it is left out. | +| `LabelNotRotula` | A listed file is not a `.rotula` label. | +| `LabelNoFile` | A label entry has no `file`. | +| `DuplicateKey` | Two labels share a `key`. | +| `UnknownPrinter` | A label or `defaultPrinter` names a printer that is not defined. | +| `PrinterTarget` | A printer has neither or both of `windowsPrinter` and `host`. | +| `PrinterPort`, `PrinterDpi`, `PrinterProfile` | A printer's port, resolution, darkness or speed is out of range. | +| `Copies`, `LabelCopies` | `maxCopies` or `defaultCopies` is out of range, for the station or one label. | +| `PinHash` | `exitPinHash` is not a code made by Rotula. | + +## The exit PIN + +The PIN is never stored. `exitPinHash` holds a salted PBKDF2-SHA256 code made from it +(`pbkdf2-sha256$$$`, 600,000 iterations by default), which Rotula checks in constant time. +To make one, use **File > Create print station PIN...** in the designer, or run `Rotula.exe --station-pin` on any PC. +Type the PIN twice (4 to 32 characters), then copy the line it gives into `station.json`. + +Keep the folder of `station.json` writable only by the people who set up the station: anyone who can edit the file +can remove the PIN. + +## For developers + +- `Rotula.Files.Station.StationConfigLoader` reads and checks the configuration; `StationConfig` is the result. +- `StationPin` makes and checks the PIN codes. +- `ScanSequencer` holds the scanner rules (which terminator moves where, when a scan prints, which scan opens a label) + with no UI, so it is tested on its own. +- `StationViewModel` in the app drives the screen through `IPrintService` and the label's `PromptSet`, so the + station prints exactly what the Print dialog would. diff --git a/docs/printing.md b/docs/printing.md index b7509f2..b3e8774 100644 --- a/docs/printing.md +++ b/docs/printing.md @@ -45,9 +45,62 @@ A character that the code page cannot represent returns `Print.Error.EncodingFai `PrintAsync` to prepend them in the same job, send them alone with `ApplyProfileAsync`, or print a small sample label with `PrintTestLabelAsync`. +## Printer status (`~HS`) + +Rotula can ask a ZPL compatible printer for its host status with the `~HS` command and read the three-string reply. +`HostStatusParser.Parse` (UI-free, in `PrinterStatus.cs`) decodes it into a `PrinterStatus`: + +| String | Fields Rotula uses | +|---|---| +| 1 `aaa,b,c,dddd,eee,f,g,h,iii,j,k,l` | paper out (b), pause (c), label length in dots (dddd), formats in the receive buffer (eee), buffer full (f), communications diagnostics mode (g), partial format (h), corrupt RAM (j), under temperature (k), over temperature (l) | +| 2 `mmm,n,o,p,q,r,s,t,uuuuuuuu,v,www` | head up (o), ribbon out (p), thermal transfer (q), print mode (r), label waiting (t), labels remaining in the batch (uuuuuuuu), graphics stored (www) | +| 3 `xxxx,y` | not used | + +Each string is framed by STX (0x02) and ETX (0x03). Frames are recognised by their number of fields, so junk between frames +or strings in another order do not matter; a frame cut off before its ETX, or with a value that is not a number, is ignored +rather than guessed; a reply without any STX is read line by line. Only reported flags become `PrinterCondition`s, so a +partial reply never invents a fault, and `IsIdle` is only true when both strings were read. "Ribbon out" only counts in +thermal transfer mode. + +| Condition | Effect | +|---|---| +| `HeadOpen`, `PaperOut`, `RibbonOut`, `HeadTooHot`, `DiagnosticMode`, `BufferFull`, `Paused` | **Blocking**: the job is not sent (`Print.Error.`, args: printer, then every reported condition) | +| `HeadTooCold`, `CorruptRam`, `PartialFormat`, `LabelWaiting` | Warning: sent, with a `PrinterStatus.Warning` notice | + +**Reading the reply** (`IPrinterStatusReader`, implemented by `PrinterStatusReader`; fake it in tests): + +- **TCP**: one connection to host:port, write `~HS`, read until three frames arrived, the printer closes, or the timeout + (default 3 s for connect and reply together). A reply that already holds strings 1 and 2 is decoded even on a timeout. +- **Windows queue (best effort)**: a tiny RAW job `~HS`, then `ReadPrinter` on the same handle. Many USB and standard TCP + port monitors cannot read back; that is "status unavailable" (`NotSupported`), never an exception. The read runs on its + own thread and the caller stops waiting at the timeout, so a driver that blocks cannot hang printing. A queue Windows + reports offline is not queried (the query would wait in the queue). Sending `~HS` to a printer that is not ZPL compatible + would print it as text, which is why the check is off by default for Windows printers. + +**Around a job.** The check is opt-in per target: `PrinterTarget.WithStatus(PrinterStatusOptions)` (default `Off`, so library +callers send exactly what they always sent). The app sets it from the Print dialog switch "Check the printer before and +after printing", stored per transport in `settings.json` (`print.statusCheckNetwork`, default true; +`print.statusCheckWindows`, default false). + +- **Before**: `PrintAsync` asks before every block it sends (so paper running out halfway stops the run before the next + block), `PrintZplAsync` before its job. A blocking condition returns a failure with 0 bytes sent, so reserved serial + numbers go back. An unknown status never blocks: the job is sent with one `PrinterStatus.NotChecked` notice. + `ApplyProfileAsync` never asks (set-up commands print nothing). +- **After**: when every block was sent, the service waits `PollInterval` (1 s) and asks again until the printer reports + no format in the buffer, no label left and no partial format (`PrinterStatus.Verified`), a blocking fault + (`PrinterStatus.FaultAfterJob`), or `VerifyTimeout` (30 s) passes (`PrinterStatus.NotVerified`, reason `StillPrinting` or + the unavailable reason). It runs after the serial numbers were confirmed: a fault after the job keeps the labels counted as + possibly printed. Cancelling the wait gives `NotVerified` / `Stopped`; the result stays a success because the labels were + sent. "Verified" means the printer says it finished without a fault; it cannot see a label that printed badly. +- **Batches** (Data merge): every job is checked before it goes out; `IPrintService.VerifyPrintedAsync` runs once after the + last job. + +`IPrintService.GetStatusAsync(target)` backs the "Check printer status" button of the printer settings and the MCP tool +`get_printer_status` (which needs the printing permission because it sends a command to the printer). + ## Tests -Tests never print. The spooler is faked (`IRawPrinter`); TCP is tested against an in-process `TcpListener`. +Tests never print. The spooler is faked (`IRawPrinter`), status replies are scripted (`IPrinterStatusReader`); TCP printing and the TCP status query are tested against an in-process `TcpListener`. ## Reading the resolution from the driver @@ -76,3 +129,35 @@ a resolution the user picked is never overridden. The test label is sized from physical units (7 mm text, QR module size clamped to Core's 1-10) so it is readable and inside the label at 203/300/600 dpi. + +## Label sheets for office printers + +Labels can also be printed on sheets with a laser or inkjet printer. This path does not send ZPL: it draws the labels into a +PDF that is printed from any PDF reader. + +- **Model** (`Rotula.Core.Sheets`, no UI): `SheetLayout` is the paper (`Letter`, `A4`, `Legal` or a custom size), the top and + left margin, the label size, columns x rows and the pitch across and down (label size plus gap), all in millimetres. + `Validate()` returns message keys (`Sheet.Labels.TooWide`, `Sheet.Labels.OverlapAcross`, `Sheet.Labels.NearEdge` and the + others in `Issue.Sheet.*`); `ValidateStart` checks a start position and `CheckDesignSize` warns when the design is larger or + smaller than the positions. `SheetTemplates.All` holds generic Letter and A4 layouts described only by their dimensions; + `FindForStock` matches a sheet stock of the library by label size and grid, without naming a brand. +- **Imposition**: `SheetImposition.Paginate` pulls the labels lazily and fills the positions in reading order (or down the + columns), sheet after sheet. The first sheet can start at a later position (a partly used sheet); later sheets start at 1. +- **Values**: the labels come from the same iterations as printing. With data, `MergeBatch.Labels` yields each record with its + resolved values (`MergedLabel.Values`) and its copies; without data, `LabelPrintRun.LabelValues()` yields one value set per + copy (each copy of a label with a counter has its own number). The renderer draws them with + `LabelDocumentAdapter.ToRenderLabelResolved`, which does not resolve the variables again. +- **Serial numbers**: reserved before the PDF is written and confirmed only when the file is complete; a failed or cancelled + export gives them back. A PDF will be printed, so its numbers count as used, exactly as for "Save as ZPL". +- **Output**: `SheetExporter.SavePdf` writes one page per sheet at the paper's real size. Each label is drawn at its physical + size (its dots divided by its resolution), centred in its position and clipped to it; text, boxes and barcode modules stay + vectors. Optional crop marks (in the margins only) or grey outlines. The page size is exact for US paper and within 0.02 mm + for A4 (the PDF writer rounds it to whole pixels at 600 dpi). +- **The .rotula file** keeps the chosen layout in an optional root `sheet` object (template id, paper, margins, label size, + grid, pitch, order, guides). It does not raise the format version: an older build ignores it. The start position is not + saved, because it depends on the sheet in the printer today. + +**Not offered: sending the sheets straight to a Windows printer.** Office printer drivers add their own margins and often scale +to fit, which would move every label; a PDF printed at actual size (100 %, no "fit to page") keeps the positions. Sheets of +different suppliers with the same label size can differ by about a millimetre in their margins, so print one page on plain +paper with outlines, hold it against a sheet and adjust the margins. diff --git a/docs/releasing.md b/docs/releasing.md index 45aec8c..ad51681 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -7,7 +7,9 @@ gate and the hygiene check, and keeps an unsigned portable zip as a workflow art ## One-time set-up on the build machine - .NET 10 SDK, Inno Setup 6 (`winget install JRSoftware.InnoSetup --scope user`), the Azure CLI, and the Microsoft `sign` - tool (`dotnet tool install --global sign --prerelease`). + tool (`dotnet tool install --global sign --prerelease`). The WiX Toolset that builds the MSI needs no install: `dotnet build` + restores `WixToolset.Sdk` from NuGet the first time. +- For the winget submission: `winget install Microsoft.WingetCreate` (optional, see step 8). - For publishing from the script: the GitHub CLI (`winget install GitHub.cli`, then `gh auth login`). ## Cutting a release @@ -17,8 +19,9 @@ gate and the hygiene check, and keeps an unsigned portable zip as a workflow art bottom. The release notes are taken from that section. 3. Merge to the main branch, make sure CI is green, then create and push the tag `vX.Y.Z` on that commit. 4. Run `az login` (the signing uses that session). -5. Double-click `packaging\installer\build-installer.cmd`. It publishes the app and the MCP server (`Rotula.Mcp.exe`), writes the - combined licence text, signs both programs, builds the installer, signs it, verifies all three signatures, builds the +5. Double-click `packaging\installer\build-installer.cmd`. It publishes the app, the MCP server (`Rotula.Mcp.exe`) and the print engine + (`rotula.exe`), writes the combined licence text, signs the three programs, builds the per-user installer (Inno Setup) and the machine-wide MSI + (`packaging\msi`, WiX Toolset) from the signed programs, signs both, verifies all four signatures, builds the portable zip from the signed programs, writes the winget manifests and `SHA256SUMS.txt`. Everything is in `artifacts\` (git-ignored). If you are not logged in, the files are still built but labelled `UNSIGNED` and must not be published. Set `ROTULA_NOPAUSE=1` to skip the final "press Enter" (automation only). @@ -28,14 +31,24 @@ gate and the hygiene check, and keeps an unsigned portable zip as a workflow art ``` gh release create vX.Y.Z --repo P4Software/Rotula --title "Rotula X.Y.Z" --verify-tag --notes-file ^ - artifacts\Rotula-X.Y.Z-win-x64-setup.exe artifacts\Rotula-X.Y.Z-win-x64-portable.zip ^ + artifacts\Rotula-X.Y.Z-win-x64-setup.exe artifacts\Rotula-X.Y.Z-win-x64.msi artifacts\Rotula-X.Y.Z-win-x64-portable.zip ^ artifacts\LICENSE-THIRD-PARTY.txt artifacts\SHA256SUMS.txt artifacts\Rotula-X.Y.Z-winget-manifests.zip ``` The script never replaces files of a release that already exists; it adds only the missing ones and says so. -8. **Winget**: copy `artifacts\winget\manifests\p\P4Software\Rotula\X.Y.Z\` into a fork of `microsoft/winget-pkgs`, check it - with `winget validate`, and open a pull request. The manifest already holds the real installer hash and download URL, so - submit it only after the GitHub Release is public. +8. **Winget** (only after the GitHub Release is public: the manifests hold the real download URLs and hashes). The set in + `artifacts\winget\manifests\p\P4Software\Rotula\X.Y.Z\` has four files: version, installer (the Inno exe for + `--scope user` and the MSI for `--scope machine`), en-US default locale and es-MX locale. + 1. Check it: `winget validate --manifest artifacts\winget\manifests\p\P4Software\Rotula\X.Y.Z`. + 2. Try it in Windows Sandbox or a test VM: `winget settings --enable LocalManifestFiles` (once, elevated), then + `winget install --manifest ` and `winget install --manifest --scope machine`, and uninstall both. + 3. Submit it, either with wingetcreate, which forks `microsoft/winget-pkgs` and opens the pull request for you: + `wingetcreate submit --token artifacts\winget\manifests\p\P4Software\Rotula\X.Y.Z` + or by hand: fork `microsoft/winget-pkgs`, copy the folder to `manifests/p/P4Software/Rotula/X.Y.Z/` on a new branch, and + open a pull request titled `New version: P4Software.Rotula version X.Y.Z` (the first one: `New package: ...`). + 4. Follow the pull request: the winget bots validate, install and scan the package and label the pull request; answer any + `Needs-Author-Feedback` label. A moderator merges it, and the version reaches `winget search` within a few hours. + Never edit the hashes by hand; if a release file changes, build again (and use a new version number). ## Verifying a signature @@ -44,14 +57,16 @@ Get-AuthenticodeSignature .\artifacts\Rotula-X.Y.Z-win-x64-setup.exe | Format-Li ``` `Status` must be `Valid`, the signer subject must contain `Barrdega Sistemas NA LLC`, and a timestamp certificate must be -present. Do the same for `Rotula.App.exe` and `Rotula.Mcp.exe` inside the portable zip. The build script performs these checks itself and never +present. Do the same for the MSI, and for `Rotula.App.exe`, `Rotula.Mcp.exe` and `rotula.exe` inside the portable zip. The build script performs these checks itself and never trusts the exit code of the signing tool. Compare downloads with `SHA256SUMS.txt` (`Get-FileHash`). ## What is in a release -- `Rotula-X.Y.Z-win-x64-setup.exe`: per-user installer. Installs `Rotula.App.exe`, `Rotula.Mcp.exe`, `LICENSE`, - `LICENSE-THIRD-PARTY.txt`, `THIRD-PARTY-NOTICES.md`, `README.md`, `docs\mcp.md` and an empty `packs` folder (with a one-line +- `Rotula-X.Y.Z-win-x64-setup.exe`: per-user installer. Installs `Rotula.App.exe`, `Rotula.Mcp.exe`, `rotula.exe`, `LICENSE`, + `LICENSE-THIRD-PARTY.txt`, `THIRD-PARTY-NOTICES.md`, `README.md`, `docs\mcp.md`, `docs\print-engine.md` and an empty `packs` folder (with a one-line `README.txt`) beside the program. Uninstalling removes them, and the MCP settings file created next to the exe on first run. +- `Rotula-X.Y.Z-win-x64.msi`: machine-wide Windows Installer package for administrators (Program Files, every user; Intune, + Group Policy, Configuration Manager). The same files as the setup exe. See [deployment.md](deployment.md). - `Rotula-X.Y.Z-win-x64-portable.zip`: the same files, nothing installed. - `LICENSE-THIRD-PARTY.txt`: also a separate release file, because the winget manifest links to it as the agreement. - `SHA256SUMS.txt` and `Rotula-X.Y.Z-winget-manifests.zip`. diff --git a/docs/stock-catalog.md b/docs/stock-catalog.md index d026f1b..3569151 100644 --- a/docs/stock-catalog.md +++ b/docs/stock-catalog.md @@ -177,8 +177,10 @@ reported; it never stops Rotula from starting. - The gap between labels along the roll is unknown for every entry except one (black-mark sensing). Calibrate the printer on the stock. -- Multi-up (several labels across, or label sheets) is not supported yet. `columns`, `rows` and the gaps are in the - data and are reported by `StockApplier`, but the label document is laid out as a single label. +- Multi-up rolls (several labels across) are not supported yet. `columns`, `rows` and the gaps are in the data and are + reported by `StockApplier`, but the label document is laid out as a single label. A sheet stock whose label size and grid + match a built-in sheet template (`SheetTemplates.FindForStock`) opens the label sheets export on that template; the catalogue + states no margins for sheets, so the template's geometry is used. - 40 entries are unverified: 15 where two sources disagree on size or series, 23 stated only by the PDF extraction (11 Zebra part numbers and the 12 generic `std-*` sizes), 2 where the export lists the part twice with different sizes. They are listed in the rebuild output. - Roll length for long rolls comes from the stock name; the label height of those continuous rolls is the user's choice. diff --git a/docs/zpl-coverage.md b/docs/zpl-coverage.md index 52fb9b6..365ad1d 100644 --- a/docs/zpl-coverage.md +++ b/docs/zpl-coverage.md @@ -82,8 +82,8 @@ Nothing is silently dropped. | `^LS` | Label shift | Supported | Yes | Yes | Exact | Shifts every field to the left by a number of dots; a negative value shifts to the right. Written only when it is not zero. | | `^LT` | Label top | Supported | Yes | Yes | Exact | Moves the whole format down (or up for a negative value) by a few dot rows to fine-tune where it lands on the label. Rotula accepts -240 to 240; most printers only accept -120 to 120. | | `^MU` | Units of measure | Supported | No | Yes | n/a | Switches the units used by later numbers to dots, inches or millimetres. Values are converted to dots on import and a notice is shown. | -| `^PM` | Mirror the label | Supported | Yes | Yes | No | Prints the whole label as a mirror image, left to right. The on-screen preview does not mirror. | -| `^PO` | Print orientation | Supported | Yes | Yes | No | Prints the whole label rotated by 180 degrees. The on-screen preview always shows the normal orientation. | +| `^PM` | Mirror the label | Supported | Yes | Yes | Exact | Prints the whole label as a mirror image, left to right. The Preview window shows it mirrored ("As printed"); the canvas and the image and PDF exports stay as designed. | +| `^PO` | Print orientation | Supported | Yes | Yes | Exact | Prints the whole label rotated by 180 degrees. The Preview window shows it upside down ("As printed", with a switch to "As designed"); the canvas and the image and PDF exports stay upright. | | `^PQ` | Print quantity | Supported | Yes | Yes | n/a | Sets how many labels print, how many between pauses, and how many copies of each serial number. | | `^PW` | Print width | Supported | Yes | Yes | Exact | Sets the width of the printable area in dots. | | `^XA` | Start of label | Supported | Yes | Yes | n/a | Opens a label format. Everything up to the matching end-of-label command belongs to one label. | @@ -280,7 +280,7 @@ Nothing is silently dropped. | `~HI` | Host identification | Out of scope | No | No | No | Asks the printer to identify itself. *Out of scope: A query that makes the printer answer the host; a label file has no use for the reply.* | | `~HM` | Host RAM status | Out of scope | No | No | No | Asks for the memory status. *Out of scope: A query that makes the printer answer the host; a label file has no use for the reply.* | | `~HQ` | Host query | Out of scope | No | No | No | Asks for a named piece of printer information. *Out of scope: A query that makes the printer answer the host; a label file has no use for the reply.* | -| `~HS` | Host status | Out of scope | No | No | No | Asks for the printer status. *Out of scope: A query that makes the printer answer the host; a label file has no use for the reply.* | +| `~HS` | Host status | Out of scope | No | No | No | Asks for the printer status. *Out of scope: A query that makes the printer answer the host; a label file has no use for the reply. Rotula's printing sends it on its own to check the printer before a job and to verify the job afterwards (see docs/printing.md).* | | `^HT` | Host linked fonts list | Out of scope | No | No | No | Asks for the list of linked fonts. *Out of scope: A query that makes the printer answer the host; a label file has no use for the reply.* | | `~HU` | Alert configuration return | Out of scope | No | No | No | Sends the alert configuration back to the host. *Out of scope: A query that makes the printer answer the host; a label file has no use for the reply.* | | `^HV` | Host verification | Out of scope | No | No | No | Sends the data of a field back to the host. *Out of scope: A query that makes the printer answer the host; a label file has no use for the reply.* | diff --git a/packaging/installer/Rotula.iss b/packaging/installer/Rotula.iss index ee26746..d35fc60 100644 --- a/packaging/installer/Rotula.iss +++ b/packaging/installer/Rotula.iss @@ -38,7 +38,8 @@ DefaultGroupName=Rotula DisableProgramGroupPage=yes ; Per-user install: no UAC prompt, and winget can install it silently without elevation. PrivilegesRequired=lowest -PrivilegesRequiredOverridesAllowed=dialog +; /ALLUSERS (all users, Program Files, needs administrator rights) and /CURRENTUSER work on the command line for silent deployment (docs/deployment.md). +PrivilegesRequiredOverridesAllowed=commandline dialog ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible ; The page the user must accept: Rotula's MIT licence plus the full terms of every non-permissive component that ships @@ -63,6 +64,9 @@ VersionInfoProductVersion={#AppVersionNumeric} [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" +; The same four languages as the application (Loc.SupportedLanguages). Both files ship with Inno Setup itself. +Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl" +Name: "french"; MessagesFile: "compiler:Languages\French.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked @@ -73,9 +77,12 @@ Name: "associate"; Description: "Open .rotula label files with Rotula"; GroupDes Source: "{#SourceDir}\Rotula.App.exe"; DestDir: "{app}"; Flags: ignoreversion ; The MCP server: a console program for AI clients, started by them, so it gets no Start menu entry. docs\mcp.md says how to connect it. Source: "{#SourceDir}\Rotula.Mcp.exe"; DestDir: "{app}"; Flags: ignoreversion +; The print engine (command line and REST API) for scripts and warehouse systems: no Start menu entry. docs\print-engine.md. +Source: "{#SourceDir}\rotula.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#SourceDir}\LICENSE"; DestDir: "{app}"; Flags: ignoreversion Source: "{#SourceDir}\LICENSE-THIRD-PARTY.txt"; DestDir: "{app}"; Flags: ignoreversion Source: "{#SourceDir}\docs\mcp.md"; DestDir: "{app}\docs"; Flags: ignoreversion +Source: "{#SourceDir}\docs\print-engine.md"; DestDir: "{app}\docs"; Flags: ignoreversion Source: "{#SourceDir}\packs\README.txt"; DestDir: "{app}\packs"; Flags: ignoreversion Source: "{#SourceDir}\THIRD-PARTY-NOTICES.md"; DestDir: "{app}"; Flags: ignoreversion Source: "{#SourceDir}\README.md"; DestDir: "{app}"; Flags: ignoreversion diff --git a/packaging/installer/build-installer.ps1 b/packaging/installer/build-installer.ps1 index 37ebc18..966b532 100644 --- a/packaging/installer/build-installer.ps1 +++ b/packaging/installer/build-installer.ps1 @@ -2,8 +2,9 @@ Builds the signed Rotula release. Double-click build-installer.cmd (it runs this script). Steps: read the version, publish the app and the MCP server, write the combined licence text, - sign both programs, build the installer, sign the installer, verify all three signatures, build - the portable zip from the SIGNED programs, write the winget manifests and the checksums. Everything lands in the git-ignored "artifacts" folder. + sign both programs, build the per-user installer (Inno Setup) and the machine-wide MSI (WiX Toolset) from the SIGNED + programs, sign both, verify all four signatures, build the portable zip from the SIGNED programs, write the winget + manifests and the checksums. Everything lands in the git-ignored "artifacts" folder. Signing uses Azure Artifact Signing (account p4software) through your Azure login (az login). If signing is not possible the files are still built and clearly marked UNSIGNED, and they are @@ -140,7 +141,7 @@ function Find-ThirdPartyLicense($packageId) { # (obj\project.assets.json) of the shipped projects, the folder from the package folders recorded there. Nothing is # guessed from the global cache, so an old cached version can never be picked up by mistake. $versions = @{} - foreach ($proj in 'Rotula.App', 'Rotula.Mcp') { + foreach ($proj in 'Rotula.App', 'Rotula.Mcp', 'Rotula.Cli') { $assetsPath = Join-Path $repo "src\$proj\obj\project.assets.json" if (-not (Test-Path -LiteralPath $assetsPath)) { throw "The restore output for $proj was not found ($assetsPath). Run the build again." } $assets = Get-Content -LiteralPath $assetsPath -Raw | ConvertFrom-Json @@ -223,6 +224,9 @@ try { (Join-Path ${env:ProgramFiles(x86)} 'Inno Setup 6\ISCC.exe'), (Join-Path $env:ProgramFiles 'Inno Setup 6\ISCC.exe')) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1 if (-not $iscc) { throw "Inno Setup is not installed. Install it with: winget install JRSoftware.InnoSetup --scope user (then double-click build-installer.cmd again)" } + # The MSI is built with the WiX Toolset SDK, which dotnet restores from NuGet on the first build (nothing to install by hand). + $msiProject = Join-Path $repo 'packaging\msi\Rotula.Msi.wixproj' + if (-not (Test-Path -LiteralPath $msiProject)) { throw "The MSI project was not found at $msiProject." } # --- Version ------------------------------------------------------------------------------ if ($cfg.versionOverride) { $version = [string]$cfg.versionOverride } @@ -232,6 +236,8 @@ try { } $version = $version.Trim().TrimStart('v') if ($version -notmatch '^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$') { throw "The version '$version' is not major.minor.patch (optionally with a -suffix). Fix in Directory.Build.props." } + # Windows Installer only knows numeric versions: 0.4.0-rc1 becomes 0.4.0 inside the MSI (the file name keeps the full text). + $versionNumeric = $version.Split('-')[0] Write-Host "Building Rotula $version" -ForegroundColor Cyan # --- Signing availability ----------------------------------------------------------------- @@ -247,12 +253,13 @@ try { New-Item -ItemType Directory -Force -Path $artifacts, $stage | Out-Null # --- 1. Publish --------------------------------------------------------------------------- - Write-Host '1/6 Publishing the app and the MCP server (this takes a few minutes)...' + Write-Host '1/6 Publishing the app, the MCP server and the print engine (this takes a few minutes)...' # Same flags for both programs. DebugType=none keeps the huge SkiaSharp symbol file (.pdb) out of the output; only the # single .exe of each program is copied to the stage folder, so no .pdb or documentation .xml can slip into a release. $publishMcp = Join-Path $repo 'publish\win-x64-mcp' - if (Test-Path -LiteralPath $publishMcp) { Remove-Item -LiteralPath $publishMcp -Recurse -Force } - foreach ($p in @(@('Rotula.App', $publish), @('Rotula.Mcp', $publishMcp))) { + $publishEngine = Join-Path $repo 'publish\win-x64-engine' + foreach ($d in @($publishMcp, $publishEngine)) { if (Test-Path -LiteralPath $d) { Remove-Item -LiteralPath $d -Recurse -Force } } + foreach ($p in @(@('Rotula.App', $publish), @('Rotula.Mcp', $publishMcp), @('Rotula.Cli', $publishEngine))) { & dotnet publish (Join-Path $repo "src\$($p[0])\$($p[0]).csproj") -c Release -r win-x64 --self-contained true ` -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=none ` "-p:Version=$version" -o $p[1] @@ -261,9 +268,11 @@ try { Copy-Item -LiteralPath (Join-Path $publish 'Rotula.App.exe') -Destination $stage Copy-Item -LiteralPath (Join-Path $publishMcp 'Rotula.Mcp.exe') -Destination $stage + Copy-Item -LiteralPath (Join-Path $publishEngine 'rotula.exe') -Destination $stage foreach ($f in 'LICENSE', 'THIRD-PARTY-NOTICES.md', 'README.md') { Copy-Item -LiteralPath (Join-Path $repo $f) -Destination $stage } New-Item -ItemType Directory -Force -Path (Join-Path $stage 'docs'), (Join-Path $stage 'packs') | Out-Null Copy-Item -LiteralPath (Join-Path $repo 'docs\mcp.md') -Destination (Join-Path $stage 'docs') + Copy-Item -LiteralPath (Join-Path $repo 'docs\print-engine.md') -Destination (Join-Path $stage 'docs') # The icon of .rotula documents: the installer registers it by path, and the portable zip carries it too. New-Item -ItemType Directory -Force -Path (Join-Path $stage 'Assets') | Out-Null Copy-Item -LiteralPath (Join-Path $repo 'src\Rotula.App\Assets\rotula-document.ico') -Destination (Join-Path $stage 'Assets') @@ -274,31 +283,44 @@ try { Write-Host " Combined licence written; includes the full terms of: $($shipped -join ', ')" $exe = Join-Path $stage 'Rotula.App.exe' $mcpExe = Join-Path $stage 'Rotula.Mcp.exe' + $engineExe = Join-Path $stage 'rotula.exe' # --- 2. Sign the program ------------------------------------------------------------------ $signed = $false Write-Host '2/6 Signing the programs...' - if ($canSign) { $signed = ((SignFile $exe) -and (SignFile $mcpExe)) } else { Write-Host ' Skipped (see the note above).' } + if ($canSign) { $signed = ((SignFile $exe) -and (SignFile $mcpExe) -and (SignFile $engineExe)) } else { Write-Host ' Skipped (see the note above).' } if ($canSign -and -not $signed) { Write-Host ' Signing failed; the output will be UNSIGNED.' -ForegroundColor Yellow } - # --- 3. Installer ------------------------------------------------------------------------- - Write-Host '3/6 Building the installer...' + # --- 3. Installers ------------------------------------------------------------------------ + Write-Host '3/6 Building the installers...' & $iscc "/DAppVersion=$version" "/DSourceDir=$stage" "/DOutputDir=$artifacts" (Join-Path $PSScriptRoot 'Rotula.iss') if ($LASTEXITCODE -ne 0) { throw 'The installer could not be built. Read the messages above.' } $setup = Join-Path $artifacts "Rotula-$version-win-x64-setup.exe" if (-not (Test-Path -LiteralPath $setup)) { throw "The installer was not found at $setup after the build." } - # --- 4. Sign the installer ---------------------------------------------------------------- - Write-Host '4/6 Signing the installer...' + # The MSI packs the same stage folder, so it carries the SIGNED programs too. Its build output stays in publish\ (git-ignored). + $msiOut = Join-Path $repo 'publish\msi' + if (Test-Path -LiteralPath $msiOut) { Remove-Item -LiteralPath $msiOut -Recurse -Force } + & dotnet build $msiProject -c Release "-p:ProductVersion=$versionNumeric" "-p:StageDir=$stage" -o $msiOut + if ($LASTEXITCODE -ne 0) { throw 'The MSI could not be built. Read the messages above.' } + $builtMsi = Get-ChildItem -LiteralPath $msiOut -Recurse -Filter 'Rotula.msi' | Select-Object -First 1 + if (-not $builtMsi) { throw "The MSI was not found in $msiOut after the build." } + $msi = Join-Path $artifacts "Rotula-$version-win-x64.msi" + Copy-Item -LiteralPath $builtMsi.FullName -Destination $msi + + # --- 4. Sign the installers --------------------------------------------------------------- + Write-Host '4/6 Signing the installers...' $setupSigned = $false - if ($signed) { $setupSigned = SignFile $setup } else { Write-Host ' Skipped.' } + if ($signed) { $setupSigned = ((SignFile $setup) -and (SignFile $msi)) } else { Write-Host ' Skipped.' } # --- 5. Verify ---------------------------------------------------------------------------- Write-Host '5/6 Checking the signatures...' Show-Signature $exe Show-Signature $mcpExe + Show-Signature $engineExe Show-Signature $setup - $allSigned = ((Test-Signed $exe) -and (Test-Signed $mcpExe) -and (Test-Signed $setup)) + Show-Signature $msi + $allSigned = ((Test-Signed $exe) -and (Test-Signed $mcpExe) -and (Test-Signed $engineExe) -and (Test-Signed $setup) -and (Test-Signed $msi)) # --- 6. Zip, manifests, checksums --------------------------------------------------------- Write-Host '6/6 Packaging the portable zip, winget manifests and checksums...' @@ -312,13 +334,16 @@ try { if ($allSigned) { $wingetOut = Join-Path $artifacts 'winget' - & (Join-Path $repo 'scripts\New-WingetManifest.ps1') -Version $version -InstallerPath $setup -OutputDir $wingetOut -Repository $cfg.githubRepository + & (Join-Path $repo 'scripts\New-WingetManifest.ps1') -Version $version -InstallerPath $setup -MsiPath $msi -OutputDir $wingetOut -Repository $cfg.githubRepository New-ZipFromFolder $wingetOut (Join-Path $artifacts "Rotula-$version-winget-manifests.zip") } else { # Rename so an unsigned installer can never be mistaken for the release file; no winget files for it. $unsignedSetup = Join-Path $artifacts "Rotula-$version-win-x64-setup-UNSIGNED.exe" Move-Item -LiteralPath $setup -Destination $unsignedSetup $setup = $unsignedSetup + $unsignedMsi = Join-Path $artifacts "Rotula-$version-win-x64-UNSIGNED.msi" + Move-Item -LiteralPath $msi -Destination $unsignedMsi + $msi = $unsignedMsi Set-Content -LiteralPath (Join-Path $artifacts 'UNSIGNED-DO-NOT-PUBLISH.txt') -Encoding ASCII -Value @( 'These files are NOT signed. Do not publish them.', "Run 'az login', then double-click build-installer.cmd again to get signed files.") @@ -339,7 +364,7 @@ try { if ($cfg.publishToGitHub) { Write-Host 'publishToGitHub is true, but nothing was published because the files are not signed.' -ForegroundColor Yellow } } else { Write-Host '' - Write-Host "Done. All programs and the installer are signed by '$expectedSigner'. Output: $artifacts" -ForegroundColor Green + Write-Host "Done. All programs and both installers are signed by '$expectedSigner'. Output: $artifacts" -ForegroundColor Green # --- Optional publish ------------------------------------------------------------------- if (-not $cfg.publishToGitHub) { @@ -352,7 +377,7 @@ try { $null = & gh api "repos/$repoName/git/ref/tags/$tag" 2>$null if ($LASTEXITCODE -ne 0) { throw "The tag $tag does not exist on GitHub yet. Tag the commit you built and push the tag, then build again." } - $assets = @($setup, $zip, $licenseAsset, (Join-Path $artifacts 'SHA256SUMS.txt'), (Join-Path $artifacts "Rotula-$version-winget-manifests.zip")) + $assets = @($setup, $msi, $zip, $licenseAsset, (Join-Path $artifacts 'SHA256SUMS.txt'), (Join-Path $artifacts "Rotula-$version-winget-manifests.zip")) $notes = Join-Path $artifacts 'release-notes.md' $haveNotes = Get-ReleaseNotes $version $notes $null = & gh release view $tag --repo $repoName 2>$null diff --git a/packaging/msi/Package.wxs b/packaging/msi/Package.wxs new file mode 100644 index 0000000..fbc4c4b --- /dev/null +++ b/packaging/msi/Package.wxs @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/msi/Rotula.Msi.wixproj b/packaging/msi/Rotula.Msi.wixproj new file mode 100644 index 0000000..4005006 --- /dev/null +++ b/packaging/msi/Rotula.Msi.wixproj @@ -0,0 +1,29 @@ + + + + + Rotula + Package + x64 + + 0.0.0 + $(MSBuildThisFileDirectory)..\..\artifacts\stage + $(MSBuildThisFileDirectory)..\..\ + ProductVersion=$(ProductVersion);StageDir=$(StageDir);RepoRoot=$(RepoRoot) + + false + + + + + + + diff --git a/packaging/policies/Rotula.admx b/packaging/policies/Rotula.admx new file mode 100644 index 0000000..59f52e7 --- /dev/null +++ b/packaging/policies/Rotula.admx @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + auto + + + en + + + es + + + pt-BR + + + fr-CA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/policies/en-US/Rotula.adml b/packaging/policies/en-US/Rotula.adml new file mode 100644 index 0000000..c122d03 --- /dev/null +++ b/packaging/policies/en-US/Rotula.adml @@ -0,0 +1,88 @@ + + + Rotula + Settings for the Rotula label designer. + + + Rotula 0.4.0 or later + P4 Software + Rotula + + Language of Rotula + Sets the language of Rotula and locks it: View > Language is disabled and says "Managed by your organization". + +"Same as Windows" uses Spanish when the Windows display language is Spanish, Portuguese (Brazil) when it is any Portuguese, French (Canada) when it is any French, and English otherwise. + +Not configured or disabled: each user chooses the language. + +Registry value: Language (text: auto, en, es, pt-BR or fr-CA). + Same as Windows + English + Spanish + Portuguese (Brazil) + French (Canada) + + Check for updates + Controls the daily update check and locks the choice (Help > Check for updates once a day). + +Enabled: once a day Rotula asks the public GitHub page of the project which version is the newest, with one anonymous request. Nothing about the user or the computer is sent. When a newer version exists, a notice with a link to the release page appears. Rotula never downloads or installs anything itself. + +Disabled: Rotula makes no update request at all, and users cannot switch it on. + +Not configured: each user decides. The check is off until the user switches it on. + +Registry value: UpdateCheck (number: 1 on, 0 off). + + Default printer + Preselects this Windows printer in the Print dialog for users who have not chosen a printer yet. Users can still choose another printer; their choice is remembered. + +Type the printer name exactly as Windows shows it. If the printer is not installed on the computer, Rotula falls back to the Windows default printer without a message. + +Registry value: DefaultPrinter (text). + + Allow data source add-ons from the user's folder + Data source add-ons are programs. Rotula loads add-ons from the "packs" folder next to the program (installed by an administrator) and, after the user approves each one, from %AppData%\Rotula\packs. + +Disabled: add-ons in the user's folder are not loaded and not offered for approval. Add-ons in the installed "packs" folder still work. + +Enabled or not configured: users may approve add-ons in their own folder. + +Registry value: UserAddOns (number: 0 blocks the user's folder). + + Allow the MCP server to print + The Rotula MCP server (Rotula.Mcp.exe) lets an AI assistant design labels. Printing is off unless its settings file allows it. + +Disabled: the MCP server never prints, whatever its settings file says. + +Enabled or not configured: the settings file of the MCP server decides (allowPrinting and allowedPrinters). This policy cannot switch printing on by itself. + +Registry value: McpPrinting (number: 0 blocks). + + Allow the MCP server to read databases + Disabled: the Rotula MCP server never reads saved database connections, whatever its settings file says. + +Enabled or not configured: the settings file of the MCP server decides (allowDatabase). This policy cannot switch database access on by itself. + +Registry value: McpDatabase (number: 0 blocks). + Allow the print engine to print + The print engine (rotula.exe) prints labels without the designer: from a command line, a script or a warehouse system calling its REST API. + +Disabled: the print engine never prints and never asks printers for their status, from the command line or its REST server. + +Enabled or not configured: the command line prints with the rights of whoever runs it, and the REST server prints only what its settings file allows (API keys, label folders, allowedPrinters). This policy cannot switch anything on by itself. + +Registry value: PrintEngine (number: 0 blocks). + + + + Language: + + + + + + + + + diff --git a/packaging/policies/es-ES/Rotula.adml b/packaging/policies/es-ES/Rotula.adml new file mode 100644 index 0000000..71fa4d4 --- /dev/null +++ b/packaging/policies/es-ES/Rotula.adml @@ -0,0 +1,88 @@ + + + Rotula + Configuración del diseñador de etiquetas Rotula. + + + Rotula 0.4.0 o posterior + P4 Software + Rotula + + Idioma de Rotula + Fija el idioma de Rotula y lo bloquea: Ver > Idioma queda desactivado e indica "Administrado por tu organización". + +"Igual que Windows" usa el español cuando el idioma de presentación de Windows es español, el portugués (Brasil) cuando es cualquier portugués, el francés (Canadá) cuando es cualquier francés, y el inglés en los demás casos. + +Sin configurar o deshabilitada: cada usuario elige el idioma. + +Valor del registro: Language (texto: auto, en, es, pt-BR o fr-CA). + Igual que Windows + Inglés + Español + Portugués (Brasil) + Francés (Canadá) + + Buscar actualizaciones + Controla la búsqueda diaria de actualizaciones y bloquea la opción (Ayuda > Buscar actualizaciones una vez al día). + +Habilitada: una vez al día Rotula pregunta a la página pública del proyecto en GitHub cuál es la versión más reciente, con una sola solicitud anónima. No se envía nada sobre el usuario ni sobre el equipo. Si hay una versión más reciente, aparece un aviso con un enlace a la página de la versión. Rotula nunca descarga ni instala nada por sí mismo. + +Deshabilitada: Rotula no hace ninguna solicitud de actualización y los usuarios no pueden activarla. + +Sin configurar: decide cada usuario. La búsqueda está desactivada hasta que el usuario la activa. + +Valor del registro: UpdateCheck (número: 1 activada, 0 desactivada). + + Impresora predeterminada + Preselecciona esta impresora de Windows en el cuadro Imprimir para los usuarios que todavía no han elegido una impresora. Los usuarios pueden elegir otra; su elección se recuerda. + +Escribe el nombre de la impresora exactamente como lo muestra Windows. Si la impresora no está instalada en el equipo, Rotula usa la impresora predeterminada de Windows sin mostrar ningún mensaje. + +Valor del registro: DefaultPrinter (texto). + + Permitir complementos de orígenes de datos en la carpeta del usuario + Los complementos de orígenes de datos son programas. Rotula carga los complementos de la carpeta "packs" junto al programa (instalados por un administrador) y, después de que el usuario apruebe cada uno, los de %AppData%\Rotula\packs. + +Deshabilitada: los complementos de la carpeta del usuario no se cargan ni se ofrecen para aprobar. Los de la carpeta "packs" instalada siguen funcionando. + +Habilitada o sin configurar: los usuarios pueden aprobar complementos en su propia carpeta. + +Valor del registro: UserAddOns (número: 0 bloquea la carpeta del usuario). + + Permitir que el servidor MCP imprima + El servidor MCP de Rotula (Rotula.Mcp.exe) permite que un asistente de IA diseñe etiquetas. La impresión está desactivada salvo que su archivo de configuración la permita. + +Deshabilitada: el servidor MCP nunca imprime, diga lo que diga su archivo de configuración. + +Habilitada o sin configurar: decide el archivo de configuración del servidor MCP (allowPrinting y allowedPrinters). Esta directiva no puede activar la impresión por sí sola. + +Valor del registro: McpPrinting (número: 0 bloquea). + + Permitir que el servidor MCP lea bases de datos + Deshabilitada: el servidor MCP de Rotula nunca lee las conexiones a bases de datos guardadas, diga lo que diga su archivo de configuración. + +Habilitada o sin configurar: decide el archivo de configuración del servidor MCP (allowDatabase). Esta directiva no puede activar el acceso a bases de datos por sí sola. + +Valor del registro: McpDatabase (número: 0 bloquea). + Permitir que el motor de impresión imprima + El motor de impresión (rotula.exe) imprime etiquetas sin el diseñador: desde la línea de comandos, un script o un sistema de almacén que llama a su API REST. + +Deshabilitada: el motor de impresión nunca imprime ni consulta el estado de las impresoras, ni desde la línea de comandos ni desde su servidor REST. + +Habilitada o no configurada: la línea de comandos imprime con los permisos de quien la ejecuta, y el servidor REST imprime solo lo que permite su archivo de configuración (claves de API, carpetas de etiquetas, allowedPrinters). Esta directiva no puede activar nada por sí sola. + +Valor del registro: PrintEngine (número: 0 bloquea). + + + + Idioma: + + + + + + + + + diff --git a/packaging/winget/P4Software.Rotula.installer.yaml b/packaging/winget/P4Software.Rotula.installer.yaml index 98e2266..e936b7e 100644 --- a/packaging/winget/P4Software.Rotula.installer.yaml +++ b/packaging/winget/P4Software.Rotula.installer.yaml @@ -1,22 +1,42 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json +# Two installers for the same package: the per-user Inno Setup installer (no administrator rights; the default for +# "winget install P4Software.Rotula") and the machine-wide MSI ("--scope machine"). Double-underscore placeholders are filled by +# scripts/New-WingetManifest.ps1, which reads the MSI's ProductCode from the signed package. PackageIdentifier: P4Software.Rotula PackageVersion: __VERSION__ -InstallerType: inno -Scope: user InstallModes: - interactive - silent - silentWithProgress UpgradeBehavior: install -# Inno Setup registers the product as _is1; naming it lets winget match the installed app for upgrades. -AppsAndFeaturesEntries: - - DisplayName: Rotula - Publisher: P4 Software - ProductCode: "{6B0E5C1A-3F7D-4C52-9A1E-2D8F4B7A9C31}_is1" +FileExtensions: + - rotula +ReleaseDate: __RELEASE_DATE__ Installers: - Architecture: x64 + InstallerType: inno + Scope: user InstallerUrl: __INSTALLER_URL__ InstallerSha256: __INSTALLER_SHA256__ -ReleaseDate: __RELEASE_DATE__ + # Inno Setup registers the product as _is1; naming it lets winget match the installed app for upgrades. + ProductCode: "{6B0E5C1A-3F7D-4C52-9A1E-2D8F4B7A9C31}_is1" + AppsAndFeaturesEntries: + - DisplayName: Rotula + Publisher: P4 Software + ProductCode: "{6B0E5C1A-3F7D-4C52-9A1E-2D8F4B7A9C31}_is1" + - Architecture: x64 + InstallerType: wix + Scope: machine + InstallerUrl: __MSI_URL__ + InstallerSha256: __MSI_SHA256__ + # A per-machine MSI needs administrator rights; winget asks for them before it runs msiexec silently. + ElevationRequirement: elevationRequired + # The ProductCode changes with every version (MajorUpgrade); the UpgradeCode never does. + ProductCode: "__MSI_PRODUCT_CODE__" + AppsAndFeaturesEntries: + - DisplayName: Rotula + Publisher: P4 Software + ProductCode: "__MSI_PRODUCT_CODE__" + UpgradeCode: "{4B2C6076-5B82-411C-B751-EC4B70BEE9C6}" ManifestType: installer -ManifestVersion: 1.9.0 +ManifestVersion: 1.10.0 diff --git a/packaging/winget/P4Software.Rotula.locale.en-US.yaml b/packaging/winget/P4Software.Rotula.locale.en-US.yaml index 43d4a9e..00cfd39 100644 --- a/packaging/winget/P4Software.Rotula.locale.en-US.yaml +++ b/packaging/winget/P4Software.Rotula.locale.en-US.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.10.0.schema.json PackageIdentifier: P4Software.Rotula PackageVersion: __VERSION__ PackageLocale: en-US @@ -15,7 +15,8 @@ Description: >- Rotula is a free, open source, offline-first label designer. Lay out text, barcodes, QR codes, boxes and images, merge data from CSV or Excel files, preview the label and print ZPL-compatible output to USB, spooler or network (port 9100) printers. It also includes an MCP server for AI assistants and read-only - database sources. It sends no telemetry and connects only to the printers and databases you set up. + database sources. It sends no telemetry and connects only to the printers and databases you set up, plus, only + if you switch it on, one anonymous daily check for a newer version. Moniker: rotula Tags: - label @@ -32,7 +33,9 @@ Agreements: Documentations: - DocumentLabel: MCP server (connect an AI assistant) DocumentUrl: https://github.com/P4Software/Rotula/blob/main/docs/mcp.md + - DocumentLabel: Deployment (silent install, MSI, policies) + DocumentUrl: https://github.com/P4Software/Rotula/blob/main/docs/deployment.md - DocumentLabel: Third-party notices DocumentUrl: https://github.com/P4Software/Rotula/blob/main/THIRD-PARTY-NOTICES.md ManifestType: defaultLocale -ManifestVersion: 1.9.0 +ManifestVersion: 1.10.0 diff --git a/packaging/winget/P4Software.Rotula.locale.es-MX.yaml b/packaging/winget/P4Software.Rotula.locale.es-MX.yaml new file mode 100644 index 0000000..7dae67f --- /dev/null +++ b/packaging/winget/P4Software.Rotula.locale.es-MX.yaml @@ -0,0 +1,35 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.locale.1.10.0.schema.json +# Spanish texts for "winget show P4Software.Rotula" on Spanish systems. Fields not listed here come from the en-US manifest. +PackageIdentifier: P4Software.Rotula +PackageVersion: __VERSION__ +PackageLocale: es-MX +Publisher: P4 Software +PublisherUrl: https://github.com/P4Software +PublisherSupportUrl: https://github.com/P4Software/Rotula/issues +PackageName: Rotula +PackageUrl: https://github.com/P4Software/Rotula +License: MIT +LicenseUrl: https://github.com/P4Software/Rotula/blob/main/LICENSE +Copyright: Copyright (c) P4 Software +ShortDescription: Diseñador de etiquetas para Windows, sin conexión, con salida compatible con ZPL. +Description: >- + Rotula es un diseñador de etiquetas libre, de código abierto y que funciona sin conexión. Coloca textos, códigos de + barras, códigos QR, recuadros e imágenes, combina datos de archivos CSV o Excel, revisa la vista previa e imprime + salida compatible con ZPL en impresoras USB, de cola de Windows o de red (puerto 9100). Incluye además un servidor MCP + para asistentes de IA y orígenes de datos de solo lectura. No envía telemetría y solo se conecta a las impresoras y bases + de datos que configures y, únicamente si lo activas, a una comprobación anónima diaria de nuevas versiones. +Tags: + - etiquetas + - diseñador-de-etiquetas + - codigo-de-barras + - zpl + - impresion +ReleaseNotesUrl: __RELEASE_NOTES_URL__ +Agreements: + - AgreementLabel: Licencia de Rotula y condiciones de terceros (en inglés) + AgreementUrl: __LICENSE_THIRD_PARTY_URL__ +Documentations: + - DocumentLabel: Instalación en empresas (instalación silenciosa, MSI, directivas) + DocumentUrl: https://github.com/P4Software/Rotula/blob/main/docs/deployment.md +ManifestType: locale +ManifestVersion: 1.10.0 diff --git a/packaging/winget/P4Software.Rotula.yaml b/packaging/winget/P4Software.Rotula.yaml index d7a5ace..7624483 100644 --- a/packaging/winget/P4Software.Rotula.yaml +++ b/packaging/winget/P4Software.Rotula.yaml @@ -1,7 +1,7 @@ -# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json -# Double-underscore placeholders are filled by scripts/New-WingetManifest.ps1 (run by the release workflow). +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.10.0.schema.json +# Double-underscore placeholders are filled by scripts/New-WingetManifest.ps1 (run by packaging/installer/build-installer.ps1). PackageIdentifier: P4Software.Rotula PackageVersion: __VERSION__ DefaultLocale: en-US ManifestType: version -ManifestVersion: 1.9.0 +ManifestVersion: 1.10.0 diff --git a/scripts/New-WingetManifest.ps1 b/scripts/New-WingetManifest.ps1 index fc05fb2..d141528 100644 --- a/scripts/New-WingetManifest.ps1 +++ b/scripts/New-WingetManifest.ps1 @@ -2,36 +2,60 @@ .SYNOPSIS Fills the winget manifest templates in packaging/winget for one release. .DESCRIPTION - Replaces the __PLACEHOLDER__ tokens (version, installer URL, SHA-256, release date, notes URL, third-party licence URL) - and writes the ready-to-submit manifest set to -OutputDir/manifests/p/P4Software/Rotula//, - the folder layout the winget-pkgs repository expects. + Replaces the __PLACEHOLDER__ tokens (version, installer URLs, SHA-256 hashes, the MSI's ProductCode, release date, notes URL, + third-party licence URL) and writes the ready-to-submit manifest set to -OutputDir/manifests/p/P4Software/Rotula//, + the folder layout the winget-pkgs repository expects. The ProductCode is read from the MSI itself (Windows only). .EXAMPLE - ./scripts/New-WingetManifest.ps1 -Version 0.1.0 -InstallerPath artifacts/Rotula-0.1.0-win-x64-setup.exe -OutputDir artifacts/winget + ./scripts/New-WingetManifest.ps1 -Version 0.4.0 -InstallerPath artifacts/Rotula-0.4.0-win-x64-setup.exe -MsiPath artifacts/Rotula-0.4.0-win-x64.msi -OutputDir artifacts/winget #> param( [Parameter(Mandatory)] [string] $Version, [Parameter(Mandatory)] [string] $InstallerPath, + [Parameter(Mandatory)] [string] $MsiPath, [Parameter(Mandatory)] [string] $OutputDir, [string] $Repository = 'P4Software/Rotula' ) $ErrorActionPreference = 'Stop' + +function Get-MsiProperty([string] $path, [string] $name) { + # Reads one row of the MSI's Property table through the Windows Installer automation interface (works in PowerShell 5.1 and 7). + $installer = New-Object -ComObject WindowsInstaller.Installer + try { + $db = $installer.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $null, $installer, @((Resolve-Path -LiteralPath $path).Path, 0)) + $view = $db.GetType().InvokeMember('OpenView', 'InvokeMethod', $null, $db, @("SELECT ``Value`` FROM ``Property`` WHERE ``Property`` = '$name'")) + [void]$view.GetType().InvokeMember('Execute', 'InvokeMethod', $null, $view, $null) + $record = $view.GetType().InvokeMember('Fetch', 'InvokeMethod', $null, $view, $null) + if (-not $record) { throw "The MSI $path has no $name." } + $value = $record.GetType().InvokeMember('StringData', 'GetProperty', $null, $record, 1) + [void]$view.GetType().InvokeMember('Close', 'InvokeMethod', $null, $view, $null) + return $value + } finally { + [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($installer) + } +} + $Version = $Version.TrimStart('v') $tag = "v$Version" -$name = Split-Path -Leaf $InstallerPath -$sha = (Get-FileHash -Algorithm SHA256 -LiteralPath $InstallerPath).Hash.ToUpperInvariant() +$base = "https://github.com/$Repository/releases/download/$tag" +$productCode = Get-MsiProperty $MsiPath 'ProductCode' +if ($productCode -notmatch '^\{[0-9A-Fa-f-]{36}\}$') { throw "The MSI ProductCode '$productCode' does not look like a GUID." } $tokens = @{ '__VERSION__' = $Version - '__INSTALLER_URL__' = "https://github.com/$Repository/releases/download/$tag/$name" - '__INSTALLER_SHA256__' = $sha + '__INSTALLER_URL__' = "$base/$(Split-Path -Leaf $InstallerPath)" + '__INSTALLER_SHA256__' = (Get-FileHash -Algorithm SHA256 -LiteralPath $InstallerPath).Hash.ToUpperInvariant() + '__MSI_URL__' = "$base/$(Split-Path -Leaf $MsiPath)" + '__MSI_SHA256__' = (Get-FileHash -Algorithm SHA256 -LiteralPath $MsiPath).Hash.ToUpperInvariant() + '__MSI_PRODUCT_CODE__' = $productCode.ToUpperInvariant() '__RELEASE_DATE__' = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd') '__RELEASE_NOTES_URL__' = "https://github.com/$Repository/releases/tag/$tag" - '__LICENSE_THIRD_PARTY_URL__' = "https://github.com/$Repository/releases/download/$tag/LICENSE-THIRD-PARTY.txt" + '__LICENSE_THIRD_PARTY_URL__' = "$base/LICENSE-THIRD-PARTY.txt" } $dest = Join-Path $OutputDir "manifests/p/P4Software/Rotula/$Version" New-Item -ItemType Directory -Force -Path $dest | Out-Null $templates = Join-Path $PSScriptRoot '../packaging/winget' foreach ($file in Get-ChildItem $templates -Filter 'P4Software.Rotula*.yaml') { - $text = Get-Content -LiteralPath $file.FullName -Raw + # -Encoding UTF8: the Spanish locale file has accents, and Windows PowerShell 5.1 would read a BOM-less file as ANSI. + $text = Get-Content -LiteralPath $file.FullName -Raw -Encoding UTF8 foreach ($key in $tokens.Keys) { $text = $text.Replace($key, $tokens[$key]) } if ($text -match '__[A-Z_]+__') { throw "Unfilled placeholder in $($file.Name): $($Matches[0])" } # winget-pkgs expects UTF-8 without a BOM; write through .NET so Windows PowerShell 5 and pwsh 7 behave the same. diff --git a/src/Rotula.App/App.xaml.cs b/src/Rotula.App/App.xaml.cs index 680cab8..3fe8304 100644 --- a/src/Rotula.App/App.xaml.cs +++ b/src/Rotula.App/App.xaml.cs @@ -27,10 +27,20 @@ protected override void OnStartup(StartupEventArgs e) Editing.PreviewCache.RenderFailed += LogCrash; AppDomain.CurrentDomain.UnhandledException += (_, args) => LogCrash(args.ExceptionObject as Exception); + var launch = StationLaunch.Parse(e.Args); + if (launch.Mode != LaunchMode.Designer) + { + StartStation(launch); + return; + } + try { + // What the organization fixed through Group Policy or Intune (HKLM wins over HKCU; docs/deployment.md). Read once. + var policy = Rotula.Files.Policy.RotulaPolicy.Read(Rotula.Files.Policy.RegistryPolicySource.ForThisComputer()); // Add-ons load on a background thread while the window opens; a failure of the loader itself only ends up in the log. - var packOptions = new Rotula.Data.Packs.PackLoadOptions(); + // A policy can keep add-ons in the user's own folder from loading (the installed "packs" folder is still used). + var packOptions = new Rotula.Data.Packs.PackLoadOptions { IncludeUserPacks = policy.AllowUserAddOns }; var packLoading = PackStartup.Begin(packOptions, LogCrash); // The first New label dialog should find the stock library already read. StockLibrary.Preload(); @@ -40,13 +50,18 @@ protected override void OnStartup(StartupEventArgs e) new WpfNewLabelDialogService(), CreateZplImporter(), new Rotula.Printing.PrintService(), new JsonSettingsStore(), dialogs, new DatabaseServices(), // Serial numbers that remember their value live in %AppData%\Rotula\counters.json (see docs/variables.md). // Windows-font text is drawn to a picture by the SkiaSharp rasterizer, so Print, Save ZPL, Check All and Export can all print it. - new VariableServices(Rotula.Files.FileCounterStore.OpenDefault(), dialogs, textRasterizer: new Rotula.Rendering.SkiaTextRasterizer())); + new VariableServices(Rotula.Files.FileCounterStore.OpenDefault(), dialogs, textRasterizer: new Rotula.Rendering.SkiaTextRasterizer()), + Policy: policy, + // The opt-in update check: nothing is sent unless the user (Help menu) or a policy switches it on. + Updates: new UpdateChecker(new GitHubReleaseFeed(), Views.AboutWindow.VersionString()), + Links: new ShellUrlLauncher()); var vm = new MainViewModel(services); var window = new MainWindow(vm); MainWindow = window; window.Show(); // When loading is over, tell the user (non-modally) about add-ons that could not be loaded or wait for approval. _ = ShowPackNoticeAsync(packLoading, packOptions, vm); + _ = CheckForUpdatesAsync(vm); // Double-clicking a .rotula file starts the program with the path as its only argument. if (e.Args.Length > 0 && File.Exists(e.Args[0])) vm.LoadFile(e.Args[0]); @@ -59,6 +74,47 @@ protected override void OnStartup(StartupEventArgs e) } } + /// + /// Starts the print station (or its PIN tool) instead of the designer. The station uses the same printing service, counter memory and saved + /// language as the designer; a configuration that cannot be used is explained and the program ends. + /// + private void StartStation(StationLaunch launch) + { + try + { + var settings = new JsonSettingsStore().Load(); + Loc.Instance.SetLanguage(settings.Language); + if (launch.Mode == LaunchMode.StationPin) + { + var pinWindow = new Views.StationPinWindow(new StationPinViewModel(new WpfClipboardService())) { ShowInTaskbar = true }; + MainWindow = pinWindow; + pinWindow.Show(); + return; + } + var load = Rotula.Files.Station.StationConfigLoader.Load(launch.Path); + if (!load.CanOpen) + { + ShowCrashMessage(Loc.Get("Station.Open.Failed") + Environment.NewLine + Environment.NewLine + StationViewModel.DescribeIssues(load.Issues)); + Shutdown(2); + return; + } + // Labels whose data comes from an add-on database need the add-ons; they load in the background as in the designer. + PackStartup.Begin(new Rotula.Data.Packs.PackLoadOptions(), LogCrash); + var variables = new VariableServices(Rotula.Files.FileCounterStore.OpenDefault(), null, textRasterizer: new Rotula.Rendering.SkiaTextRasterizer()); + var station = new StationViewModel(load.Config!, load.Warnings, + new StationServices(new Rotula.Printing.PrintService(), variables, new WpfMessageService(), settings.Print, new DatabaseServices())); + var window = new Views.StationWindow(station); + MainWindow = window; + window.Show(); + } + catch (Exception ex) + { + LogCrash(ex); + ShowCrashMessage(Loc.Get("Crash.Startup", LogPath)); + Shutdown(1); + } + } + private async Task ShowPackNoticeAsync(Task loading, Rotula.Data.Packs.PackLoadOptions options, MainViewModel vm) { try @@ -72,6 +128,19 @@ private async Task ShowPackNoticeAsync(Task lo } } + private static async Task CheckForUpdatesAsync(MainViewModel vm) + { + try + { + await vm.CheckForUpdatesAsync(); + } + catch (Exception ex) + { + // The update check is a convenience; it must never disturb the user. + LogCrash(ex); + } + } + /// The ZPL importer: the parser that ships in Rotula.Core. private static IZplImporter CreateZplImporter() => new CoreZplImporter(); diff --git a/src/Rotula.App/AssemblyInfo.cs b/src/Rotula.App/AssemblyInfo.cs index 379c50e..640412f 100644 --- a/src/Rotula.App/AssemblyInfo.cs +++ b/src/Rotula.App/AssemblyInfo.cs @@ -9,7 +9,7 @@ // app, or any theme specific resource dictionaries) )] -// The UI text lives in satellite assemblies (Strings.en.resx / Strings.es.resx) and Loc always asks for "en" or "es" -// explicitly, so no neutral-language attribute is used: NeutralResourcesLanguage(..., Satellite) would also redirect WPF's +// The UI text lives in satellite assemblies (Strings.en / es / pt-BR / fr-CA .resx) and Loc always asks for one of those +// cultures explicitly, so no neutral-language attribute is used: NeutralResourcesLanguage(..., Satellite) would also redirect WPF's // own XAML resource lookup to a satellite and the application would fail to start. [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Rotula.Tests")] diff --git a/src/Rotula.App/Editing/PreviewCache.cs b/src/Rotula.App/Editing/PreviewCache.cs index f3d5e8d..19acf5d 100644 --- a/src/Rotula.App/Editing/PreviewCache.cs +++ b/src/Rotula.App/Editing/PreviewCache.cs @@ -27,6 +27,9 @@ public sealed class PreviewCache : IDisposable /// Creates a cache for a session. public PreviewCache(EditorSession session) => _session = session; + /// The label this cache draws (the session's current document). + public Core.Model.LabelDocument Document => _session.Document; + /// /// Values of the data-merge record shown on the canvas, or null to draw the sample values. Changing it must be followed /// by so the cached render is rebuilt. @@ -70,10 +73,16 @@ public RenderLabel GetDesignLabel() /// Draws the label exactly as it prints (objects that are not printed for the shown record are left out). The caller owns and must dispose the /// result. Returns null and sets when the label cannot be drawn. /// - public RenderResult? RenderPrinted(double scale, out string? errorKey) + /// Output pixels per printer dot. + /// Message key of the reason the label could not be drawn, or null. + /// + /// True to turn (^POI) or mirror (^PMY) the picture the way the printer puts it on the media; false shows it as designed. + /// + public RenderResult? RenderPrinted(double scale, out string? errorKey, bool asPrinted = false) { errorKey = null; var label = GetLabel(); + if (asPrinted) label = LabelDocumentAdapter.AsPrinted(label, _session.Document); if (label.WidthDots <= 0 || label.HeightDots <= 0) { errorKey = "Preview.InvalidSize"; return null; } try { return LabelRenderer.RenderBitmap(label, new RenderOptions { Scale = scale }); } catch (ArgumentOutOfRangeException) { errorKey = "Preview.TooLarge"; return null; } diff --git a/src/Rotula.App/Localization/Loc.cs b/src/Rotula.App/Localization/Loc.cs index e35590b..26953db 100644 --- a/src/Rotula.App/Localization/Loc.cs +++ b/src/Rotula.App/Localization/Loc.cs @@ -6,14 +6,18 @@ namespace Rotula.App.Localization; /// -/// The application's translator. Every piece of UI text comes from Resources/Strings.en.resx or Strings.es.resx through -/// this class. XAML binds to the indexer ({loc:T Key}), so switching the language updates the open windows at once +/// The application's translator. Every piece of UI text comes from one of the Resources/Strings.<language>.resx files +/// (en, es, pt-BR, fr-CA) through this class. XAML binds to the indexer ({loc:T Key}), so switching the language updates the open windows at once /// (the "live" option, see docs/decisions.md). /// public sealed class Loc : INotifyPropertyChanged { - /// Language codes the application ships. - public static readonly string[] SupportedLanguages = { "en", "es" }; + /// + /// Culture names of the languages the application ships, English (the reference) first. Each has a + /// Resources/Strings.<name>.resx file. Full names, not two-letter codes: the Portuguese is Brazilian and the French is + /// Canadian, and their number and date formats follow those regions. + /// + public static readonly string[] SupportedLanguages = { "en", "es", "pt-BR", "fr-CA" }; private readonly ResourceManager _resources = new("Rotula.App.Resources.Strings", typeof(Loc).Assembly); private CultureInfo _culture = CultureInfo.GetCultureInfo("en"); @@ -24,11 +28,11 @@ public sealed class Loc : INotifyPropertyChanged /// public event PropertyChangedEventHandler? PropertyChanged; - /// The language currently shown (always "en" or "es"). + /// The language currently shown (always one of ). public CultureInfo Culture => _culture; - /// Two-letter code of the language currently shown. - public string LanguageCode => _culture.TwoLetterISOLanguageName; + /// Culture name of the language currently shown, as listed in ("en", "pt-BR"...). + public string LanguageCode => _culture.Name; /// /// Translates a key. A key that is missing in the current language falls back to English; a key missing everywhere is @@ -66,14 +70,24 @@ private string Lookup(string key) => } /// - /// Picks the language: "en" or "es" explicitly, or (for "auto" or anything unknown) the Windows display language - /// when it is Spanish, English otherwise. + /// Picks the language: one of when the setting names it, or (for "auto" or anything + /// unknown) the closest match to the Windows display language: any Spanish is "es", any Portuguese "pt-BR", any French + /// "fr-CA", and everything else English. /// public static string ResolveLanguage(string? setting, CultureInfo? windowsUiCulture = null) { - if (setting is "en" or "es") return setting; + // Settings saved before pt-BR and fr-CA existed hold "en", "es" or "auto"; they keep meaning the same thing. + var chosen = SupportedLanguages.FirstOrDefault(l => string.Equals(l, setting, StringComparison.OrdinalIgnoreCase)); + if (chosen is not null) return chosen; var ui = windowsUiCulture ?? CultureInfo.CurrentUICulture; - return ui.TwoLetterISOLanguageName.Equals("es", StringComparison.OrdinalIgnoreCase) ? "es" : "en"; + // One translation per language: a pt-PT or fr-FR desktop is far better served by pt-BR or fr-CA than by English. + return ui.TwoLetterISOLanguageName.ToLowerInvariant() switch + { + "es" => "es", + "pt" => "pt-BR", + "fr" => "fr-CA", + _ => "en", + }; } /// Switches the language and tells every binding to refresh. diff --git a/src/Rotula.App/MainWindow.xaml b/src/Rotula.App/MainWindow.xaml index 02c60f5..2620f11 100644 --- a/src/Rotula.App/MainWindow.xaml +++ b/src/Rotula.App/MainWindow.xaml @@ -170,10 +170,14 @@ + + + + @@ -212,6 +216,11 @@ + + + + + @@ -249,6 +258,11 @@ + + + + @@ -343,8 +357,25 @@ - - + + + + + + + + + + @@ -357,6 +388,7 @@ + @@ -720,6 +752,19 @@ + + + + + + + + diff --git a/src/Rotula.App/Resources/Strings.en.resx b/src/Rotula.App/Resources/Strings.en.resx index 6e0d2c7..cc6b2d0 100644 --- a/src/Rotula.App/Resources/Strings.en.resx +++ b/src/Rotula.App/Resources/Strings.en.resx @@ -1325,7 +1325,7 @@ The printer at {0}:{1} did not answer in time. Check that it is switched on and on the same network, then try again. - The device at {0}:{1} refused the connection. Check the port (Zebra printers use 9100) and that the printer's network port is enabled. + The device at {0}:{1} refused the connection. Check the port (most label printers use 9100) and that the printer's network port is enabled. Cannot reach {0}:{1}. Check the address, the network cable or Wi-Fi, and that the printer is switched on. @@ -1484,7 +1484,7 @@ Enter the host name or IP address of the network printer, for example 192.168.1.50. - The port must be a whole number from 1 to 65535 (Zebra printers use 9100). + The port must be a whole number from 1 to 65535 (most label printers use 9100). {0} must be a number. Enter digits only, for example 12. @@ -2249,7 +2249,7 @@ Welcome to Rotula - Design a label, check it on screen and print it on your Zebra printer. + Design a label, check it on screen and print it on your ZPL-compatible printer. New label @@ -5846,4 +5846,842 @@ Brand and product names belong to their owners; Rotula is not affiliated with th Locked: this object cannot be moved, resized or deleted until you unlock it. + + Nothing was sent: the printer "{0}" reports that its printhead is open. Close the head until it clicks, then print again. + + + Nothing was sent: the printer "{0}" reports that it is out of labels. Load labels, close the head, press Feed once, then print again. + + + Nothing was sent: the printer "{0}" reports that the ribbon is out. Load a new ribbon, close the head, then print again. + + + Nothing was sent: the printer "{0}" reports that its printhead is too hot. Wait a few minutes for it to cool down, then print again. + + + Nothing was sent: the printer "{0}" is in communications diagnostics mode and would print the data as text instead of labels. Switch the mode off on the printer (or restart it), then print again. + + + Nothing was sent: the printer "{0}" reports that its memory for incoming jobs is full. Wait until it has printed what it already has, then print again. + + + Nothing was sent: the printer "{0}" is paused. Press Pause on the printer to resume, then print again. + + + The printer also reports: {0}. + + + The printer status could not be read before printing ({0}), so the labels were sent without that check. + + + The printer reports: {0}. The labels were sent; check the print quality. + + + Printed and verified: the printer reports it finished the job without problems. + + + The labels were sent, but then the printer reported a problem: {0}. Some labels may not have printed. Fix the printer and check the labels before printing again. + + + The labels were sent, but Rotula could not confirm that they were printed ({0}). Check the printer. + + + this printer or its Windows driver does not answer status requests, which is common with USB drivers + + + Windows reports the printer as offline + + + the printer could not be reached + + + the printer did not answer in time + + + the printer's answer was not a ZPL status reply + + + it was still printing when Rotula stopped waiting + + + the check was stopped + + + the reason is unknown + + + printhead open + + + out of labels + + + ribbon out + + + printhead too hot + + + communications diagnostics mode on + + + memory for incoming jobs full + + + paused + + + printhead too cold (labels may print lighter) + + + settings lost (the printer is using its defaults) + + + a job is still arriving + + + a printed label is waiting to be taken + + + unknown + + + rewind + + + peel-off + + + tear-off + + + cutter + + + applicator + + + delayed cut + + + linerless peel + + + linerless rewind + + + partial cutter + + + RFID + + + kiosk + + + stream + + + Check printer status + + + Asks the printer for its status (paper, head, ribbon, pause) with a small ZPL command. Nothing is printed on a ZPL compatible printer. + + + Asking the printer for its status... + + + The printer is ready. + + + The printer cannot print right now: {0}. + + + The printer reports: {0}. + + + The printer status could not be read: {0}. You can still print. + + + Only part of the status was received, so some problems may not be listed. + + + Labels still to print: {0}. Jobs waiting in the printer: {1}. + + + Print mode: {0}. {1}. Label length: {2} dots. + + + Thermal transfer (with ribbon) + + + Direct thermal (no ribbon) + + + Check the printer before and after printing + + + Before each job Rotula asks the printer for its status and does not send while it is out of labels, open, paused or out of ribbon. After the job it waits until the printer reports it finished. For ZPL compatible printers; many USB drivers cannot answer, and then printing goes ahead without the check. + + + Open print station... + + + Create print station PIN... + + + Print station + + + Choose the folder of the print station (with its station.json, or with the labels to offer) + + + The print station cannot open. + + + Exit + + + All labels + + + Search or scan a label + + + Type a name or scan the label's code, then press Enter. Or tap a label below. + + + No label matches the search. + + + No label has the code "{0}". Scan it again or choose a label from the list. + + + Some labels are not available: + + + The label "{0}" cannot be used: {1} + + + Printer: {0} + + + No printer is set up for this label. Ask the person who set up the station to add one to station.json. + + + Quantity + + + 1 to {0} labels + + + Enter a quantity from 1 to {0}, then press Print again. + + + Print + + + Print all records + + + {0} records from the label's data are ready to print. + + + The label's data could not be read: {0} + + + the data has no records. + + + This label asks no questions. Set the quantity and press Print. + + + Some answers are not accepted. Correct the fields marked in red, then print again. + + + Preview + + + Checking the printer... + + + Printer ready + + + Printer status unknown + + + The printer needs attention + + + Printing... + + + Rotula checks the printer before sending and waits until it reports that it finished. + + + Not printed: the printer needs attention + + + Sent to the printer + + + Printed and verified + + + Sent, but not confirmed + + + Sent, but the printer reports a problem + + + Not printed + + + Enter the PIN to leave the print station + + + PIN + + + Leave the station + + + That PIN is not right. Try again. + + + Too many wrong PINs. Wait {0} seconds, then try again. + + + Create a print station PIN + + + The PIN is needed to leave the print station. Rotula keeps only a protected code made from it, never the PIN itself. Copy the line into the station's station.json. + + + New PIN + + + Type the PIN again + + + Create + + + Line for station.json + + + Copy + + + Copied. Paste it into station.json, next to the other settings. + + + A PIN needs {0} to {1} characters. + + + The two PINs are not the same. Type them again. + + + "{0}" was not found. Check the path given to the print station. + + + "{0}" could not be read. Check that the file is not locked and that this user may read it. + + + "{0}" has a mistake near line {1} ({2}): a setting is misspelt, has a value of the wrong kind, or a comma or quote is missing. + + + The station settings were written for a newer version of Rotula (format {0}). Update Rotula on this PC. + + + There is no label to offer. Put .rotula files in the station's folder or list them under "labels" in station.json. + + + The label "{0}" listed in station.json was not found and is left out. + + + "{0}" is not a Rotula label (.rotula file). Correct it in station.json. + + + Label entry {0} in station.json has no "file". Add the label's file name. + + + Two labels use the code "{0}". Give every label its own code, so a scan opens the right one. + + + The printer "{0}" is not defined under "printers" in station.json. + + + The printer "{0}" needs either "windowsPrinter" (an installed printer) or "host" (a network address), not both. + + + The port of the printer "{0}" must be from 1 to 65535 (usually 9100). + + + The resolution of the printer "{0}" must be 203, 300 or 600. + + + The printer "{0}" has a darkness outside 0 to 30 or a speed outside 1 to 14. + + + The station's copies settings are not valid: "maxCopies" must be from 1 to 99999999 and "defaultCopies" from 1 to "maxCopies". + + + The copies settings of the label "{0}" are not valid: "maxCopies" must be from 1 to 99999999 and "defaultCopies" from 1 to "maxCopies". + + + "exitPinHash" is not a PIN code made by Rotula. Create it with File > Create print station PIN (a PIN is never stored as plain text). + + + + Managed by your organization + + + Check for updates once a day + + + When on, Rotula asks the project page on GitHub once a day whether a newer version exists. Nothing about you or this computer is sent, and nothing is downloaded or installed. + + + Rotula {0} is available. You can download it from the release page. + + + Update available + + + View release + + + Close this notice + + + The browser could not be opened. The release page is {0} + + + Show the label as printed or as designed + + + As printed + + + Show the label the way it comes out of the printer, turned or mirrored by the label's print settings. + + + As designed + + + Show the label upright, the way you edit it on the canvas. + + + Shown as printed: this label's print orientation is set to inverted, so the printer prints it upside down. + + + Shown as printed: this label is set to print as a mirror image, so left and right are swapped. + + + Shown as printed: this label is set to print upside down and as a mirror image. + + + Shown as designed, the way you edit it. Choose "As printed" to see how it comes out of the printer. + + + Prints upside down + + + Prints mirrored + + + Prints upside down, mirrored + + + Print orientation + + + The canvas shows the label upright so you can edit it. Open the preview to see it as printed. Change this in the label properties. + + + Label sheets (PDF)... + + + Label sheets (PDF)... + + + Put the labels on sheets for an office (laser or inkjet) printer and save them as a PDF. + + + Label sheets + + + Print labels on sheets with an office (laser or inkjet) printer. Rotula saves the sheets as a PDF at their real size; print it at actual size (100 %). + + + Data is loaded: the ticked records go on the sheets, with their copies and serial numbers, as when printing. + + + Sheet + + + Sheets of different suppliers with the same label size can differ by a millimetre. Print one page on plain paper, hold it against a sheet of labels and adjust the margins if needed. + + + Custom layout + + + {0}, {1} per sheet ({2} x {3}), {4} + + + {0} x {1} in + + + {0} x {1} mm + + + Paper + + + Sizes in inches + + + All sizes in millimetres + + + All sizes in inches + + + Paper width + + + Paper height + + + Label width + + + Label height + + + Labels across + + + Labels down + + + Top margin + + + Left margin + + + Across: edge to edge + + + Down: edge to edge + + + The distance from the left (or top) edge of one label to the same edge of the next one: the label size plus the gap between labels. + + + Start at position + + + On a partly used sheet, the first free position. Positions are counted along the rows from the top left. You can also click a position in the preview. + + + Fill order + + + Guides + + + Print the PDF at actual size (100 %), without "Fit to page", or the labels will not line up with the sheet. + + + Sheet preview + + + Click a position to start there. Grey positions are already used; numbers show where the first labels go. + + + Position {0}: already used + + + Position {0}: label {1} + + + Position {0}: empty + + + Save as PDF... + + + Labels: {0}. Sheets: {1} ({2} per sheet). + + + {0} per sheet. The copies come from a column of the data, so the number of sheets is known when the PDF is made. + + + {0}: type a number. + + + Copies: type a whole number from 1 to {0}. + + + Making the sheets... + + + Saved {2} (labels: {0}, sheets: {1}). Print it at actual size (100 %). + + + That is more than {0} labels, too many for one PDF. Tick fewer records or lower the copies, and save the rest in another file. + + + Letter (8.5 x 11 in) + + + A4 (210 x 297 mm) + + + Legal (8.5 x 14 in) + + + Custom size + + + Along the rows, then down + + + Down the columns, then across + + + None + + + Crop marks in the margins + + + Outline of each label (test on plain paper) + + + The paper must be between {0} and {1} mm on each side. + + + The labels must be at least {0} mm wide and high. + + + A sheet can have 1 to {0} labels across and 1 to {1} down. + + + The margins cannot be negative. + + + The labels overlap across: from one label edge to the next is {0} mm, but a label is {1} mm wide. Make the distance at least the label width. + + + The labels overlap down: from one label edge to the next is {0} mm, but a label is {1} mm high. Make the distance at least the label height. + + + The labels do not fit across the paper: the last column ends at {0} mm, the paper is {1} mm wide. Reduce the left margin, the distance or the number of labels across. + + + The labels do not fit down the paper: the last row ends at {0} mm, the paper is {1} mm high. Reduce the top margin, the distance or the number of labels down. + + + Some labels come within {0} mm of the paper edge. Most office printers cannot print there, so keep text and barcodes away from those label edges. + + + The start position must be a number from 1 to {0}. + + + The label design is {0} x {1} mm, larger than the {2} x {3} mm labels of this sheet. It is printed at its own size, centred, and cut at the label edges. Choose a sheet with larger labels or resize the design. + + + The label design is {0} x {1} mm, smaller than the {2} x {3} mm labels of this sheet. It is printed at its own size, centred on each label. + + + {0} needs NAME=VALUE, but got "{1}". + + + "{1}" is not a valid value for {0}. + + + rotula {0}: prints Rotula labels without the designer, for scripts, schedulers and warehouse systems. + +Commands: + rotula print LABEL.rotula --printer NAME | --host ADDRESS [options] + Prints a label. Options: + --port N TCP port of --host (default 9100) + --copies N copies of the label, or of every record + --data FILE records from a .csv, .txt, .xlsx or .json file (one label per record) + --sheet NAME the Excel sheet to read + --set NAME=VALUE a field value (repeat for more fields) + --answer NAME=VALUE an answer to a question the label asks at print time + --dpi 203|300|600 printhead resolution (default: what the Windows driver reports) + --darkness 0-30 darkness sent before the labels + --speed 1-14 print speed sent before the labels + --no-status do not ask the printer for its status before every job + --no-verify do not wait for the printer to report that it finished + --dry-run show the ZPL without printing (--output FILE writes it to a file) + --reference TEXT your own reference, repeated in the result + --counter-file FILE where serial numbers are remembered (default: the same file as the app) + rotula status --printer NAME | --host ADDRESS [--port N] + Asks the printer whether it can print right now. + rotula printers + Lists the Windows printers. + rotula serve [--settings FILE] [--urls URL] + Runs the REST API. "rotula serve --init [--label-folder FOLDER]" writes a starter settings file with a new key. + rotula new-key + Makes a new API key and the hash to put in the settings file. + +Every command takes --json (output for programs) and --lang en|es|pt-BR|fr-CA. +Exit codes: 0 done, 1 printing failed, 2 wrong command line, 3 label errors, 4 printer not ready, 5 blocked by policy, 6 cancelled. + + + Created the settings file {0}. Add your printers to allowedPrinters, then run "rotula serve". + + + The settings file {0} already exists; it was not changed. + + + The option {0} needs a value. + + + New API key (shown only now; give it to the system that calls the server): {0} +Hash to add to apiKeyHashes in the settings file: {1} + + + Name exactly one label file, for example: rotula print shipping.rotula --printer "Dock 1" + + + Record {0}: + + + Run "rotula help" for the commands and options. + + + Unknown command "{0}". + + + Unknown option {0}. + + + Warning: + + + Some answers to the label's questions are missing or not valid. The label asks: {0}. Questions marked * must be answered; the others take their default. + + + A valid API key is required. Send it in the X-Api-Key header. + + + The request could not be read: {0} + + + Copies must be a whole number from 1 to {0}. + + + The data file {0} could not be read: {1} + + + The data file {0} does not exist.{1} + + + The data in {0} must be a list of records (objects of field names and values) or one record. {1} + + + The resolution must be 203, 300 or 600 DPI. + + + This server does not accept labels sent as content (allowInlineLabels is off). Name a label file in one of its label folders. + + + Send either a label file name (label) or the label content (labelContent), not both. + + + The label {0} was not found. + + + The label {0} is not a .rotula file inside one of this server's label folders. Name it relative to a label folder, without a drive letter or "..". + + + Name a label file (label) or send the label content (labelContent). + + + The label {0} is larger than the limit of {1} KB. + + + The data has no records, so nothing was printed. + + + No record produced a label, so nothing was sent. See the problems listed with the result. + + + The file {0} could not be written: {1} + + + Name either a Windows printer or the address of a network printer, not both. + + + The printer {0} is not on this server's list of allowed printers (allowedPrinters). Nothing was sent. + + + Name a printer: a Windows printer (printer) or the address of a network printer (host). + + + Printing with the print engine is switched off by your organization's policy for this computer. Ask your IT department. + + + A request may have at most {0} records. + + + Send either the values of one label or a list of records, not both. + + + [default] + + + No printers are installed in Windows. + + + [offline] + + + Nothing was printed (dry run). This is the ZPL that would be sent to {0}. + + + Sent {0} label(s) to {1}. + + + Sent {0} record(s), {1} label(s), to {2} in {3} job(s). + + + The server could not listen on {0}: {1} + + + Rotula print engine listening on {0}. Settings: {1}. Press Ctrl+C to stop. + + + "{0}" in apiKeyHashes is not a key hash (64 hexadecimal characters). Make one with "rotula new-key". + + + The label folder {0} does not exist. + + + The settings file {0} cannot be used: {1} + + + The settings file {0} does not exist. Create one with "rotula serve --init". + + + No label folders are listed (labelFolders), and labels sent as content are switched off, so no label can be printed. + + + The settings file {0} lists no API key (apiKeyHashes). Make one with "rotula new-key". + + + No printers are allowed yet (allowedPrinters), so every print request will be refused. + + + The server accepts calls from other computers over plain http: API keys and label data cross the network unencrypted. Use https. + diff --git a/src/Rotula.App/Resources/Strings.es.resx b/src/Rotula.App/Resources/Strings.es.resx index 653f841..1139cb1 100644 --- a/src/Rotula.App/Resources/Strings.es.resx +++ b/src/Rotula.App/Resources/Strings.es.resx @@ -734,7 +734,7 @@ Mostrar cuadrícula - _Ayuda + Ay_uda Importar ZPL... @@ -1325,7 +1325,7 @@ La impresora en {0}:{1} no respondió a tiempo. Compruebe que esté encendida y en la misma red; luego inténtelo de nuevo. - El dispositivo en {0}:{1} rechazó la conexión. Compruebe el puerto (las Zebra usan 9100) y que el puerto de red de la impresora esté activado. + El dispositivo en {0}:{1} rechazó la conexión. Compruebe el puerto (la mayoría de las impresoras de etiquetas usan el 9100) y que el puerto de red de la impresora esté activado. No se puede llegar a {0}:{1}. Revise la dirección, el cable de red o el Wi-Fi, y que la impresora esté encendida. @@ -1484,7 +1484,7 @@ Indique el nombre de host o la dirección IP de la impresora de red, por ejemplo 192.168.1.50. - El puerto debe ser un número entero de 1 a 65535 (las Zebra usan 9100). + El puerto debe ser un número entero de 1 a 65535 (la mayoría de las impresoras de etiquetas usan el 9100). {0} debe ser un número. Escriba solo dígitos, por ejemplo 12. @@ -2249,7 +2249,7 @@ Bienvenido a Rotula - Diseñe una etiqueta, revísela en pantalla e imprímala en su impresora Zebra. + Diseñe una etiqueta, revísela en pantalla e imprímala en su impresora compatible con ZPL. Nueva etiqueta @@ -5846,4 +5846,842 @@ Las marcas y los nombres de producto pertenecen a sus dueños; Rotula no está a Bloqueado: este objeto no se puede mover, cambiar de tamaño ni borrar hasta que lo desbloquee. + + No se envió nada: la impresora "{0}" indica que el cabezal está abierto. Cierre el cabezal hasta que haga clic y vuelva a imprimir. + + + No se envió nada: la impresora "{0}" indica que no tiene etiquetas. Cargue etiquetas, cierre el cabezal, pulse Avance una vez y vuelva a imprimir. + + + No se envió nada: la impresora "{0}" indica que se acabó la cinta. Coloque una cinta nueva, cierre el cabezal y vuelva a imprimir. + + + No se envió nada: la impresora "{0}" indica que el cabezal está demasiado caliente. Espere unos minutos a que se enfríe y vuelva a imprimir. + + + No se envió nada: la impresora "{0}" está en modo de diagnóstico de comunicaciones e imprimiría los datos como texto en lugar de etiquetas. Desactive ese modo en la impresora (o reiníciela) y vuelva a imprimir. + + + No se envió nada: la impresora "{0}" indica que su memoria para trabajos entrantes está llena. Espere a que imprima lo que ya tiene y vuelva a imprimir. + + + No se envió nada: la impresora "{0}" está en pausa. Pulse Pausa en la impresora para reanudar y vuelva a imprimir. + + + La impresora también indica: {0}. + + + No se pudo leer el estado de la impresora antes de imprimir ({0}), así que las etiquetas se enviaron sin esa comprobación. + + + La impresora indica: {0}. Las etiquetas se enviaron; revise la calidad de impresión. + + + Impreso y verificado: la impresora indica que terminó el trabajo sin problemas. + + + Las etiquetas se enviaron, pero luego la impresora indicó un problema: {0}. Puede que algunas etiquetas no se hayan impreso. Solucione el problema y revise las etiquetas antes de volver a imprimir. + + + Las etiquetas se enviaron, pero Rotula no pudo confirmar que se imprimieran ({0}). Revise la impresora. + + + esta impresora o su controlador de Windows no responde a consultas de estado, algo habitual con controladores USB + + + Windows indica que la impresora está fuera de línea + + + no se pudo contactar con la impresora + + + la impresora no respondió a tiempo + + + la respuesta de la impresora no era una respuesta de estado ZPL + + + seguía imprimiendo cuando Rotula dejó de esperar + + + la comprobación se detuvo + + + el motivo es desconocido + + + cabezal abierto + + + sin etiquetas + + + sin cinta + + + cabezal demasiado caliente + + + modo de diagnóstico de comunicaciones activado + + + memoria para trabajos entrantes llena + + + en pausa + + + cabezal demasiado frío (las etiquetas pueden salir más claras) + + + configuración perdida (la impresora usa sus valores de fábrica) + + + todavía está llegando un trabajo + + + una etiqueta impresa espera a que la retiren + + + desconocido + + + rebobinado + + + despegado + + + corte manual + + + cortador + + + aplicador + + + corte diferido + + + despegado sin soporte + + + rebobinado sin soporte + + + cortador parcial + + + RFID + + + quiosco + + + continuo + + + Comprobar estado de la impresora + + + Pregunta a la impresora su estado (papel, cabezal, cinta, pausa) con un pequeño comando ZPL. En una impresora compatible con ZPL no se imprime nada. + + + Consultando el estado de la impresora... + + + La impresora está lista. + + + La impresora no puede imprimir ahora: {0}. + + + La impresora indica: {0}. + + + No se pudo leer el estado de la impresora: {0}. Puede imprimir igualmente. + + + Solo se recibió parte del estado, así que puede que falten problemas en la lista. + + + Etiquetas pendientes: {0}. Trabajos en espera en la impresora: {1}. + + + Modo de impresión: {0}. {1}. Largo de etiqueta: {2} puntos. + + + Transferencia térmica (con cinta) + + + Térmica directa (sin cinta) + + + Comprobar la impresora antes y después de imprimir + + + Antes de cada trabajo Rotula consulta el estado de la impresora y no envía nada si no tiene etiquetas, está abierta, en pausa o sin cinta. Después del trabajo espera a que la impresora indique que terminó. Para impresoras compatibles con ZPL; muchos controladores USB no pueden responder y entonces se imprime sin la comprobación. + + + Abrir puesto de impresión... + + + Crear PIN del puesto de impresión... + + + Puesto de impresión + + + Elija la carpeta del puesto de impresión (con su station.json, o con las etiquetas que se ofrecerán) + + + No se puede abrir el puesto de impresión. + + + Salir + + + Todas las etiquetas + + + Busque o escanee una etiqueta + + + Escriba un nombre o escanee el código de la etiqueta y pulse Intro. O toque una etiqueta de la lista. + + + Ninguna etiqueta coincide con la búsqueda. + + + Ninguna etiqueta tiene el código "{0}". Escanéelo de nuevo o elija una etiqueta de la lista. + + + Algunas etiquetas no están disponibles: + + + No se puede usar la etiqueta "{0}": {1} + + + Impresora: {0} + + + No hay ninguna impresora configurada para esta etiqueta. Pida a quien configuró el puesto que añada una en station.json. + + + Cantidad + + + De 1 a {0} etiquetas + + + Escriba una cantidad de 1 a {0} y pulse Imprimir de nuevo. + + + Imprimir + + + Imprimir todos los registros + + + Hay {0} registros de los datos de la etiqueta listos para imprimir. + + + No se pudieron leer los datos de la etiqueta: {0} + + + los datos no tienen registros. + + + Esta etiqueta no hace preguntas. Indique la cantidad y pulse Imprimir. + + + Algunas respuestas no se aceptan. Corrija los campos marcados en rojo y vuelva a imprimir. + + + Vista previa + + + Comprobando la impresora... + + + Impresora lista + + + Estado de la impresora desconocido + + + La impresora necesita atención + + + Imprimiendo... + + + Rotula comprueba la impresora antes de enviar y espera hasta que indique que terminó. + + + No se imprimió: la impresora necesita atención + + + Enviado a la impresora + + + Impreso y verificado + + + Enviado, pero sin confirmar + + + Enviado, pero la impresora indica un problema + + + No se imprimió + + + Escriba el PIN para salir del puesto de impresión + + + PIN + + + Salir del puesto + + + Ese PIN no es correcto. Inténtelo de nuevo. + + + Demasiados PIN incorrectos. Espere {0} segundos y vuelva a intentarlo. + + + Crear un PIN para el puesto de impresión + + + El PIN se necesita para salir del puesto de impresión. Rotula solo guarda un código protegido creado a partir de él, nunca el PIN. Copie la línea en el station.json del puesto. + + + PIN nuevo + + + Escriba el PIN otra vez + + + Crear + + + Línea para station.json + + + Copiar + + + Copiado. Péguelo en station.json, junto a los demás ajustes. + + + Un PIN necesita de {0} a {1} caracteres. + + + Los dos PIN no coinciden. Escríbalos de nuevo. + + + No se encontró "{0}". Revise la ruta indicada al puesto de impresión. + + + No se pudo leer "{0}". Compruebe que el archivo no esté bloqueado y que este usuario pueda leerlo. + + + "{0}" tiene un error cerca de la línea {1} ({2}): un ajuste está mal escrito, tiene un valor de otro tipo, o falta una coma o unas comillas. + + + Los ajustes del puesto se escribieron para una versión más reciente de Rotula (formato {0}). Actualice Rotula en este equipo. + + + No hay ninguna etiqueta que ofrecer. Ponga archivos .rotula en la carpeta del puesto o enumérelos en "labels" de station.json. + + + No se encontró la etiqueta "{0}" indicada en station.json y se omite. + + + "{0}" no es una etiqueta de Rotula (archivo .rotula). Corríjalo en station.json. + + + La entrada de etiqueta {0} de station.json no tiene "file". Añada el nombre del archivo de la etiqueta. + + + Dos etiquetas usan el código "{0}". Dé a cada etiqueta su propio código para que un escaneo abra la correcta. + + + La impresora "{0}" no está definida en "printers" de station.json. + + + La impresora "{0}" necesita "windowsPrinter" (una impresora instalada) o "host" (una dirección de red), pero no ambos. + + + El puerto de la impresora "{0}" debe estar entre 1 y 65535 (normalmente 9100). + + + La resolución de la impresora "{0}" debe ser 203, 300 o 600. + + + La impresora "{0}" tiene una oscuridad fuera de 0 a 30 o una velocidad fuera de 1 a 14. + + + Los ajustes de copias del puesto no son válidos: "maxCopies" debe estar entre 1 y 99999999 y "defaultCopies" entre 1 y "maxCopies". + + + Los ajustes de copias de la etiqueta "{0}" no son válidos: "maxCopies" debe estar entre 1 y 99999999 y "defaultCopies" entre 1 y "maxCopies". + + + "exitPinHash" no es un código de PIN creado por Rotula. Créelo con Archivo > Crear PIN del puesto de impresión (el PIN nunca se guarda como texto). + + + + Administrado por tu organización + + + Buscar actualizaciones una vez al día + + + Si está activado, Rotula pregunta una vez al día a la página del proyecto en GitHub si hay una versión más reciente. No se envía nada sobre ti ni sobre este equipo, y no se descarga ni se instala nada. + + + Rotula {0} está disponible. Puedes descargarlo desde la página de la versión. + + + Actualización disponible + + + Ver la versión + + + Cerrar este aviso + + + No se pudo abrir el navegador. La página de la versión es {0} + + + Mostrar la etiqueta como se imprime o como se diseñó + + + Como se imprime + + + Muestra la etiqueta tal como sale de la impresora, girada o reflejada según la configuración de impresión de la etiqueta. + + + Como se diseñó + + + Muestra la etiqueta derecha, tal como la edita en el lienzo. + + + Vista como se imprime: la orientación de impresión de esta etiqueta es invertida, así que la impresora la imprime al revés. + + + Vista como se imprime: esta etiqueta está configurada para imprimirse en espejo, así que la izquierda y la derecha se intercambian. + + + Vista como se imprime: esta etiqueta está configurada para imprimirse al revés y en espejo. + + + Vista como se diseñó, tal como la edita. Elija "Como se imprime" para ver cómo sale de la impresora. + + + Se imprime al revés + + + Se imprime en espejo + + + Se imprime al revés y en espejo + + + Orientación de impresión + + + El lienzo muestra la etiqueta derecha para que pueda editarla. Abra la vista previa para verla como se imprime. Puede cambiarlo en las propiedades de la etiqueta. + + + Hojas de etiquetas (PDF)... + + + Hojas de etiquetas (PDF)... + + + Coloca las etiquetas en hojas para una impresora de oficina (láser o de inyección de tinta) y las guarda como PDF. + + + Hojas de etiquetas + + + Imprime etiquetas en hojas con una impresora de oficina (láser o de inyección de tinta). Rotula guarda las hojas como PDF a su tamaño real; imprímelo a tamaño real (100 %). + + + Hay datos cargados: los registros marcados van a las hojas, con sus copias y números de serie, igual que al imprimir. + + + Hoja + + + Las hojas de distintos proveedores con el mismo tamaño de etiqueta pueden variar un milímetro. Imprime una página en papel normal, ponla sobre una hoja de etiquetas y ajusta los márgenes si hace falta. + + + Diseño personalizado + + + {0}, {1} por hoja ({2} x {3}), {4} + + + {0} x {1} in + + + {0} x {1} mm + + + Papel + + + Medidas en pulgadas + + + Todas las medidas en milímetros + + + Todas las medidas en pulgadas + + + Ancho del papel + + + Alto del papel + + + Ancho de la etiqueta + + + Alto de la etiqueta + + + Etiquetas a lo ancho + + + Etiquetas a lo alto + + + Margen superior + + + Margen izquierdo + + + A lo ancho: de borde a borde + + + A lo alto: de borde a borde + + + La distancia desde el borde izquierdo (o superior) de una etiqueta hasta el mismo borde de la siguiente: el tamaño de la etiqueta más el espacio entre etiquetas. + + + Empezar en la posición + + + En una hoja ya empezada, la primera posición libre. Las posiciones se cuentan por filas desde arriba a la izquierda. También puedes hacer clic en una posición de la vista previa. + + + Orden de llenado + + + Guías + + + Imprime el PDF a tamaño real (100 %), sin "Ajustar a la página"; si no, las etiquetas no coincidirán con la hoja. + + + Vista previa de la hoja + + + Haz clic en una posición para empezar ahí. Las posiciones grises ya están usadas; los números muestran dónde van las primeras etiquetas. + + + Posición {0}: ya usada + + + Posición {0}: etiqueta {1} + + + Posición {0}: vacía + + + Guardar como PDF... + + + Etiquetas: {0}. Hojas: {1} ({2} por hoja). + + + {0} por hoja. Las copias vienen de una columna de los datos, así que el número de hojas se sabe al crear el PDF. + + + {0}: escribe un número. + + + Copias: escribe un número entero del 1 al {0}. + + + Creando las hojas... + + + Se guardó {2} (etiquetas: {0}, hojas: {1}). Imprímelo a tamaño real (100 %). + + + Son más de {0} etiquetas, demasiadas para un PDF. Marca menos registros o baja las copias, y guarda el resto en otro archivo. + + + Carta (8,5 x 11 in) + + + A4 (210 x 297 mm) + + + Oficio (8,5 x 14 in) + + + Tamaño personalizado + + + Por filas, luego hacia abajo + + + Por columnas, luego a la derecha + + + Ninguna + + + Marcas de corte en los márgenes + + + Contorno de cada etiqueta (prueba en papel normal) + + + El papel debe medir entre {0} y {1} mm por lado. + + + Las etiquetas deben medir al menos {0} mm de ancho y de alto. + + + Una hoja puede tener de 1 a {0} etiquetas a lo ancho y de 1 a {1} a lo alto. + + + Los márgenes no pueden ser negativos. + + + Las etiquetas se superponen a lo ancho: de un borde al siguiente hay {0} mm, pero cada etiqueta mide {1} mm de ancho. La distancia debe ser al menos el ancho de la etiqueta. + + + Las etiquetas se superponen a lo alto: de un borde al siguiente hay {0} mm, pero cada etiqueta mide {1} mm de alto. La distancia debe ser al menos el alto de la etiqueta. + + + Las etiquetas no caben a lo ancho del papel: la última columna termina a {0} mm y el papel mide {1} mm. Reduce el margen izquierdo, la distancia o el número de etiquetas a lo ancho. + + + Las etiquetas no caben a lo alto del papel: la última fila termina a {0} mm y el papel mide {1} mm. Reduce el margen superior, la distancia o el número de etiquetas a lo alto. + + + Algunas etiquetas llegan a menos de {0} mm del borde del papel. La mayoría de las impresoras de oficina no imprimen ahí, así que aleja el texto y los códigos de barras de esos bordes. + + + La posición inicial debe ser un número del 1 al {0}. + + + El diseño de la etiqueta mide {0} x {1} mm, más que las etiquetas de {2} x {3} mm de esta hoja. Se imprime a su propio tamaño, centrado, y se recorta en los bordes de la etiqueta. Elige una hoja con etiquetas más grandes o cambia el tamaño del diseño. + + + El diseño de la etiqueta mide {0} x {1} mm, menos que las etiquetas de {2} x {3} mm de esta hoja. Se imprime a su propio tamaño, centrado en cada etiqueta. + + + {0} necesita NOMBRE=VALOR, pero recibió "{1}". + + + "{1}" no es un valor válido para {0}. + + + rotula {0}: imprime etiquetas de Rotula sin el diseñador, para scripts, programadores de tareas y sistemas de almacén. + +Comandos: + rotula print ETIQUETA.rotula --printer NOMBRE | --host DIRECCIÓN [opciones] + Imprime una etiqueta. Opciones: + --port N puerto TCP de --host (predeterminado 9100) + --copies N copias de la etiqueta, o de cada registro + --data ARCHIVO registros de un archivo .csv, .txt, .xlsx o .json (una etiqueta por registro) + --sheet NOMBRE la hoja de Excel que se lee + --set NOMBRE=VALOR el valor de un campo (repítalo para más campos) + --answer NOMBRE=VALOR la respuesta a una pregunta que la etiqueta hace al imprimir + --dpi 203|300|600 resolución del cabezal (predeterminado: la que indica el controlador de Windows) + --darkness 0-30 oscuridad enviada antes de las etiquetas + --speed 1-14 velocidad enviada antes de las etiquetas + --no-status no pregunta el estado a la impresora antes de cada trabajo + --no-verify no espera a que la impresora indique que terminó + --dry-run muestra el ZPL sin imprimir (--output ARCHIVO lo guarda en un archivo) + --reference TEXTO su propia referencia, repetida en el resultado + --counter-file ARCHIVO dónde se recuerdan los números de serie (predeterminado: el mismo archivo que la aplicación) + rotula status --printer NOMBRE | --host DIRECCIÓN [--port N] + Pregunta a la impresora si puede imprimir ahora. + rotula printers + Lista las impresoras de Windows. + rotula serve [--settings ARCHIVO] [--urls URL] + Ejecuta la API REST. "rotula serve --init [--label-folder CARPETA]" crea un archivo de configuración inicial con una clave nueva. + rotula new-key + Crea una clave de API nueva y el hash que va en el archivo de configuración. + +Todos los comandos aceptan --json (salida para programas) y --lang en|es|pt-BR|fr-CA. +Códigos de salida: 0 hecho, 1 falló la impresión, 2 línea de comandos incorrecta, 3 errores en la etiqueta, 4 impresora no lista, 5 bloqueado por directiva, 6 cancelado. + + + Se creó el archivo de configuración {0}. Agregue sus impresoras en allowedPrinters y ejecute "rotula serve". + + + El archivo de configuración {0} ya existe; no se modificó. + + + La opción {0} necesita un valor. + + + Clave de API nueva (solo se muestra ahora; entréguela al sistema que llama al servidor): {0} +Hash que debe agregar a apiKeyHashes en el archivo de configuración: {1} + + + Indique exactamente un archivo de etiqueta, por ejemplo: rotula print envio.rotula --printer "Muelle 1" + + + Registro {0}: + + + Ejecute "rotula help" para ver los comandos y las opciones. + + + Comando desconocido "{0}". + + + Opción desconocida {0}. + + + Advertencia: + + + Faltan respuestas a las preguntas de la etiqueta o alguna no es válida. La etiqueta pregunta: {0}. Las preguntas marcadas con * son obligatorias; las demás toman su valor predeterminado. + + + Se necesita una clave de API válida. Envíela en el encabezado X-Api-Key. + + + No se pudo leer la solicitud: {0} + + + Las copias deben ser un número entero de 1 a {0}. + + + No se pudo leer el archivo de datos {0}: {1} + + + El archivo de datos {0} no existe.{1} + + + Los datos de {0} deben ser una lista de registros (objetos con nombres de campo y valores) o un solo registro. {1} + + + La resolución debe ser 203, 300 o 600 DPI. + + + Este servidor no acepta etiquetas enviadas como contenido (allowInlineLabels está desactivado). Indique un archivo de etiqueta de una de sus carpetas de etiquetas. + + + Envíe el nombre de un archivo de etiqueta (label) o el contenido de la etiqueta (labelContent), no ambos. + + + No se encontró la etiqueta {0}. + + + La etiqueta {0} no es un archivo .rotula dentro de una de las carpetas de etiquetas de este servidor. Indíquela relativa a una carpeta de etiquetas, sin letra de unidad ni "..". + + + Indique un archivo de etiqueta (label) o envíe el contenido de la etiqueta (labelContent). + + + La etiqueta {0} supera el límite de {1} KB. + + + Los datos no tienen registros, así que no se imprimió nada. + + + Ningún registro produjo una etiqueta, así que no se envió nada. Vea los problemas indicados en el resultado. + + + No se pudo escribir el archivo {0}: {1} + + + Indique una impresora de Windows o la dirección de una impresora de red, no ambas. + + + La impresora {0} no está en la lista de impresoras permitidas de este servidor (allowedPrinters). No se envió nada. + + + Indique una impresora: una impresora de Windows (printer) o la dirección de una impresora de red (host). + + + La impresión con el motor de impresión está desactivada por la directiva de su organización para este equipo. Consulte a su departamento de TI. + + + Una solicitud puede tener como máximo {0} registros. + + + Envíe los valores de una etiqueta o una lista de registros, no ambos. + + + [predeterminada] + + + No hay impresoras instaladas en Windows. + + + [sin conexión] + + + No se imprimió nada (prueba). Este es el ZPL que se enviaría a {0}. + + + Se enviaron {0} etiqueta(s) a {1}. + + + Se enviaron {0} registro(s), {1} etiqueta(s), a {2} en {3} trabajo(s). + + + El servidor no pudo escuchar en {0}: {1} + + + Motor de impresión de Rotula escuchando en {0}. Configuración: {1}. Pulse Ctrl+C para detenerlo. + + + "{0}" en apiKeyHashes no es un hash de clave (64 caracteres hexadecimales). Cree uno con "rotula new-key". + + + La carpeta de etiquetas {0} no existe. + + + No se puede usar el archivo de configuración {0}: {1} + + + El archivo de configuración {0} no existe. Cree uno con "rotula serve --init". + + + No hay carpetas de etiquetas (labelFolders) y las etiquetas enviadas como contenido están desactivadas, así que no se puede imprimir ninguna etiqueta. + + + El archivo de configuración {0} no tiene ninguna clave de API (apiKeyHashes). Cree una con "rotula new-key". + + + Todavía no hay impresoras permitidas (allowedPrinters), así que se rechazará toda solicitud de impresión. + + + El servidor acepta llamadas de otros equipos por http sin cifrar: las claves de API y los datos de las etiquetas viajan por la red sin cifrado. Use https. + diff --git a/src/Rotula.App/Resources/Strings.fr-CA.resx b/src/Rotula.App/Resources/Strings.fr-CA.resx new file mode 100644 index 0000000..c724575 --- /dev/null +++ b/src/Rotula.App/Resources/Strings.fr-CA.resx @@ -0,0 +1,6687 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cliquez sur un élément pour le sélectionner. Faites-le glisser pour le déplacer. Les touches fléchées déplacent la sélection d’un point ; avec Maj, de huit points. Appuyez sur Suppr pour le supprimer. + + + Zone de conception + + + Annuler + + + Créer + + + Rotula n’a pas pu démarrer. Rien n’a été modifié. Les détails ont été enregistrés dans {0}. Veuillez signaler le problème en joignant ce fichier. + + + Un problème inattendu est survenu lors de la dernière action. Votre étiquette est toujours ouverte ; par précaution, enregistrez-la sous un autre nom. Les détails ont été enregistrés dans {0}. + + + L’exportation a échoué + + + L’exportation s’est terminée avec des avertissements + + + Corrigez d’abord les erreurs + + + L’étiquette contient des erreurs ; elle ne peut donc pas encore être convertie en ZPL ni imprimée. Corrigez ces problèmes et réessayez : + + + L’image n’a pas pu être utilisée + + + Le ZPL n’a pas pu être importé + + + Certaines parties n’ont pas été importées + + + Format de l’étiquette + + + Nouvelle étiquette + + + Le fichier n’a pas pu être ouvert + + + L’étiquette n’a pas été imprimée + + + « {0} » contient des modifications non enregistrées. Voulez-vous d’abord les enregistrer ? + + + Modifications non enregistrées + + + L’étiquette n’a pas pu être enregistrée + + + Sans titre + + + Rectangle + + + Cercle + + + Champ de données + + + Exemple + + + Texte + + + Image + + + Ligne + + + Texte + + + Windows Latin-1 (1252) + + + Page de codes 850 (anciennes imprimantes) + + + UTF-8 (recommandé) + + + Code 128 + + + Code 39 + + + Code 93 + + + Data Matrix + + + EAN-13 + + + 2 parmi 5 entrelacé + + + PDF417 + + + Code QR + + + UPC-A + + + Automatique + + + EAN-128 + + + Aucun + + + Mode UCC + + + Coin supérieur gauche (^FO) + + + Ligne de base / coin inférieur gauche (^FT) + + + 180° (à l’envers) + + + 0° (normal) + + + 270° dans le sens horaire + + + 90° dans le sens horaire + + + Noir + + + Blanc + + + Au-dessus des barres + + + Sous les barres + + + Floyd-Steinberg (photos) + + + Tramage ordonné + + + Seuil fixe + + + À l’envers + + + Normale + + + Points + + + Pouces + + + Millimètres + + + Horizontale + + + Verticale + + + Continu + + + Marque noire + + + Détection d’espacement + + + Thermique directe + + + Transfert thermique (ruban) + + + 203 ppp + + + 300 ppp + + + 600 ppp + + + Élevée (30 %) + + + Faible (7 %) + + + Moyenne (15 %) + + + Quartile (25 %) + + + Centré + + + Justifié + + + Gauche + + + Droite + + + {0} est endommagé ou a été modifié à la main et ne peut pas être lu. Ouvrez une copie de sauvegarde si vous en avez une. + + + {0} a été enregistré par une version plus récente de Rotula (format de fichier {1}). Mettez Rotula à jour pour l’ouvrir. + + + {0} n’est pas un fichier d’étiquette Rotula. Choisissez un fichier dont le nom se termine par .rotula. + + + {0} n’a pas pu être lu. Vérifiez que le fichier existe toujours et que vous avez le droit de l’ouvrir. + + + Le fichier {0} n’a pas pu être écrit. Vérifiez que le dossier existe, qu’il reste de l’espace libre sur le disque et que vous avez le droit d’y écrire, puis réessayez ou choisissez un autre emplacement. + + + Tous les fichiers + + + Images + + + Document PDF + + + Image JPEG + + + Image PNG + + + Étiquettes Rotula + + + Fichiers ZPL + + + {0} n’est pas une image que Rotula peut lire. Choisissez un fichier PNG, JPG, BMP ou GIF. + + + Le ZPL n’a pas pu être interprété. Vérifiez que le fichier contient une étiquette qui commence par ^XA et se termine par ^XZ. + + + L’importation de ZPL n’est pas encore offerte dans cette version. Vous pouvez tout de même concevoir l’étiquette ici et en exporter le ZPL. + + + Ligne {0} : {1} + + + le nom du compteur est absent ou non valide (utilisez des lettres, des chiffres et des traits de soulignement) + + + le remplissage par des zéros doit être compris entre 0 et 18 + + + la valeur de départ n’est pas valide + + + pair + + + impair + + + Le Code 128 ne peut encoder que les caractères standard du clavier. Retirez les accents et les symboles spéciaux des données. + + + Le caractère {0} ne peut pas être utilisé dans les données Code 128, car l’imprimante le lit comme une commande. Retirez-le. + + + Le Code 39 n’accepte que les lettres majuscules, les chiffres et - . espace $ / + %. Modifiez les données pour n’utiliser que ces caractères. + + + Le Code 93 ne peut encoder que les caractères standard du clavier. Retirez les accents et les symboles spéciaux des données. + + + Le nombre de colonnes doit être compris entre {0} et {1}. + + + Le code à barres ne contient aucune donnée. Tapez le texte ou le nombre à encoder. + + + Les données Data Matrix sont trop longues (maximum {0} caractères). Raccourcissez le texte. + + + Ce type de code à barres n’accepte que des chiffres (0 à 9). Retirez les lettres ou les symboles. + + + Le dernier chiffre n’est pas le bon chiffre de contrôle EAN-13. Il devrait être {0} ; sinon, retirez le dernier chiffre et laissez l’imprimante l’ajouter. + + + L’EAN-13 nécessite {0} chiffres (le chiffre de contrôle est ajouté pour vous) ou {1} chiffres, chiffre de contrôle compris. + + + La hauteur des barres doit être comprise entre {0} et {1} points. + + + Le 2 parmi 5 entrelacé nécessite un nombre {0} de chiffres avec ce réglage de chiffre de contrôle. Ajoutez ou retirez un chiffre, ou modifiez l’option du chiffre de contrôle. + + + La taille du module doit être comprise entre {0} et {1} points. + + + La largeur de la barre étroite doit être comprise entre {0} et {1} points. + + + Le niveau de sécurité PDF417 doit être compris entre {0} et {1}. + + + Les données PDF417 sont trop longues (maximum {0} caractères). Raccourcissez le texte. + + + Le modèle QR doit être 1 ou 2. Choisissez 2, à moins que votre lecteur n’exige l’ancien modèle. + + + Les données du code QR sont trop longues (maximum {0} caractères). Raccourcissez le texte. + + + Le nombre de rangées doit être 0 (automatique) ou compris entre {0} et {1}. + + + Ce type de code à barres n’est pas pris en charge. Choisissez un autre type. + + + Le dernier chiffre n’est pas le bon chiffre de contrôle UPC-A. Il devrait être {0} ; sinon, retirez le dernier chiffre et laissez l’imprimante l’ajouter. + + + L’UPC-A nécessite {0} chiffres (le chiffre de contrôle est ajouté pour vous) ou {1} chiffres, chiffre de contrôle compris. + + + Le rapport entre barre large et barre étroite doit être compris entre {0} et {1}. + + + Cet élément dépasse de l’étiquette ; une partie ne sera donc pas imprimée. Déplacez-le à l’intérieur de l’étiquette. + + + La position doit être comprise entre {0} et {1} points. Déplacez l’élément à l’intérieur de l’étiquette. + + + Le type d’élément {0} ne peut pas être converti en ZPL. Supprimez-le. + + + Erreur : + + + La hauteur de la police doit être comprise entre {0} et {1} points. + + + Le diamètre du cercle doit être compris entre {0} et {1} points. + + + L’arrondi des coins doit être compris entre {0} et {1}. + + + La taille et l’épaisseur doivent être comprises entre {0} et {1} points. + + + La taille de l’image n’est pas valide (largeur jusqu’à {0} points, hauteur jusqu’à {1} points). Réduisez l’image. + + + Cette image ne contient aucune illustration. Choisissez un fichier image dans les propriétés. + + + Le seuil doit être compris entre {0} et {1}. + + + Certaines barres ressemblent à un code à barres dessiné avec des lignes. Vérifiez cette partie de l’étiquette. + + + Ce Code 128 utilise des codes d’invocation (>), qui sont conservés tels qu’ils ont été saisis. + + + Le niveau de qualité Data Matrix {0} n’est pas pris en charge et a été ignoré. + + + Ce code QR utilise le mode de caractères manuel, qui est conservé tel qu’il a été saisi. + + + La commande {0} n’est pas reconnue par le concepteur. Elle a été conservée en ZPL brut et sera imprimée sans modification. + + + L’encodage des caractères passe à ^CI{0} en cours de route. Le texte qui suit est lu avec cet encodage. + + + Le jeu de caractères ^CI{0} est un jeu national et n’a été qu’approximé. + + + La réaffectation de caractères dans ^CI est ignorée. + + + Le jeu de caractères ^CI{0} n’est pas pris en charge ; certains caractères pourraient donc mal s’afficher. + + + Un champ n’a pas de ^FS à la fin. Il a été fermé automatiquement. + + + Le fichier n’a pas pu être lu ({0}). Vérifiez qu’il existe et que vous avez le droit de l’ouvrir. + + + L’étiquette utilise une police personnalisée (^A@). L’aperçu utilise une police de remplacement. + + + Un graphique utilise le format {0}, qui n’est pas pris en charge. Il a été conservé en ZPL brut. + + + Un graphique contient des données non valides ou est trop volumineux (total de {0} octets ; la limite est de 16 Mo par fichier). Il a été conservé en ZPL brut. + + + Un graphique contient moins de données qu’il n’en déclare. Il a été importé jusqu’où les données le permettent. + + + L’enregistrement d’une image dans l’imprimante (^IS {0}) n’est pas pris en charge. Il a été conservé en ZPL brut. + + + Une partie du fichier n’a pas pu être interprétée ({0}). Le reste a été importé. + + + L’origine de l’étiquette (^LH) arrive après certains champs ; elle ne s’applique donc qu’à ce qui suit. + + + Le fichier contient {0} étiquettes. Seule la première a été ouverte. + + + Un deuxième ^XA commence avant la fin de l’étiquette précédente. + + + L’étiquette n’a pas de ^XZ à la fin. Elle a été fermée automatiquement. + + + La longueur de l’étiquette (^LL) est absente ; une longueur a donc été estimée. Vérifiez-la dans les propriétés de l’étiquette. + + + Le fichier ne commence pas par ^XA. Il a quand même été lu comme une seule étiquette. + + + La largeur de l’étiquette (^PW) est absente ; une largeur a donc été estimée. Vérifiez-la dans les propriétés de l’étiquette. + + + Un ^XZ sans ^XA correspondant a été ignoré. + + + {0} a une valeur non valide ({1}) ; la valeur par défaut a donc été utilisée. + + + {0} a une valeur hors plage ({1}) ; elle a donc été ignorée. + + + Une commande brute contient des caractères qui ne sont pas de l’ASCII simple. Vérifiez qu’elle s’imprime correctement. + + + Le fichier rappelle le format stocké {0}, qui se trouve dans l’imprimante et ne peut pas être affiché ici. + + + Le fichier rappelle le graphique stocké {0}, qui se trouve dans l’imprimante et ne peut pas être affiché ici. + + + Le fichier rappelle l’image stockée {0}, qui se trouve dans l’imprimante et ne peut pas être affichée ici. + + + {0} a été approximé par {1}. + + + Le réglage {0} n’est pas pris en charge par le concepteur. Il a été conservé en ZPL brut. + + + Le stockage d’un format dans l’imprimante (^DF {0}) n’est pas pris en charge. Il a été conservé en ZPL brut. + + + Une partie du texte n’a pas pu être décodée avec {0}. Vérifiez les lettres accentuées. + + + Du texte situé hors de tout champ a été ignoré : {0} + + + Les positions ont été converties depuis des unités {0} (facteur {1}). + + + La hauteur de l’étiquette doit être comprise entre {0} et {1} points. Modifiez la hauteur dans les propriétés de l’étiquette. + + + La largeur de l’étiquette doit être comprise entre {0} et {1} points. Modifiez la largeur dans les propriétés de l’étiquette. + + + La colonne de données {0} n’est pas utilisée par l’étiquette. + + + La colonne des copies {0} est introuvable dans les données. Choisissez une colonne existante. + + + Le nombre de copies ({0}) n’est pas valide. Utilisez un nombre entier égal ou supérieur à 0. + + + Le compteur {0} a un réglage non valide ({1}). + + + le nom du compteur est absent ou non valide (utilisez des lettres, des chiffres et des traits de soulignement) + + + Le compteur {0} a dépassé sa valeur maximale. + + + le remplissage par des zéros doit être compris entre 0 et 18 + + + la valeur de départ n’est pas valide + + + Une valeur entre guillemets du fichier de données a un guillemet ouvrant, mais pas de guillemet fermant ; les dernières valeurs pourraient donc être erronées. Corrigez les guillemets dans le fichier et chargez-le de nouveau. + + + La variable {0} est associée à la colonne {1}, qui ne figure pas dans les données. + + + La variable {0} de l’étiquette n’a aucune colonne correspondante dans les données. + + + Une ligne de données n’a pas pu être fusionnée ({0}). Vérifiez cette ligne dans le fichier de données. + + + Le nombre avant pause doit être compris entre {0} et {1}. + + + Le nombre de copies doit être compris entre {0} et {1}. + + + Le nombre de répliques doit être compris entre {0} et {1}. + + + Le nombre maximal de lignes doit être compris entre {0} et {1}. + + + La largeur du bloc de texte doit être comprise entre {0} et {1} points. + + + Le caractère {0} ne peut pas être imprimé avec l’encodage de caractères choisi. Choisissez UTF-8 dans les propriétés de l’étiquette ou retirez le caractère. + + + Problème détecté ({0}). Vérifiez les réglages de cet élément. + + + La variable {0} n’est pas déclarée. Ajoutez-la aux variables de l’étiquette ou corrigez son nom. + + + Avertissement : + + + Descendre + + + Monter + + + À propos de Rotula + + + Aligner + + + En bas + + + Centrer horizontalement + + + Centrer verticalement + + + À gauche + + + À droite + + + En haut + + + _Organiser + + + Avancer d’un niveau + + + Mettre au premier plan + + + Copier + + + Couper + + + Supprimer + + + Répartir + + + Horizontalement + + + Verticalement + + + Dupliquer + + + É_dition + + + Quitter + + + Exporter + + + Document PDF... + + + Image JPEG... + + + Image PNG... + + + Fichier ZPL... + + + _Fichier + + + Afficher la grille + + + A_ide + + + Importer du ZPL... + + + Langue + + + Comme Windows + + + Menu principal + + + Nouvelle... + + + Ouvrir... + + + Coller + + + Imprimer... + + + Ouvrir un fichier récent + + + Rétablir + + + Rétablir {0} + + + Faire pivoter de 90° dans le sens antihoraire + + + Faire pivoter de 90° dans le sens horaire + + + Enregistrer + + + Enregistrer sous... + + + Tout sélectionner + + + Sélectionner l’élément suivant + + + Sélectionner l’élément précédent + + + Reculer d’un niveau + + + Mettre en arrière-plan + + + Aligner sur la grille + + + Aligner sur les repères + + + Annuler + + + Annuler {0} + + + Points + + + Pouces + + + Millimètres + + + Unités des règles + + + _Affichage + + + Taille réelle (100 %) + + + Ajuster l’étiquette à la fenêtre + + + Zoom avant + + + Zoom arrière + + + Code à barres + + + Valeurs par défaut et imprimante + + + {0} + + + Image + + + Étiquette + + + Calques + + + Marges (points) + + + {0} éléments sont sélectionnés. Déplacez-les, alignez-les ou organisez-les ensemble, ou sélectionnez un seul élément pour modifier ses propriétés. + + + Position + + + Tâche d’impression + + + Propriétés + + + Sélection + + + Forme + + + Format de l’étiquette + + + Texte + + + Éléments + + + Variables + + + Code ZPL (lecture seule) + + + 100 x 150 mm + + + 100 x 50 mm + + + 2 x 1 po + + + 4 x 3 po + + + 4 x 6 po + + + Format personnalisé + + + L’aperçu est approximatif : le texte est dessiné avec une police de remplacement ; la forme des lettres et les retours à la ligne peuvent donc différer légèrement sur l’étiquette imprimée. + + + Remarque : l’aperçu est approximatif + + + L’étiquette n’a pas de format valide ; elle ne peut donc pas être dessinée. Indiquez une largeur et une hauteur supérieures à zéro dans les propriétés de l’étiquette. + + + L’étiquette est trop grande pour être dessinée à ce niveau de zoom. Faites un zoom arrière et réessayez. + + + L’étiquette n’a pas pu être imprimée. Vérifiez la connexion de l’imprimante et réessayez. + + + Hauteur des barres (points) + + + Retour à la ligne automatique (ajuster le texte dans un cadre) + + + Alignement + + + Retrait négatif (points) + + + Interligne supplémentaire (points) + + + Nombre maximal de lignes + + + Largeur du cadre (points) + + + Ajouter un chiffre de contrôle + + + Choisir une autre image... + + + Mode Code 128 + + + Couleur + + + Colonnes (0 = automatique) + + + Arrondi des coins (0 à 8) + + + Données (utilisez {{nom}} pour les variables) + + + Hauteur de police par défaut (points) + + + Largeur de police par défaut (points) + + + Rotation par défaut + + + Diamètre (points) + + + Direction + + + Conversion en noir et blanc + + + Résolution de l’imprimante (ppp) + + + Encodage des caractères + + + Hauteur de la police (points) + + + Police (laisser vide pour utiliser celle de l’étiquette) + + + Une seule lettre ou un seul chiffre. 0 est la police vectorielle. + + + Largeur de la police (points, 0 = automatique) + + + Hauteur (points) + + + Imprimer le texte avec les barres + + + Position du texte + + + Laissez vide pour utiliser la valeur par défaut de l’étiquette. + + + Inverser le noir et le blanc + + + Hauteur + + + Unité + + + Largeur + + + Longueur (points) + + + Bas + + + Gauche + + + Droite + + + Haut + + + Détection du support + + + Type de support + + + Taille du module (points) + + + Largeur de la barre étroite (points) + + + Nom + + + Orientation d’impression + + + La position correspond à + + + Le coin supérieur gauche, ou la ligne de base du texte / le coin inférieur gauche. + + + Pause toutes les N étiquettes (0 = jamais) + + + Niveau de sécurité (1 à 8) + + + PDF417 compact (tronqué) + + + Correction d’erreurs + + + Modèle QR (1 ou 2) + + + Copies + + + Répliques de chaque numéro de série + + + Impression inversée (inverse ce qui se trouve dessous) + + + Rotation + + + Valeur de l’étiquette + + + Rangées (0 = automatique) + + + Type de code à barres + + + Texte + + + Épaisseur du trait (points) + + + Seuil (0 à 255) + + + Nom de la variable + + + Variables (une par ligne : nom=valeur d’exemple) + + + Pour utiliser une variable dans un texte ou dans les données d’un code à barres, écrivez {{nom}}. + + + Rapport large/étroite (2,0 à 3,0) + + + Largeur (points) + + + X (points) + + + Y (points) + + + Règle horizontale + + + Règle verticale + + + Redimensionner le panneau des calques + + + Ajouté : {0} + + + Barre d’état + + + Exporté : {0} + + + Importé : {0}. Enregistrez l’étiquette pour la conserver. + + + {0} éléments sélectionnés + + + Nouvelle étiquette créée + + + Aucun problème détecté + + + Aucune sélection + + + Il n’y a rien à coller. Copiez d’abord un élément. + + + 1 élément sélectionné + + + Ouvert : {0} + + + Envoyé à l’imprimante + + + {0} erreurs, {1} avertissements + + + Enregistré : {0} + + + Sélectionné : {0} + + + Zoom + + + ZPL copié dans le presse-papiers + + + Le code ZPL n’a pas pu être créé + + + Code à barres + + + Rectangle + + + Cercle + + + Champ de données + + + Image + + + Ligne + + + Code QR + + + Texte + + + Barre d’édition + + + Barre Fichier + + + Barre Affichage + + + l’ajout + + + l’alignement + + + l’avancement d’un niveau + + + la mise au premier plan + + + la suppression + + + la répartition + + + la duplication + + + les réglages de l’étiquette + + + le déplacement + + + le collage + + + la modification de propriété + + + la réorganisation + + + le redimensionnement + + + la rotation + + + le recul d’un niveau + + + la mise en arrière-plan + + + La police doit être une seule lettre ou un seul chiffre. + + + Le format de l’étiquette n’est pas valide. La largeur peut être d’au plus {0} points et la hauteur d’au plus {1} points. Tapez un format plus petit. + + + Ce n’est pas un nombre entier. Tapez uniquement des chiffres, par exemple 120. + + + Ce n’est pas un nombre. Tapez un nombre comme 12,5. + + + La valeur doit être comprise entre {0} et {1}. Tapez une valeur dans cette plage. + + + Cette case ne peut pas être vide. Tapez un nombre. + + + Le texte est trop long. Utilisez au plus {0} caractères. + + + Chaque ligne doit avoir la forme nom=valeur. Les noms peuvent contenir des lettres, des chiffres et des traits de soulignement, et chaque nom ne peut apparaître qu’une fois. + + + Copier le ZPL + + + Fermer + + + Arrêter + + + Arrêté. Rien de plus n’a été fait. + + + Un problème inattendu est survenu ({0}). Rien n’a été modifié ; réessayez. + + + Windows n’a aucune imprimante nommée « {0} ». Actualisez la liste des imprimantes et choisissez-en une autre, ou vérifiez que l’imprimante est installée. + + + Vous n’avez pas l’autorisation d’imprimer sur « {0} ». Demandez l’accès à votre administrateur ou choisissez une autre imprimante. + + + L’imprimante « {0} » est hors ligne. Allumez-la, vérifiez le câble et assurez-vous qu’elle n’est pas en pause, puis réessayez. + + + L’imprimante à l’adresse {0}:{1} n’a pas répondu à temps. Vérifiez qu’elle est allumée et sur le même réseau, puis réessayez. + + + L’appareil à l’adresse {0}:{1} a refusé la connexion. Vérifiez le port (la plupart des imprimantes d’étiquettes utilisent le 9100) et que le port réseau de l’imprimante est activé. + + + Impossible de joindre {0}:{1}. Vérifiez l’adresse, le câble réseau ou le Wi-Fi, et que l’imprimante est allumée. + + + L’impression a été annulée. Rien de plus n’a été envoyé. + + + L’étiquette contient des erreurs ; rien n’a donc été envoyé. Corrigez les problèmes ci-dessous et réessayez. + + + Les réglages de l’imprimante sont hors plage ; rien n’a donc été envoyé. Corrigez les valeurs et réessayez. + + + L’étiquette contient des caractères que le jeu de caractères {0} ne peut pas imprimer. Changez le jeu de caractères de l’étiquette pour UTF-8 ou retirez ces caractères. + + + L’impression par les imprimantes Windows n’est offerte que sous Windows. Utilisez une imprimante réseau ou enregistrez plutôt le fichier ZPL. + + + L’imprimante a signalé un problème inattendu ({0}). Vérifiez la connexion et réessayez, ou enregistrez le fichier ZPL et envoyez-le autrement. + + + Le contraste doit être un nombre de 0 à 30. Modifiez-le, ou laissez-le vide pour conserver le réglage de l’imprimante. + + + La vitesse doit être un nombre de 1 à 14 pouces par seconde. Modifiez-la, ou laissez-la vide pour conserver le réglage de l’imprimante. + + + La largeur de l’étiquette est hors plage : elle doit être comprise entre 1 et 9999 points à la résolution choisie. Vérifiez la largeur et la résolution (ppp). + + + La hauteur de l’étiquette est hors plage : elle doit être comprise entre 1 et 32000 points à la résolution choisie. Vérifiez la hauteur et la résolution (ppp). + + + Imprimer + + + Choisissez une imprimante, vérifiez ses réglages et imprimez l’étiquette. + + + Imprimante + + + Configuration de l’imprimante + + + Imprimante Windows (USB ou installée) + + + Imprimante réseau (TCP/IP) + + + Imprimante + + + Actualiser la liste des imprimantes + + + Recherche des imprimantes + + + Aucune imprimante n’est installée dans Windows. Installez le pilote de l’imprimante, ou passez à une imprimante réseau et entrez son adresse. + + + L’imprimante « {0} » utilisée la dernière fois n’est pas installée sur cet ordinateur. Choisissez une autre imprimante. + + + (par défaut) + + + (hors ligne) + + + Nom d’hôte ou adresse IP + + + Port + + + Résolution + + + Détection du support + + + Largeur de l’étiquette (mm) + + + Hauteur de l’étiquette (mm) + + + Contraste (0-30) + + + Vitesse (1-14 po/s) + + + Laissez le contraste et la vitesse vides pour conserver les réglages enregistrés dans l’imprimante. + + + Copies + + + Imprimer + + + Enregistrer en ZPL... + + + Imprimer une étiquette d’essai + + + Imprime une petite étiquette d’exemple pour vérifier la connexion, la largeur et les accents. + + + L’étiquette contient des erreurs et ne peut pas encore être imprimée : + + + L’étiquette sera imprimée avec les valeurs de l’enregistrement affiché dans la zone de conception. + + + Enregistrements à imprimer + + + Cochez les enregistrements à imprimer. Chaque enregistrement coché imprime sa propre étiquette, dans l’ordre des données, avec l’imprimante et le nombre de copies choisis ici. + + + Aucun enregistrement n’est coché ; il n’y a donc rien à imprimer. Cochez au moins un enregistrement dans la liste ci-dessus. + + + La fenêtre Données définit les copies de chaque enregistrement ; ce réglage remplace donc ce nombre. + + + Envoi de l’étiquette à l’imprimante... + + + Envoi de l’étiquette d’essai... + + + Enregistrement du fichier ZPL... + + + Envoyé à l’imprimante ({0} copies). + + + L’étiquette d’essai a été envoyée à l’imprimante. + + + Enregistré : {0}. + + + Aucune imprimante n’est sélectionnée. Choisissez une imprimante installée, ou passez à une imprimante réseau et entrez son adresse. + + + Entrez le nom d’hôte ou l’adresse IP de l’imprimante réseau, par exemple 192.168.1.50. + + + Le port doit être un nombre entier de 1 à 65535 (la plupart des imprimantes d’étiquettes utilisent le 9100). + + + {0} doit être un nombre. Entrez uniquement des chiffres, par exemple 12. + + + Le nombre de copies doit être un entier de 1 à 99999999. + + + Une étiquette avec une quantité d’impression (^PQ) + + + Répéter l’étiquette une fois par copie + + + Ignorer l’enregistrement et continuer + + + S’arrêter à l’enregistrement + + + Chiffres + + + Lettres et chiffres + + + Recommencer + + + Signaler une erreur + + + Imprimante Windows + + + Imprimante réseau (TCP/IP) + + + Fichiers CSV et Excel + + + Fusion de données + + + Ouvrir un fichier de données... + + + Recharger + + + Retirer les données + + + Aucun fichier de données ouvert + + + Les données restent sur cet ordinateur. Seuls les réglages ci-dessous sont enregistrés dans le fichier de l’étiquette, jamais les données elles-mêmes. + + + Fusionnez une feuille de calcul avec votre étiquette + + + Ouvrez un fichier CSV ou Excel (.xlsx). Chaque ligne devient une étiquette, et chaque colonne peut remplir une {{variable}} de l’étiquette. Vous pourrez ensuite parcourir les enregistrements et tous les imprimer. + + + Cette étiquette se souvient de son fichier de données, mais les données ne sont pas chargées. Choisissez Recharger pour le relire, ou ouvrez un autre fichier. + + + Données + + + Variables + + + Compteurs + + + Copies et tâches + + + Vérifier et imprimer + + + Feuille + + + Séparateur + + + Détecter automatiquement + + + virgule (,) + + + point-virgule (;) + + + tabulation + + + Séparateur détecté : {0} + + + Séparateur utilisé : {0} + + + La première ligne contient les noms des colonnes + + + {0} enregistrements, {1} colonnes + + + Aperçu des données + + + Affichage des {0} premiers enregistrements. + + + Remarques sur le fichier de données + + + {0} a été chargé : {1} enregistrements. + + + Lecture de {0}... + + + {0} n’a pas pu être ouvert. S’il est ouvert dans Excel ou un autre programme, fermez-le et réessayez ; sinon, vérifiez que le fichier existe toujours. + + + {0} n’a pas pu être lu comme un fichier CSV ou Excel (.xlsx). Ouvrez-le dans Excel, enregistrez-le de nouveau en .xlsx ou .csv et réessayez. + + + {0} ne contient aucune colonne. Vérifiez que la première ligne contient les noms des colonnes, ou décochez « La première ligne contient les noms des colonnes ». + + + Ouvrez d’abord un fichier de données. + + + Le fichier de données ne contient aucun enregistrement (ligne) à fusionner. + + + Premier enregistrement + + + Enregistrement précédent + + + Enregistrement suivant + + + Dernier enregistrement + + + Enregistrement actuel + + + Aller à l’enregistrement + + + Aller + + + Entrez un numéro d’enregistrement de 1 à {0}. + + + Aucun enregistrement + + + Enregistrement {0} sur {1} + + + Sélectionné {0} sur {1} (ligne {2}) + + + Ligne {0} (non sélectionnée ; {1} sélectionnés) + + + Enregistrement {0} sur {1} (aucun sélectionné) + + + Enregistrements à imprimer + + + Cochez les enregistrements à imprimer. Espace coche la ligne en surbrillance, Maj+clic coche une plage et Ctrl+A coche toutes les lignes affichées. Seuls les enregistrements cochés sont imprimés, vérifiés ou exportés. + + + N° + + + Numéro de ligne + + + Sélectionner ou désélectionner tous les enregistrements affichés + + + Sélectionner l’enregistrement {0} + + + Tout sélectionner + + + Sélectionner les enregistrements affichés : {0} + + + Tout désélectionner + + + Désélectionner les enregistrements affichés : {0} + + + Inverser la sélection + + + Inverser les enregistrements affichés : {0} + + + {0} sur {1} sélectionnés + + + Affichage de {0} enregistrements sur {1} + + + Aucun enregistrement ne correspond au filtre. Modifiez le texte ou effacez le filtre. + + + Filtre + + + Filtrer les enregistrements + + + Tapez du texte pour n’afficher que les enregistrements qui le contiennent (sans tenir compte des majuscules et des minuscules). + + + Colonne où chercher + + + Toutes les colonnes + + + N’afficher que la sélection + + + Effacer le filtre + + + Imprimer les {0} étiquettes sélectionnées + + + Imprimer l’étiquette sélectionnée + + + Imprimer les {0} étiquettes + + + Aucun enregistrement n’est sélectionné ; il n’y a donc rien à imprimer, à vérifier ni à exporter. Sélectionnez d’abord des enregistrements dans l’onglet Données. + + + Aucun enregistrement n’est sélectionné. Sélectionnez au moins un enregistrement dans l’onglet Données. + + + Envoyer les {0} enregistrements sélectionnés (sur {3}) à {1} ? Cela représente {2} étiquettes, copies comprises. L’impression commencera immédiatement. + + + Envoyer {0} enregistrements à {1} ? Cela représente {2} étiquettes, copies comprises. L’impression commencera immédiatement. + + + Afficher cet enregistrement dans la zone de conception + + + Chaque variable de l’étiquette prend sa valeur dans la colonne du même nom. Choisissez une autre colonne ici si les noms ne correspondent pas. + + + L’étiquette n’a encore aucune variable. Ajoutez un texte contenant {{nom}} (ou un champ de données) et elle apparaîtra ici. + + + Même nom (automatique) + + + Remplie par la colonne {0} + + + Aucune colonne correspondante : choisissez-en une + + + Remplie par le compteur + + + Aucun compteur portant ce nom : ajoutez-en un dans l’onglet Compteurs + + + Points à vérifier + + + Ajouter un compteur + + + Retirer + + + Aucun compteur pour l’instant. Un compteur produit un numéro de série comme 0001, 0002, 0003 pour chaque enregistrement. + + + (sans nom) + + + Nom + + + Tapez ceci dans l’étiquette + + + Compte avec + + + Commencer à + + + Pas + + + Remplissage par des zéros (chiffres) + + + Préfixe + + + Suffixe + + + Quand le compteur est épuisé + + + Premières valeurs + + + Réglages du compteur : {0}. + + + Le pas doit être un nombre entier, par exemple 1 ou -1. + + + Le remplissage par des zéros doit être un nombre entier de 0 à 18. + + + Copies de chaque enregistrement + + + Le nombre de copies provient de + + + La quantité propre à l’étiquette + + + Le même nombre pour tous les enregistrements + + + Une colonne des données + + + Copies + + + Colonne contenant les copies + + + Le nombre de copies doit être un entier de 1 à 99999999. + + + Façon d’écrire les copies + + + Une quantité d’impression (^PQ) est plus compacte et plus rapide. Répéter l’étiquette n’est utile que si votre imprimante ignore ^PQ. + + + Tâches et problèmes + + + Un enregistrement qui contient des erreurs + + + Enregistrements par tâche d’impression (0 = une seule tâche) + + + Compté en enregistrements, et non en copies. Des tâches plus petites ménagent l’imprimante et permettent de s’arrêter en cours de route. + + + Le nombre d’enregistrements par tâche doit être un entier de 0 à 1000000. + + + Vérifier et imprimer les enregistrements sélectionnés + + + Vérifiez d’abord quels enregistrements sélectionnés posent problème. L’impression envoie chaque enregistrement sélectionné et valide à l’imprimante de gauche, dans l’ordre des données. Choisissez les enregistrements dans l’onglet Données. + + + Vérifier les enregistrements sélectionnés + + + Exporter le ZPL... + + + Tout imprimer + + + Écrire un fichier par tâche lors de l’exportation + + + Vérification de tous les enregistrements... + + + Envoi des enregistrements à l’imprimante... + + + Écriture du ZPL... + + + {0} enregistrements vérifiés : {1} prêts, {2} avec des erreurs. + + + Affichage des {0} premiers problèmes sur {1}. + + + Les {0} enregistrements sont prêts à imprimer. + + + {0} enregistrements contiennent des erreurs et seraient exclus. Consultez la liste pour chaque problème et corrigez les données ou l’étiquette. + + + Imprimer tous les enregistrements + + + Envoyer {0} enregistrements à {1} ? L’impression commencera immédiatement. + + + {0} étiquettes envoyées à l’imprimante en {1} tâche(s). + + + {0} étiquettes écrites dans un fichier. + + + {0} étiquettes écrites dans {1} fichiers. + + + Remplacer les fichiers existants ? + + + {0} fichier(s) portant les mêmes noms existent déjà et seront remplacés. + + + {0} tâche(s) avaient déjà été envoyées avant l’arrêt ; ne les imprimez pas de nouveau. + + + Arrêté. {0} tâche(s) avaient déjà été envoyées à l’imprimante. + + + {0} enregistrement(s) ont été exclus en raison d’erreurs ; consultez la liste. + + + L’impression s’est arrêtée à l’enregistrement {0} en raison du réglage « s’arrêter ». + + + Aucune étiquette n’a été produite, car tous les enregistrements contiennent des erreurs. Vérifiez les enregistrements et l’étiquette. + + + Problèmes par enregistrement + + + Enregistrement + + + Type + + + Ce qui s’est passé + + + Erreur + + + Avertissement + + + Ce fichier d’étiquette indiquait un fichier de données dans un emplacement que Rotula ne suit pas automatiquement (un chemin réseau, une adresse Web ou un chemin relatif) ; il a donc été ignoré pour votre sécurité. Choisissez « Ouvrir un fichier de données » pour sélectionner le fichier vous-même. + + + Le fichier de données mémorisé ({0}) ne se trouve pas sur un disque local ; Rotula ne l’ouvrira donc pas automatiquement. Choisissez « Ouvrir un fichier de données » pour le sélectionner vous-même. + + + Le fichier de données {0} est introuvable. Il a peut-être été déplacé ou renommé ; choisissez « Ouvrir un fichier de données » pour le retrouver. + + + Chemin du fichier de données mémorisé + + + Réduire + + + Agrandir + + + Restaurer + + + Fermer + + + Étiquette + + + Propriétés de l’étiquette + + + Imprimante + + + Actualiser la liste des imprimantes + + + Fusion de données... + + + Ouvrir un fichier de données... + + + Zoom + + + Étiquette + + + Imprimante + + + Choisissez l’imprimante de cette étiquette. La configuration de l’imprimante se trouve sous Imprimante > Imprimer. + + + Sélectionner une imprimante + + + Aperçu + + + Afficher l’étiquette en plus grand, telle qu’elle sera dessinée + + + Imprimer + + + Imprimer... (Ctrl+P) + + + Sélectionner + + + Redimensionner le panneau des propriétés + + + Objets + + + Type de l’objet sélectionné + + + SORTIE ZPL + + + Exporter en .zpl + + + Format, résolution et type de l’étiquette + + + {0} objets + + + 1 objet + + + aucune sélection + + + {0} sélectionné(s) + + + Aperçu de l’étiquette + + + Aperçu - {0} + + + Afficher le code ZPL + + + Afficher ou masquer le panneau du code ZPL + + + 1 erreur + + + {0} erreurs + + + 1 avertissement + + + {0} avertissements + + + Aperçu approximatif + + + Aucun objet pour l’instant + + + Choisissez un outil à gauche pour ajouter du texte, un code à barres ou une forme. + + + Format personnalisé... + + + Formats courants + + + Résolution d’impression + + + Choisissez le format de l’étiquette et la résolution d’impression + + + le format de l’étiquette + + + Format, marges et valeurs par défaut de l’imprimante + + + Rien n’est sélectionné ; voici donc les réglages de l’étiquette entière. Cliquez sur un objet dans la zone de conception ou dans la liste pour le modifier. + + + {0} au total + + + Largeur ({0}) + + + Hauteur ({0}) + + + mm + + + po + + + points + + + Résolution de {0} ppp détectée dans le pilote de l’imprimante + + + La résolution n’a pas pu être lue dans le pilote ; choisissez-la manuellement. + + + Lecture du pilote de l’imprimante... + + + Utiliser le format de l’imprimante ({0}) + + + Remplit la largeur et la hauteur de l’étiquette avec le format de papier indiqué par le pilote de l’imprimante + + + Cette étiquette a été conçue à {0} ppp. Elle sera convertie automatiquement à {1} ppp pour s’imprimer au même format. + + + Passer l’étiquette à {0} ppp + + + Convertit la conception ouverte à la résolution de l’imprimante. Vous pouvez annuler avec Ctrl+Z. + + + Points à vérifier après la conversion ({0}) + + + Une partie de l’étiquette n’a pas pu être convertie exactement. Vérifiez le résultat imprimé, ou passez l’étiquette à la résolution de l’imprimante et révisez-la. + + + Le code à barres « {0} » a une largeur de barre de {1} points, qui devient {2} points à la nouvelle résolution. Le code est légèrement plus large ou plus étroit qu’avant. Vérifiez qu’il se lit toujours. + + + Dans « {0} », la valeur « {1} » donnerait {2} points à la nouvelle résolution, mais l’imprimante accepte {3}. Elle a été fixée à {3}. Vérifiez l’élément. + + + « {0} » contient du ZPL brut, qui ne peut pas être redimensionné automatiquement. Il conserve ses positions d’origine en points et pourrait s’imprimer plus petit ou plus grand que le reste. Modifiez-le ou remplacez-le par des éléments ordinaires. + + + Le graphique stocké « {0} » est une image matricielle fixe et ne peut pas être redimensionné pour la nouvelle résolution. Il s’imprimera à une taille physique différente. Remplacez-le par un graphique conçu pour cette résolution. + + + Cette étiquette contient un élément qui ne peut pas être converti automatiquement de {0} ppp à {1} ppp. Rien n’a été modifié. + + + Impossible de changer l’étiquette + + + OK + + + Enregistrer + + + Ne pas enregistrer + + + Information + + + Avertissement + + + Erreur + + + Question + + + À propos de Rotula + + + Version {0} + + + Logiciel de conception d’étiquettes et de codes à barres pour Windows (ZPL), gratuit et à code source ouvert (MIT) + + + Fonctionne hors ligne. Aucune télémétrie, aucun accès au réseau. + + + L’aperçu à l’écran est approximatif : le texte est dessiné avec une police de remplacement ; la forme des lettres et les retours à la ligne peuvent donc différer légèrement sur l’étiquette imprimée. + + + https://github.com/P4Software/Rotula + + + Ouvrir la page du projet dans votre navigateur Web + + + Le navigateur Web n’a pas pu être ouvert. La page du projet est {0} + + + Avis de tiers + + + Avis de tiers + + + Le fichier des avis ne fait pas partie de cette version. Il est offert à https://github.com/P4Software/Rotula, dans THIRD-PARTY-NOTICES.md. + + + Bienvenue dans Rotula + + + Concevez une étiquette, vérifiez-la à l’écran et imprimez-la sur votre imprimante compatible ZPL. + + + Nouvelle étiquette + + + Choisissez un format et commencez la conception. + + + Ouvrir une étiquette + + + Reprenez un fichier .rotula enregistré. + + + Importer du ZPL + + + Transformez un fichier ZPL existant en conception modifiable. + + + Étiquettes récentes + + + Les étiquettes que vous enregistrez ou ouvrez apparaîtront ici. + + + Commencer avec une étiquette vierge + + + Astuce : faites glisser les coins pour redimensionner, utilisez les touches fléchées pour déplacer et Ctrl+Z pour annuler n’importe quelle étape. + + + Écran d’accueil + + + Fichiers de base de données + + + Choisir un fichier de base de données SQLite + + + Données provenant de + + + Fichier (CSV ou Excel) + + + Base de données + + + Base de données + + + Base de données : aucune donnée lue pour l’instant + + + Connexion à la base de données : {0} + + + {0} lignes ont été lues à partir de la connexion à la base de données « {1} ». + + + {0} lignes, {1} colonnes + + + Connexion enregistrée + + + Nouvelle connexion... + + + Gérer les connexions... + + + Il n’y a encore aucune connexion enregistrée. Choisissez « Nouvelle connexion... » pour ajouter la première. + + + Choisissez d’abord une connexion enregistrée, ou créez-en une avec « Nouvelle connexion... ». + + + Changer la source de données... + + + Revenir aux données chargées + + + Cette étiquette utilise la connexion enregistrée « {0} ». Connectez-vous et rechargez les données. + + + Rotula ne s’est connecté à rien. Il se connecte seulement quand vous cliquez sur le bouton, et n’exécute la requête ci-dessous que si elle passe la vérification de lecture seule. + + + Requête enregistrée dans cette étiquette + + + Se connecter et recharger les données + + + Il n’y a aucune connexion enregistrée nommée « {0} » sur cet ordinateur. Ouvrez « Gérer les connexions... », ajoutez une connexion et donnez-lui exactement le nom « {0} », puis cliquez de nouveau sur le bouton. + + + Table ou vue + + + Requête + + + Rechercher des tables et des vues + + + Tables et vues + + + vue + + + Aucune table ni vue à afficher. Modifiez la recherche, ou vérifiez que ce compte peut lire la base de données. + + + Aperçu + + + L’aperçu affiche les 100 premières lignes. « Utiliser ces données » lit toutes les lignes jusqu’au maximum. + + + Choisissez une table ou une vue pour en voir les lignes. + + + Requête SELECT + + + Un seul SELECT est permis. Écrivez @nom à l’endroit où une valeur doit être insérée, par exemple WHERE Id = @id. + + + Tout semble correct : cette requête ne fait que lire des données. + + + Paramètres + + + Valeurs des @noms de votre requête. Choisissez Nombre ou Date quand la colonne n’est pas du texte. + + + Type du paramètre + + + Texte + + + Nombre + + + Date + + + La valeur de {0} n’est pas un nombre. Tapez des chiffres avec, au besoin, un point décimal, par exemple 12.5. + + + La valeur de {0} n’est pas une date. Tapez-la ainsi : 2026-03-31. + + + Exécuter + + + Exécute la requête (en lecture seule) et affiche les lignes ci-dessous. + + + Résultat + + + Détail technique fourni par la base de données + + + Utiliser ces données + + + Lit les lignes et les utilise comme enregistrements de cette étiquette. + + + Nombre maximal de lignes + + + Au plus ce nombre de lignes est lu (de 1 à 1 000 000). Un avertissement apparaît si le résultat a été tronqué. + + + Le nombre maximal de lignes doit être un entier de 1 à {0}. + + + Cette base de données ne contient aucune table ni vue visible par ce compte. + + + Lecture de la liste des tables... + + + Lecture de {0}... + + + Exécution de la requête... + + + Lecture de toutes les lignes de {0}... + + + Connexions aux bases de données + + + Rotula ne fait que lire des données. Utilisez un compte de base de données en lecture seule quand c’est possible. + + + Connexions enregistrées + + + Rien n’est encore enregistré. Remplissez le formulaire et choisissez Enregistrer. + + + Nouvelle + + + Supprimer + + + Détails de la connexion + + + Nom + + + Les étiquettes désignent une connexion par ce nom ; choisissez donc un nom court que vous conserverez. + + + Le nom ne peut pas être modifié, car des étiquettes y font référence. Pour utiliser un autre nom, créez une nouvelle connexion. + + + Tapez un nom pour cette connexion. + + + Une connexion nommée « {0} » existe déjà. Choisissez un autre nom, ou sélectionnez-la dans la liste pour la modifier. + + + Type de base de données + + + SQL Server + + + PostgreSQL + + + MySQL / MariaDB + + + Fichier SQLite + + + ODBC + + + Serveur + + + Port + + + Le port doit être un nombre entier de 1 à 65535, ou rester vide pour utiliser le port habituel. + + + Nom de la base de données + + + Fichier SQLite + + + Le fichier est ouvert en lecture seule ; Rotula ne peut donc jamais le modifier. + + + Parcourir... + + + Charger la liste + + + Lecture de la liste sur le serveur... + + + Choisissez une base de données du serveur, ou tapez son nom. + + + Remplissez d’abord l’adresse du serveur et l’ouverture de session, puis chargez la liste. Vous pouvez aussi simplement taper le nom. + + + Tapez le mot de passe pour charger la liste, ou tapez simplement le nom de la base de données. + + + Bases de données trouvées : {0}. Choisissez-en une, ou tapez son nom. + + + Le serveur contient plus de bases de données qu’il n’est possible d’en afficher ; voici les {0} premières. Choisissez-en une, ou tapez son nom. + + + Cet identifiant ne peut ouvrir aucune base de données sur le serveur. Tapez plutôt le nom. + + + Ce type de serveur ne peut pas lister ses bases de données. Tapez plutôt le nom. + + + Arrêté. Appuyez sur Charger la liste pour réessayer, ou tapez le nom. + + + Tapez de nouveau le mot de passe pour charger la liste : le mot de passe enregistré n’est utilisé que tant que le serveur et l’utilisateur restent les mêmes. + + + La liste n’a pas pu être lue : cet identifiant n’a peut-être pas le droit de voir les autres bases de données. Tapez plutôt le nom. + + + La liste n’a pas pu être lue. {0} Vous pouvez tout de même taper le nom. + + + Choisissez une source de données installée sur cet ordinateur, ou tapez son nom. + + + Aucune source de données ODBC n’est installée sur cet ordinateur. Tapez plutôt le nom. + + + système + + + Nom de la source de données ODBC (DSN) + + + ou une chaîne de connexion + + + Si la chaîne de connexion contient un mot de passe, Rotula le déplace dans le magasin protégé et le retire du texte enregistré. + + + Se connecter avec + + + Mon compte Windows + + + Nom d’utilisateur et mot de passe + + + Nom d’utilisateur + + + Mot de passe + + + Un mot de passe est enregistré pour cette connexion et n’est jamais affiché. Laissez ce champ vide pour le conserver, ou tapez-en un nouveau pour le remplacer. + + + Le mot de passe est stocké de façon protégée pour votre compte Windows seulement, et n’est plus jamais affiché. + + + Sécurité + + + Chiffrer la connexion + + + Laissez cette option activée. Désactivez-la seulement pour un serveur de confiance sur votre propre réseau. + + + Le chiffrement est désactivé : n’importe qui sur le chemin réseau peut lire les données et le mot de passe. Utilisez ce réglage seulement avec un serveur local de confiance. + + + Faire confiance au certificat de ce serveur + + + Seulement pour un serveur que vous connaissez et contrôlez, comme un serveur de test. Sinon, quelqu’un pourrait usurper l’identité du serveur. + + + Rotula ne vérifiera pas l’identité du serveur ; quelqu’un sur le réseau pourrait donc se faire passer pour lui et lire les données et le mot de passe. Utilisez ce réglage seulement avec un serveur que vous contrôlez. + + + Tester la connexion + + + La connexion fonctionne. + + + Vous avez modifié cette connexion, et le mot de passe enregistré ne peut pas être relu pour un test. Tapez de nouveau le mot de passe pour la tester. + + + Enregistrer + + + La connexion « {0} » a été enregistrée. + + + La connexion « {0} » a été supprimée. + + + Supprimer la connexion + + + Supprimer la connexion « {0} » ? Les étiquettes qui l’utilisent demanderont une connexion portant ce nom la prochaine fois qu’elles seront ouvertes. + + + Enregistrement... + + + Suppression... + + + Test de la connexion... + + + Le certificat de sécurité du serveur n’a pas pu être vérifié. S’il s’agit de votre propre serveur (par exemple un serveur de test), vous pouvez lui faire confiance pour cette connexion. Sinon, demandez un certificat valide à l’administrateur. + + + Faire confiance à ce serveur et enregistrer + + + nom + + + type de base de données + + + mode de connexion + + + serveur + + + port + + + nom de la base de données + + + nom d’utilisateur + + + fichier SQLite + + + chaîne de connexion + + + nom de la source de données + + + délai d’expiration de la commande + + + délai d’expiration de la connexion + + + Un problème est survenu lors de la lecture de la base de données ({0}). Réessayez, et vérifiez les détails de la connexion si le problème persiste. + + + Rotula n’a pas pu se connecter à la base de données. Vérifiez les détails de la connexion et que le serveur est en marche, puis réessayez. + + + Le serveur n’a pas accepté le nom d’utilisateur ou le mot de passe. Vérifiez-les dans « Gérer les connexions... » et réessayez. + + + La base de données a mis trop de temps à répondre. Réessayez, restreignez la requête ou adressez-vous à l’administrateur de la base de données. + + + Le serveur est injoignable. Vérifiez le nom du serveur et le port, votre réseau ou votre RPV, puis réessayez. + + + La base de données est introuvable. Vérifiez le nom de la base de données ou l’emplacement du fichier, puis réessayez. + + + Ce compte n’a pas le droit de lire ces données. Demandez un accès en lecture à l’administrateur de la base de données. + + + Rotula ne fait que lire des données ; cette requête n’a donc pas été exécutée. {0} + + + La base de données n’a pas pu exécuter la requête. Vérifiez les noms des tables et des colonnes ainsi que l’orthographe de la requête, puis réessayez. + + + Arrêté. Rien n’a été modifié. + + + La limite de {0} lignes dépasse le maximum de {1}. Choisissez une limite plus petite. + + + Le pilote ou la source de données ODBC n’est pas installé sur cet ordinateur. Installez-le, ou choisissez un autre nom de source de données. + + + Le certificat de sécurité du serveur n’est pas approuvé. S’il s’agit de votre propre serveur, ouvrez « Gérer les connexions... » et choisissez de lui faire confiance pour cette connexion. Sinon, demandez un certificat valide à l’administrateur. + + + Le réglage « {0} » est absent ou non permis. Corrigez-le dans « Gérer les connexions... » et réessayez. + + + Il n’y a aucune connexion enregistrée nommée « {0} ». Créez-la avec exactement ce nom dans « Gérer les connexions... ». + + + Une seule requête SELECT est permise. Réécrivez-la sous forme de SELECT. + + + Une seule instruction est permise. Retirez tout ce qui suit le premier point-virgule. + + + Tapez d’abord une requête SELECT. + + + Le mot « {0} » n’est pas permis, car il peut modifier des données. S’il s’agit d’un nom de colonne, mettez-le entre guillemets. + + + SELECT ... INTO n’est pas permis, car il crée des tables ou des fichiers. Retirez la partie INTO. + + + Le verrouillage de lignes (FOR UPDATE, FOR SHARE, LOCK IN SHARE MODE) n’est pas permis. Retirez cette partie. + + + La fonction « {0} » n’est pas permise, car elle peut lire des fichiers, joindre d’autres serveurs ou exécuter du code. Retirez-la. + + + Un commentaire qui commence par /*! s’exécute comme du SQL dans MySQL ; il n’est donc pas permis. Retirez-le. + + + Un guillemet ou un commentaire n’est pas fermé. Fermez-le et réessayez. + + + La requête est trop longue. Raccourcissez-la. + + + Le paramètre @{0} n’a pas de valeur. Remplissez-le sous Paramètres. + + + Seules les {0} premières lignes ont été lues. Augmentez le nombre maximal de lignes ou restreignez la requête pour lire le reste. + + + Le fichier des connexions enregistrées est endommagé et a été ignoré. Ajoutez de nouveau vos connexions ; le fichier endommagé est conservé à côté avec l’extension .damaged. + + + Vous avez modifié l’endroit ou la façon dont cette connexion ouvre une session ; le mot de passe enregistré n’a donc pas été utilisé. Tapez de nouveau le mot de passe. + + + Le type de source de données « {0} » n’est pas disponible. Son module complémentaire est peut-être absent ou désactivé. + + + Cette requête peut se lire de plusieurs façons (par exemple une barre oblique inverse devant un guillemet) ; elle n’a donc pas été exécutée. Utilisez deux apostrophes ('') au lieu de la barre oblique inverse. + + + fournisseur + + + service + + + fichier de l’autorité de certification + + + options supplémentaires + + + Le fichier des connexions enregistrées est trop volumineux et a été ignoré. Ajoutez de nouveau vos connexions. + + + Le mot de passe enregistré ne peut pas être lu. Il a probablement été enregistré par un autre utilisateur Windows ou sur un autre ordinateur. Ouvrez « Gérer les connexions... » et tapez-le de nouveau. + + + Rotula n’a pas pu enregistrer les connexions. Vérifiez que vous pouvez écrire dans votre dossier utilisateur (AppData) et réessayez. + + + La protection des mots de passe n’est pas offerte sur ce système ; les connexions qui exigent un mot de passe ne peuvent donc pas être enregistrées. Connectez-vous avec votre compte Windows, ou utilisez un fichier SQLite. + + + chiffrement + + + faire confiance au certificat du serveur + + + Code 11 + + + 2 parmi 5 industriel + + + 2 parmi 5 standard + + + LOGMARS + + + Codabar + + + MSI + + + Plessey + + + EAN-8 + + + UPC-E + + + Extension UPC/EAN + + + POSTNET + + + PLANET + + + GS1 DataBar + + + Code Aztec + + + MicroPDF417 + + + Codablock + + + MaxiCode + + + TLC39 + + + Code 49 + + + Omnidirectionnel + + + Tronqué + + + Empilé + + + Empilé omnidirectionnel + + + Limité + + + Étendu + + + Aucun + + + Un chiffre modulo 10 + + + Deux chiffres modulo 10 + + + Chiffres modulo 11 et modulo 10 + + + Codablock A (caractères du Code 39) + + + Codablock E (Code 128, FNC1 automatique) + + + Codablock F (Code 128) + + + Linéaires + + + Matriciels 2D + + + Empilés + + + Postaux + + + Chiffres de contrôle (1 ou 2) + + + Chiffres de contrôle + + + Afficher les chiffres de contrôle dans le texte + + + Caractère de début + + + Caractère de fin + + + Type de GS1 DataBar + + + Hauteur du séparateur (1-2) + + + Segments par rangée (2-22) + + + Les données contiennent des codes ECI + + + Correction d’erreurs / taille + + + Symbole de menu (configuration du lecteur) + + + Symboles de l’ensemble + + + Identifiant de l’ensemble (facultatif) + + + Mode (0-33) + + + Type de Codablock + + + Ajouter des caractères de contrôle + + + Mode (2-6) + + + Numéro du symbole (1-8) + + + Nombre total de symboles (1-8) + + + Module MicroPDF417 (points) + + + Rangée MicroPDF417 (points) + + + Mode de départ + + + Caractères par rangée + + + Automatique + + + Rotation (impossible à imprimer pour ce code à barres : choisissez Valeur de l’étiquette) + + + À propos de l’aperçu + + + L’imprimante dessine ce code à barres ; l’aperçu affiche un espace réservé. + + + Ce symbole Aztec (Rune, menu, ajout structuré ou ECI) est dessiné par l’imprimante ; l’aperçu affiche un espace réservé. + + + Les symboles Rune, de menu, d’ajout structuré et ECI sont dessinés par l’imprimante ; dans ces cas, l’aperçu affiche un espace réservé. + + + Dans l’aperçu, le rapport entre barres larges et étroites est dessiné à 2:1. + + + Dans l’aperçu, le rapport entre barres larges et étroites est dessiné à 3:1. + + + Dans l’aperçu, la largeur des barres et des espaces suit la largeur du module ; l’imprimante réelle utilise les dimensions fixes de l’USPS. + + + La partie composite 2D du GS1 DataBar n’est pas prise en charge ; dans les types empilés, la hauteur des barres est celle du symbole entier. + + + Le Code 11 n’accepte que des chiffres et le trait d’union (-). Retirez tout autre caractère. + + + Le Code 11 utilise 1 ou 2 chiffres de contrôle. Choisissez l’une de ces deux valeurs. + + + Le LOGMARS n’accepte que les lettres de A à Z, les chiffres, l’espace et - . $ / + %. Retirez tout autre caractère. + + + L’imprimante imprime toujours le texte sous un code LOGMARS ; le désactiver n’a donc aucun effet. Activez le texte pour qu’il corresponde à ce qui sera imprimé. + + + Le Codabar n’accepte que des chiffres et - $ : / . + dans les données. Les lettres de début et de fin (A à D) se choisissent dans les options ; elles ne se tapent pas dans les données. + + + Les caractères de début et de fin du Codabar doivent chacun être A, B, C ou D. Choisissez l’un d’eux. + + + Les données MSI peuvent contenir au plus {0} chiffres avec ce réglage de chiffre de contrôle. Raccourcissez les données. + + + Choisissez l’une des options de chiffre de contrôle MSI. + + + Le Plessey n’accepte que les chiffres de 0 à 9 et les lettres majuscules de A à F. Retirez tout autre caractère. + + + L’EAN-8 nécessite {0} chiffres (le chiffre de contrôle est ajouté pour vous) ou {1} chiffres, chiffre de contrôle compris. + + + Le dernier chiffre n’est pas le bon chiffre de contrôle EAN-8. Il devrait être {0} ; sinon, retirez le dernier chiffre et laissez l’imprimante l’ajouter. + + + L’UPC-E nécessite le code fabricant et produit de 10 chiffres que l’imprimante raccourcit, ou le code UPC-E de 6 chiffres (éventuellement précédé du système de numérotation 0 et suivi d’un chiffre de contrôle). Modifiez le nombre de chiffres. + + + L’UPC-E ne peut utiliser que le système de numérotation 0. Commencez le code par 0. + + + Ces chiffres ne peuvent pas être raccourcis en code UPC-E, car l’UPC-E exige des zéros à des positions précises. Modifiez les chiffres ou utilisez plutôt l’UPC-A. + + + Le dernier chiffre n’est pas le bon chiffre de contrôle UPC-E. Il devrait être {0} ; sinon, retirez le dernier chiffre et laissez l’imprimante l’ajouter. + + + Une extension UPC/EAN nécessite exactement 2 ou 5 chiffres. + + + Les données POSTNET doivent comporter l’un de ces nombres de chiffres : {0}. L’imprimante ajoute le chiffre de contrôle. + + + Les données PLANET doivent comporter l’un de ces nombres de chiffres : {0}. L’imprimante ajoute le chiffre de contrôle. + + + Choisissez l’un des types de GS1 DataBar. + + + Ce type de GS1 DataBar nécessite jusqu’à {0} chiffres (des zéros sont ajoutés devant) ou {1} chiffres, chiffre de contrôle compris. Raccourcissez les données. + + + Le dernier chiffre n’est pas le bon chiffre de contrôle GS1. Il devrait être {0} ; sinon, retirez le dernier chiffre et laissez l’imprimante l’ajouter. + + + Les données composites (2D) après un | ne sont pas prises en charge. Retirez le | et tout ce qui le suit. + + + Le GS1 DataBar Expanded nécessite des chaînes d’éléments GS1 dont les identifiants sont entre parenthèses, par exemple (01)12345678901231(10)ABC. Vérifiez les parenthèses et les identifiants. + + + Une chaîne d’éléments GS1 ne peut contenir que des caractères standard du clavier. Retirez les accents et les symboles spéciaux. + + + La chaîne d’éléments GS1 est trop longue (maximum {0} caractères). Raccourcissez-la. + + + L’élément (01) nécessite un GTIN de 14 chiffres composé uniquement de chiffres. Corrigez les chiffres entre parenthèses. + + + Le GS1 DataBar Limited ne peut encoder que des nombres jusqu’à 1999999999999 ; le premier de ses 13 chiffres doit donc être 0 ou 1. Modifiez le nombre ou choisissez un autre type. + + + Le nombre de segments par rangée doit être un nombre pair compris entre {0} et {1}. + + + La hauteur du séparateur doit être comprise entre {0} et {1} modules. + + + La valeur de correction d’erreurs Aztec n’est pas valide. Utilisez 0 (automatique), 1 à 99 (pourcentage), 101 à 104 (format compact), 201 à 232 (format complet) ou 300 (Rune). + + + Le nombre de symboles d’un ensemble à ajout structuré doit être compris entre {0} et {1}. + + + L’identifiant de l’ensemble peut comporter au plus {0} caractères. Raccourcissez-le. + + + L’identifiant de l’ensemble ne peut contenir que des caractères standard du clavier, à l’exception de la virgule, de ^ et de ~. Retirez-les. + + + Les données Aztec sont trop longues (maximum {0} caractères). Raccourcissez le texte. + + + Le mode MicroPDF417 doit être compris entre {0} et {1}. + + + Les données MicroPDF417 sont trop longues (maximum {0} caractères). Raccourcissez le texte. + + + Choisissez Codablock A, E ou F. + + + Le Codablock A n’accepte que les lettres majuscules, les chiffres et - . espace $ / + %. Modifiez les données ou choisissez Codablock E ou F. + + + Les Codablock E et F ne peuvent encoder que les caractères standard du clavier. Retirez les accents et les symboles spéciaux. + + + Le nombre de caractères par rangée doit être 0 (automatique) ou compris entre {0} et {1}. + + + Pour ce type de Codablock, le nombre de rangées doit être 0 (automatique) ou compris entre {0} et {1}. + + + Le mode MaxiCode doit être compris entre {0} et {1}. + + + Le nombre total de symboles MaxiCode doit être compris entre {0} et {1}, et le numéro de ce symbole doit être compris entre 1 et ce total. + + + Le mode {0} de MaxiCode nécessite un message de transporteur : une classe de service de 3 chiffres, un code de pays de 3 chiffres, puis le code postal (9 chiffres en mode 2, 6 lettres ou chiffres en mode 3). Complétez le début des données. + + + Les données MaxiCode sont trop longues (maximum {0} caractères). Raccourcissez le texte. + + + Le MaxiCode ne peut pas être pivoté en ZPL ; la rotation définie pour cet élément ne sera donc pas imprimée. Rétablissez la rotation par défaut de l’étiquette. + + + Les données TLC39 commencent par un numéro ECI de 6 chiffres, éventuellement suivi d’une virgule, du numéro de série et d’autres champs. Corrigez le début des données. + + + Chaque champ TLC39 après le numéro ECI ne peut contenir que des lettres et des chiffres, jusqu’à {0} caractères. Vérifiez les champs entre les virgules. + + + Les données TLC39 sont trop longues (maximum {0} caractères). Raccourcissez le texte. + + + La largeur du module MicroPDF417 doit être comprise entre {0} et {1} points. + + + La hauteur de rangée MicroPDF417 doit être comprise entre {0} et {1} points. + + + Le Code 49 ne peut encoder que les caractères standard du clavier. Retirez les accents et les symboles spéciaux. + + + Le mode de départ du Code 49 doit être automatique ou un nombre de 0 à 5. Choisissez l’une de ces valeurs. + + + Les données Code 49 sont trop longues (maximum {0} caractères). Raccourcissez le texte. + + + Ce GS1 DataBar utilise le type de symbologie {0}, que Rotula ne prend pas en charge (seulement les types 1 à 6). Il a été conservé en texte ZPL et s’imprimera quand même, mais il ne peut pas être modifié. + + + Ce GS1 DataBar comporte une partie composite (2D), que Rotula ne prend pas en charge. Il a été conservé en texte ZPL et s’imprimera quand même, mais il ne peut pas être modifié. + + + Ce code à barres postal utilise le type {0}, que Rotula ne prend pas en charge (seulement POSTNET et PLANET). Il a été conservé en texte ZPL et s’imprimera quand même, mais il ne peut pas être modifié. + + + Nombres pairs seulement. Sous 22, le symbole est empilé sur plusieurs rangées. + + + 0 = automatique. 1 à 99 = correction d’erreurs minimale en pourcentage. 101 à 104 = symbole compact de 1 à 4 couches. 201 à 232 = symbole complet de 1 à 32 couches. 300 = Aztec Rune. + + + 1 = un seul symbole. De 2 à 26, le message est réparti sur ce nombre de symboles. + + + 0 = automatique. + + + Les modes 2 et 3 transportent un message de transporteur structuré (classe de service, pays, code postal) ; les modes 4 à 6 transportent n’importe quel texte. + + + Le mode fixe le nombre de colonnes et de rangées ainsi que la correction d’erreurs (voir le guide ZPL). + + + Un compteur d’imprimante nécessite une valeur de départ de 1 à 40 caractères composée de lettres, de chiffres, d’espaces et de - / . avec au moins un chiffre. Modifiez le texte de ce champ. + + + L’imprimante ne peut pas incrémenter un code QR, car ses données commencent par un en-tête. Désactivez le compteur d’imprimante pour ce code à barres. + + + Le masque du compteur ne peut utiliser que les lettres D, H, O, A, N et %, et le pas, que des lettres, des chiffres et %. Corrigez le masque ou le pas. + + + Le caractère qui marque les références aux numéros de champ doit être un seul caractère standard du clavier, autre que ^ ou ~. Choisissez-en un autre. + + + Les caractères marqueurs de l’horloge doivent être des caractères standard du clavier, différents les uns des autres, autres que ^, ~ ou la virgule. Modifiez-les. + + + Le numéro de champ doit être compris entre {0} et {1}. + + + L’invite du champ ne peut contenir que des lettres, des chiffres et des espaces, jusqu’à 255 caractères. Modifiez l’invite. + + + Ellipse + + + Ligne diagonale + + + Symbole + + + Ellipse + + + Ligne diagonale + + + Symbole + + + Symbole + + + Inclinaison + + + Symbole + + + Hauteur du caractère (points) + + + Largeur du caractère (points) + + + Vers la droite (/ du coin inférieur gauche au coin supérieur droit) + + + Vers la gauche (\ du coin supérieur gauche au coin inférieur droit) + + + Marque déposée (R) + + + Droit d’auteur (C) + + + Marque de commerce (TM) + + + Underwriters Laboratories (UL) + + + Association canadienne de normalisation (CSA) + + + Horizontale + + + Verticale (de haut en bas) + + + Inversée (de droite à gauche) + + + Le décalage de l’étiquette doit être compris entre {0} et {1} points. Modifiez le décalage dans les réglages de l’étiquette. + + + Le haut de l’étiquette doit être compris entre {0} et {1} rangées de points. Modifiez-le dans les réglages de l’étiquette. + + + La modification du contraste doit être comprise entre {0} et {1}. Choisissez une valeur dans cette plage. + + + Les vitesses d’impression doivent être comprises entre {0} et {1} pouces par seconde. Réglez les vitesses d’impression, d’avance et de recul à des valeurs dans cette plage. + + + La police {0} ne peut pas utiliser le fichier « {1} ». Un identifiant de police est une lettre ou un chiffre, et le fichier doit avoir la forme E:NOM.TTF. Corrigez l’identifiant de police ou le nom du fichier. + + + Le graphique stocké {0} ne peut pas être utilisé. Son nom doit avoir la forme R:LOGO.GRF et ses données ne doivent pas dépasser 4 Mo. Corrigez le nom ou utilisez un graphique plus petit. + + + La largeur, la hauteur et l’épaisseur d’une ligne diagonale doivent être comprises entre {0} et {1} points. Modifiez la taille. + + + La largeur, la hauteur et l’épaisseur d’une ellipse doivent être comprises entre {0} et {1} points. Modifiez la taille. + + + La hauteur et la largeur d’un symbole doivent être comprises entre {0} et {1} points. Modifiez la taille. + + + La largeur et la hauteur d’une zone de texte doivent être comprises entre {0} et {1} points. Modifiez la taille de la zone. + + + Un champ de texte peut être un bloc avec retour à la ligne ou une zone de texte, mais pas les deux. Désactivez l’un des deux. + + + L’espace entre les caractères doit être compris entre {0} et {1} points. Modifiez l’espace. + + + L’option {0} de ce champ ne peut pas être utilisée avec le champ auquel elle appartient ; le champ entier a donc été conservé en ZPL brut et s’imprimera quand même, mais il ne peut pas être modifié. + + + Ce champ contient à la fois des données et un compteur d’imprimante, qui ne peuvent pas être combinés. Il a été conservé en ZPL brut et s’imprimera quand même, mais il ne peut pas être modifié. + + + L’attribution de police {0} arrive après que certains champs ont déjà été définis ; elle ne peut donc pas devenir un réglage de l’étiquette. Elle a été conservée en ZPL brut à son emplacement d’origine. + + + Le fichier de supports « {0} » dépasse {1} octets ; il n’a donc pas été lu. Utilisez un fichier plus petit. + + + Le fichier de supports « {0} » n’a pas pu être ouvert. Vérifiez qu’il existe et que vous avez le droit de le lire. + + + Le fichier de supports « {0} » n’est pas une liste de supports valide. Vérifiez le fichier, ou restaurez-le à partir de l’original. + + + Le fichier de supports « {0} » a une version de format ({1}) que cette version de Rotula ne comprend pas. Mettez Rotula à jour ou utilisez un autre fichier. + + + Le fichier de supports « {0} » ne contient aucune entrée. Ajoutez-y au moins un support. + + + Le fichier de supports « {0} » contient plus de {1} entrées ; seules les {1} premières ont donc été lues. Divisez le fichier en fichiers plus petits. + + + Le dossier des supports contient plus de {0} fichiers ; les fichiers en trop ont donc été ignorés. Retirez les fichiers dont vous n’avez pas besoin. + + + L’élément {1} de « {0} » n’est pas une entrée de support ; il a donc été ignoré. + + + L’entrée {1} de « {0} » n’a pas d’identifiant ; elle a donc été ignorée. Donnez un identifiant unique à chaque entrée. + + + L’entrée {1} de « {0} » a l’identifiant « {2} », qui est trop long ou contient des caractères non permis ; elle a donc été ignorée. Utilisez des lettres, des chiffres, des points, des traits de soulignement et des traits d’union, jusqu’à 64 caractères. + + + L’identifiant « {1} » apparaît deux fois dans « {0} » ; la deuxième entrée a donc été ignorée. Donnez un identifiant unique à chaque entrée. + + + Le support « {0} » n’a pas de nom ; il a donc été ignoré. Donnez-lui un nom. + + + Le champ « {1} » du support « {0} » est du mauvais type ou est trop long ; le support a donc été ignoré. Corrigez le champ. + + + Le support « {0} » n’a pas de largeur ni de hauteur utilisables ; il a donc été ignoré. Ajoutez le format de l’étiquette. + + + La valeur {1} du support « {0} » ({2}) se situe hors des formats qu’une imprimante d’étiquettes peut utiliser ; le support a donc été ignoré. Vérifiez le nombre et son unité. + + + La valeur {1} du support « {0} » est indiquée comme {2} mm et {3} po, qui ne correspondent pas à la même longueur ; le support a donc été ignoré. Corrigez l’une des deux valeurs. + + + Le champ « {1} » du support « {0} » a la valeur {2}, qui est hors de la plage permise ; le support a donc été ignoré. Corrigez la valeur. + + + L’adresse source du support « {0} » n’est pas une adresse https ; le support a donc été ignoré. Utilisez une adresse qui commence par https://. + + + Le champ « {1} » du support « {0} » ressemble à un prix. Rotula ne conserve pas les prix ; le champ a donc été ignoré. + + + Le support « {0} » est marqué comme vérifié, mais il lui manque {1}. Ajoutez cette information ou marquez le support comme non vérifié. + + + Le support « {0} » n’indique pas d’où proviennent ses données. Ajoutez un identifiant de source ou une adresse source https. + + + Le bloc de description du fichier de supports est incomplet : {0} est absent. Ajoutez cette information. + + + Le support « {0} » cite la source « {1} », que le fichier ne décrit pas. Ajoutez cette source au bloc de description. + + + Le fichier indique que {0} entrées sont vérifiées, mais il y en a {1}. Mettez à jour les totaux dans le bloc de description. + + + Le format du support « {0} » n’a pas été vérifié. Mesurez les étiquettes réelles avant d’imprimer un grand tirage. + + + L’espace entre les étiquettes du support « {0} » est inconnu. Étalonnez l’imprimante avec ce support chargé. + + + Le support « {0} » est un rouleau continu sans hauteur d’étiquette fixe ; la hauteur de l’étiquette a donc été laissée telle quelle. Indiquez la hauteur dont vous avez besoin. + + + Ce support comporte {0} étiquettes dans la largeur et {1} dans la longueur. Pour l’instant, une seule étiquette est disposée ; placez donc vous-même les autres étiquettes au besoin. + + + Le support « {0} » est une feuille prédécoupée. Le format de l’étiquette a été appliqué, mais pas la disposition de la feuille. + + + L’étiquette a déjà du contenu ; sa rotation par défaut des champs n’a donc pas été modifiée pour le support « {0} ». Modifiez vous-même la rotation si le support l’exige. + + + Le support « {0} » est rond ou ovale, mais l’étiquette de l’imprimante est rectangulaire et la forme n’est pas dessinée. Gardez votre contenu à l’intérieur de la forme. + + + {0} éléments se trouvent déjà sur l’étiquette et pourraient maintenant dépasser du nouveau format. Vérifiez qu’ils tiennent toujours. + + + Les fichiers de type « {0} » ne peuvent pas être importés. Enregistrez la liste en .csv ou en .xlsx et réessayez. + + + La liste dépasse {0} octets et n’a pas été lue. Utilisez un fichier plus petit. + + + La liste « {0} » n’a pas pu être lue. Vérifiez qu’elle n’est pas ouverte dans un autre programme et qu’il s’agit d’un fichier valide. + + + La liste ne contient aucune ligne de données. Ajoutez les supports sous la ligne d’en-tête. + + + La liste contient plus de {0} lignes ; seules les {0} premières ont donc été lues. Divisez-la en listes plus petites. + + + Aucune colonne de numéros de pièce n’a été trouvée ; rien n’a donc été importé. Ajoutez une colonne nommée « Part number ». + + + Aucune colonne de formats d’étiquette n’a été trouvée ; rien n’a donc été importé. Ajoutez une colonne de format, ou une colonne de largeur et une colonne de hauteur. + + + La colonne « {0} » ressemble à un prix et a été ignorée. Rotula ne conserve pas les prix. + + + La colonne {0} a été choisie d’après son contenu, car aucun en-tête ne correspondait : « {1} ». Vérifiez que c’est la bonne. + + + La ligne {0} n’a pas de numéro de pièce ; elle a donc été ignorée. Ajoutez le numéro de pièce. + + + La ligne {0} ({1}) n’a aucun format lisible ; elle a donc été ignorée. Écrivez le format sous la forme largeur x hauteur, avec une unité. + + + La ligne {0} ({1}) donne le format « {2} » sans préciser s’il s’agit de pouces ou de millimètres ; elle a donc été ignorée. Ajoutez « in » ou « mm ». + + + La ligne {0} ({1}) indique deux formats qui ne concordent pas (« {2} ») ; elle a donc été ignorée. Ne gardez que le bon. + + + La ligne {0} ({1}) a le format « {2} », qui se situe hors des formats qu’une imprimante d’étiquettes peut utiliser ; elle a donc été ignorée. Vérifiez le nombre et son unité. + + + La ligne {0} répète le numéro de pièce {1} ; elle a donc été ignorée. Gardez une seule ligne par numéro de pièce. + + + Adresse du serveur + + + Le nom ou l’adresse IP du serveur de base de données, par exemple db.exemple.com. Votre administrateur de base de données peut vous l’indiquer. + + + Port + + + Nom de la base de données + + + Nom d’utilisateur + + + Fichier de base de données + + + Nom de la source de données (DSN) + + + Chaîne de connexion + + + Chiffrer la connexion + + + Laissez cette option activée. Désactivez-la seulement pour un serveur de confiance sur votre propre réseau. + + + Faire confiance au certificat de ce serveur + + + Seulement pour un serveur que vous connaissez et contrôlez, comme un serveur de test. Sinon, quelqu’un pourrait usurper l’identité du serveur. + + + Fichier de l’autorité de certification (facultatif) + + + Le serveur se trouve derrière un regroupeur de connexions + + + Famille du serveur + + + MySQL + + + MariaDB + + + Base de données derrière le pilote + + + Si vous savez avec quelle base de données le pilote ODBC communique, choisissez-la pour que Rotula applique les règles de sécurité de cette base de données. Les vérifications ne sont jamais assouplies. + + + Je ne sais pas + + + SQL Server + + + PostgreSQL + + + MySQL + + + MariaDB + + + SQLite + + + Oracle + + + DuckDB + + + BigQuery + + + Snowflake + + + ClickHouse + + + Informix + + + IBM Db2 + + + Firebird + + + Microsoft Access / Excel (Jet, ACE) + + + Si le certificat du serveur provient de votre propre organisation, choisissez ici le fichier de l’autorité de certification. Votre administrateur peut vous le fournir. + + + Ce service n’accepte que la connexion Microsoft Entra, qui nécessite un module complémentaire qui n’est pas encore installé. Vous ne pouvez pas vous y connecter avec un nom d’utilisateur et un mot de passe. + + + Le pilote de ce type de fichier est introuvable sur cet ordinateur. Installez le « Microsoft Access Database Engine » 64 bits de Microsoft, puis redémarrez Rotula. + + + Ce serveur est un regroupeur de connexions. Si quelque chose ne fonctionne pas, essayez plutôt l’adresse de connexion directe. + + + Utilisez un compte de base de données qui peut seulement lire (SELECT). Cette source de données ne peut pas rendre elle-même une session en lecture seule ; le compte est donc la véritable protection. + + + MariaDB + + + Amazon RDS pour MySQL + + + Amazon Aurora (MySQL) + + + Amazon RDS pour PostgreSQL + + + Amazon Aurora (PostgreSQL) + + + Amazon Redshift + + + CockroachDB + + + YugabyteDB (YSQL) + + + Supabase (connexion directe) + + + Supabase (regroupeur de connexions) + + + Neon (connexion directe) + + + Neon (regroupeur de connexions) + + + PlanetScale + + + TiDB + + + SingleStore + + + Azure SQL Database + + + Azure Synapse Analytics + + + Microsoft Fabric + + + Google Cloud SQL pour MySQL + + + Google Cloud SQL pour PostgreSQL + + + Google Cloud SQL pour SQL Server + + + Microsoft Access (.mdb, .accdb) + + + Microsoft Excel (.xls, .xlsx) + + + Fichiers texte et CSV (un dossier) + + + Le module complémentaire du dossier « {0} » n’a pas de fichier de description valide (pack.json) ; il n’a donc pas été chargé. Demandez une copie corrigée à la personne qui vous a fourni le module. + + + Le module complémentaire « {0} » nécessite une version plus récente de Rotula (interface de données {1}). Mettez Rotula à jour, puis redémarrez-le. + + + Le module complémentaire « {0} » a été conçu pour une version antérieure de Rotula et ne peut pas être utilisé. Demandez un module à jour. + + + Le module complémentaire « {0} » n’a pas pu être ouvert. Un fichier est peut-être absent ou endommagé, ou conçu pour un autre type d’ordinateur. Réinstallez le module. + + + Le module complémentaire « {0} » s’est arrêté sur une erreur au démarrage ; il a donc été désactivé. Rotula lui-même n’est pas touché. Réinstallez ou mettez à jour le module. + + + Le module complémentaire « {0} » ne correspond pas à son fichier de description ; il n’a donc pas été chargé. Réinstallez le module. + + + Un module complémentaire propose une source de données nommée « {0} », qui n’est pas un nom valide ; le module n’a donc pas été chargé. + + + Un module complémentaire propose une source de données nommée « {0} », qui existe déjà ; le module n’a donc pas été chargé. Un module ne peut jamais remplacer une source de données. + + + Un module complémentaire nommé « {0} » est déjà chargé ; la deuxième copie a été ignorée. + + + Le module complémentaire « {0} » de votre propre dossier n’a pas été chargé. Un module est un programme : approuvez-le seulement si vous faites confiance à sa provenance. + + + Le module complémentaire « {0} » a changé depuis que vous l’avez approuvé ; il n’a donc pas été chargé. Approuvez-le de nouveau seulement si vous vous attendiez à ce changement. + + + Le dossier du module complémentaire « {0} » contient des raccourcis (liens) ou est trop volumineux ; il a donc été ignoré. + + + Approuver ce module complémentaire ? + + + « {0} » est un module complémentaire de votre propre dossier ({1}). Un module est un programme : si vous l’approuvez, il peut faire tout ce que vous pouvez faire sur cet ordinateur, y compris lire vos fichiers. Approuvez-le seulement si vous savez qui l’a créé et que vous l’avez obtenu d’une source fiable. Vous pourrez retirer l’approbation plus tard. + + + Le module complémentaire « {0} » n’a pas pu être utilisé. + + + Certains modules complémentaires de sources de données n’ont pas pu être chargés. + + + Un module complémentaire attend votre approbation. + + + Détails + + + Fermer cet avis + + + Modules complémentaires de sources de données + + + Ces modules complémentaires ont été trouvés, mais n’ont pas été chargés. Rotula lui-même fonctionne normalement. + + + Détail technique + + + Examiner... + + + Examiner le module complémentaire {0} + + + Le module complémentaire « {0} » a été approuvé et est maintenant disponible. + + + Éditeur + + + Version + + + Empreinte (SHA-256) + + + Dossier + + + Approuver et charger + + + Ignorer + + + Ignorer laisse le module complémentaire désactivé. Rotula vous le redemandera au prochain démarrage. + + + non indiqué + + + Serveurs de bases de données + + + Services infonuagiques + + + Fichiers + + + Bases de documents + + + Services Web + + + Autres (pilotes ODBC) + + + Plus d’options + + + Cette connexion nécessite le module complémentaire « {0} », qui n’est pas installé. Ses réglages sont conservés tels quels. Installez le module et redémarrez Rotula pour utiliser ou modifier cette connexion. + + + Rotula n’a pas pu confirmer les réglages de ce service à l’aide de la documentation du fournisseur. Vérifiez l’adresse et le port avant de vous y fier. + + + Fichier + + + Dossier + + + Choisir un dossier... + + + Cette adresse semble correspondre à {0}. + + + Utiliser ses réglages + + + Vous avez modifié l’endroit ou la façon dont cette connexion ouvre une session ; le mot de passe enregistré n’a donc pas été utilisé. Tapez le mot de passe dans la case Mot de passe et testez de nouveau. + + + Choisir... + + + Le champ {0} est obligatoire. + + + {0} doit être un nombre entier de {1} à {2}. + + + Rechercher des supports d’étiquettes + + + Rechercher par numéro de pièce, format ou nom + + + Essayez un numéro de pièce comme {0}, ou un format comme 4x6 ou 100x150mm. + + + Marque + + + Toutes les marques + + + Exprimé en + + + Pouces ou mm + + + Pouces + + + Millimètres + + + Type + + + Tous les types + + + Rouleau + + + Feuille + + + Ronde + + + Vérifiés seulement + + + Favoris seulement + + + Formats courants + + + Favoris + + + Récents + + + Résultats + + + Formats proches + + + Format personnalisé... + + + Tapez votre propre largeur et hauteur + + + Format d’étiquette courant + + + {0} supports d’étiquettes. Tapez pour rechercher. + + + La bibliothèque de supports n’est pas disponible. Vous pouvez tout de même utiliser les formats courants ou un format personnalisé. + + + {0} supports trouvés + + + Affichage de {0} supports sur {1}. Tapez davantage pour affiner la recherche. + + + {0} supports trouvés, et {1} formats proches ci-dessous + + + Aucun support ne correspond. Essayez moins de mots, ou seulement le numéro de pièce ou le format. + + + Chargement de la bibliothèque de supports... + + + Format vérifié + + + Format non vérifié + + + Le format a été confirmé par deux sources indépendantes. + + + Le format provient d’une seule source, ou les sources ne concordent pas. Mesurez les étiquettes réelles avant d’imprimer un grand tirage. + + + Mon support + + + Ajouter aux favoris + + + Retirer des favoris + + + favori + + + Créer l’étiquette + + + Obtenue de votre imprimante ({0} ppp). Modifiez-la si vous imprimez sur une autre imprimante. + + + Avant de commencer + + + Ce que l’on sait de ce support + + + Il s’agit d’un rouleau continu : tapez la hauteur de l’étiquette voulue. + + + Hauteur de l’étiquette + + + Ce support n’a pas de format utilisable ; il ne peut donc pas être appliqué. + + + Le rouleau mesure {0} m de long. + + + {0} x {1} mm + + + {0} x {1} po + + + {0} mm de largeur + + + {0} po de largeur + + + {0} mm + + + {0} m + + + {0} mm ({1} po) + + + {0} dans la largeur, {1} dans la longueur + + + Inconnu. Étalonnez l’imprimante avec ce support chargé. + + + Marque noire au verso + + + Non indiqué + + + Thermique directe + + + Transfert thermique + + + Thermique directe ou transfert thermique + + + Non indiqué + + + Rouleau d’étiquettes prédécoupées + + + Rouleau continu + + + Rouleau sans dorsal + + + Pile pliée en paravent + + + Feuilles + + + Marque + + + Numéro de pièce + + + Famille de produits + + + Format + + + Type d’impression + + + Présentation + + + Surface et adhésif + + + Mandrin + + + Étiquettes par rouleau + + + Longueur du rouleau + + + Étiquettes dans la largeur + + + Rayon des coins + + + Espace entre les étiquettes + + + Espace entre les colonnes + + + Espace entre les rangées + + + Sources + + + Vérification + + + Conserver ce format comme mon propre support + + + Nom + + + Numéro de pièce (facultatif) + + + Enregistrer comme mon support + + + « {0} » a été enregistré. Vous pouvez le retrouver en recherchant son nom ou son numéro de pièce. + + + Donnez un nom ou un numéro de pièce au support. + + + Le support n’a pas pu être enregistré : {0} + + + Ce format ne peut pas être conservé comme support : il se situe hors des formats qu’une imprimante d’étiquettes peut utiliser. + + + Importer une liste de fournitures... + + + Choisissez une liste de fournitures (CSV ou Excel) + + + Importer une liste de fournitures + + + Vérifiez ce que Rotula a compris avant de le conserver. Seuls les numéros de pièce, les descriptions et les formats sont lus ; les prix ne sont jamais lus. + + + {0} lignes lues, {1} supports trouvés. + + + Rien n’a pu être importé de cette liste. + + + Ligne + + + Numéro de pièce + + + Description + + + Format (mm) + + + Format (po) + + + Certitude + + + Élevée + + + Moyenne : à vérifier + + + Le format a sa propre colonne avec une unité, ou deux mentions du format concordent. + + + Le format a été trouvé dans du texte libre avec une unité. Comparez-le avec l’étiquette réelle. + + + Remarques sur la liste + + + Enregistrer {0} supports + + + {0} supports enregistrés dans {1}. Ils font maintenant partie de la recherche. + + + Les noms de marques et de produits appartiennent à leurs propriétaires ; Rotula n’y est pas affilié. + + + À propos du catalogue de supports + + + À propos du catalogue de supports + + + La bibliothèque de supports répertorie les supports d’étiquettes par numéro de pièce du fabricant, avec le format de l’étiquette en millimètres et en pouces, le type d’impression et, lorsqu’une source l’indique, la taille du mandrin, le nombre d’étiquettes par rouleau, le nombre d’étiquettes dans la largeur et le rayon des coins. + +Elle contient {0} supports, dont {1} au format vérifié (deux sources indépendantes concordent). Les numéros de pièce et les formats sont des données de produit publiées par les fabricants ; Rotula ne conserve ni prix, ni images, ni textes publicitaires. + +L’espace entre les étiquettes n’est presque jamais publié ; Rotula le laisse donc à l’imprimante : étalonnez l’imprimante avec le support chargé. Pour les supports comportant plusieurs étiquettes dans la largeur ou dans la longueur, ainsi que pour les feuilles, seul le format d’une étiquette est appliqué. + +Vos propres supports et les listes de fournitures que vous importez sont conservés dans {2}. Supprimez un fichier de ce dossier pour retirer ses supports. + +Les noms de marques et de produits appartiennent à leurs propriétaires ; Rotula n’y est pas affilié. Vous avez trouvé un format erroné ? Les corrections sont les bienvenues : consultez docs/stock-catalog.md dans le projet. + + + Support d’étiquettes + + + Support d’étiquettes : {0} + + + Support d’étiquettes : {0} (ne figure plus dans la bibliothèque) + + + numéro de pièce + + + format + + + Trouver un support + + + Format de l’étiquette + + + Filtres + + + 1 support trouvé + + + Famille de produits + + + Toutes les familles + + + Comme vos derniers réglages d’impression ({0} ppp). Modifiez-la si vous imprimez sur une autre imprimante. + + + Les adresses de ce service ressemblent à {0}. + + + Insérer un champ + + + Insère un champ de données à la position du curseur. Vous pouvez aussi taper {{ pour afficher la liste. + + + Chargez d’abord des données : Imprimante > Fusion de données + + + Ouvrir Fusion de données + + + Rechercher des champs + + + Champs disponibles + + + 1 champ disponible + + + {0} champs disponibles + + + Aucun champ ne correspond + + + Haut/Bas pour choisir, Entrée pour insérer, Échap pour fermer + + + Colonne + + + Compteur + + + Variable + + + (vide) + + + Valeur dans l’enregistrement affiché : {0} + + + Aucune colonne nommée {0} dans les données chargées + + + Affiche les données de : {0} + + + Champs de vos données + + + Ajoutez une colonne à l’étiquette sous forme de texte, de code à barres ou de code QR, ou placez-la dans l’élément sélectionné sur l’étiquette. + + + Il n’y a encore aucun champ. Ouvrez d’abord un fichier de données ou choisissez une table de la base de données. + + + Ajouter tous les champs à l’étiquette + + + Ajoute un texte par colonne, empilés de haut en bas + + + Ajouter à l’étiquette + + + Ajouter ce champ à l’étiquette sous forme de texte + + + Utiliser ici + + + Placer ce champ dans l’élément sélectionné + + + Sélectionnez un texte ou un code à barres sur l’étiquette pour y placer un champ. + + + Élément sélectionné : {0} + + + {0} a été ajouté à l’étiquette + + + {0} champs ont été ajoutés à l’étiquette + + + Les champs ne tiennent pas tous + + + Le nouveau champ ne tient pas dans l’étiquette. Augmentez la hauteur de l’étiquette ou déplacez le champ. + + + {0} des {1} nouveaux champs ne tiennent pas dans l’étiquette. Augmentez la hauteur de l’étiquette, utilisez moins de champs ou déplacez-les. + + + {0} a été placé dans {1} + + + Variables de l’étiquette + + + l’insertion du champ + + + l’ajout des champs + + + Valeur fixe + + + Champ + + + Choisissez si le contenu est une valeur fixe ou provient d’un champ de données + + + Affiche le champ {0} des données. Utilisez Champ pour en choisir un autre. + + + Utiliser plutôt une valeur fixe + + + Ajouter comme texte + + + Ajouter comme code à barres + + + Ajouter comme code QR + + + le passage à une valeur fixe + + + Utilise des champs de données + + + Autres façons d’ajouter ce champ + + + + Le nom de variable « {0} » n’est pas valide. Utilisez seulement des lettres, des chiffres et des traits de soulignement, et gardez-le court. + + + Deux variables sont nommées « {0} ». Les noms ne tiennent pas compte des majuscules et des minuscules ; donnez donc un nom différent à chaque variable. + + + La variable « {0} » porte le même nom qu’une colonne de données ; sa propre valeur est donc utilisée au lieu de la colonne. Renommez l’une des deux si vous voulez les données de la colonne. + + + + Le format de date « {1} » de la variable « {0} » ne peut pas être utilisé. Choisissez un format dans la liste ou vérifiez le modèle. + + + La langue « {1} » choisie pour la variable « {0} » n’est pas connue sur cet ordinateur ; la langue standard est donc utilisée pour les noms des mois et des jours. Choisissez une autre langue. + + + Le décalage de date de la variable « {0} » est trop grand, ou il y a trop de décalages. Utilisez des valeurs d’au plus {1} et moins de décalages. + + + La date calculée pour la variable « {0} » se situe hors des dates que Rotula peut traiter. Réduisez les décalages de date. + + + La variable « {0} » est réglée sur une date fixe, mais aucune date n’a été entrée. Entrez la date. + + + La variable « {0} » compte des jours ouvrables, ce qu’une imprimante ne sait pas faire ; l’horloge de l’ordinateur est donc utilisée. + + + La variable « {0} » arrondit au début ou à la fin d’une période, ce qui exige l’horloge de l’ordinateur ; celle-ci est donc utilisée au lieu de l’horloge de l’imprimante. + + + La variable « {0} » utilise l’heure UTC, que l’horloge de l’imprimante ne peut pas fournir ; l’horloge de l’ordinateur est donc utilisée. + + + Le format de date de la variable « {0} » utilise « {1} », que l’imprimante ne peut pas imprimer ; l’horloge de l’ordinateur est donc utilisée. Utilisez un format plus simple pour utiliser l’horloge de l’imprimante. + + + La variable « {0} » imprime des noms de mois ou de jours dans la langue « {1} », mais l’imprimante les écrit en anglais ; l’horloge de l’ordinateur est donc utilisée. Choisissez l’anglais ou un format numérique pour utiliser l’horloge de l’imprimante. + + + Une étiquette ne peut décaler l’horloge de l’imprimante que de deux façons différentes, et les deux sont déjà utilisées ; la variable « {0} » utilise donc l’horloge de l’ordinateur. + + + La variable « {0} » utilise une date fixe, qui n’est pas une horloge ; il n’y a donc pas d’horloge d’imprimante à lire, et l’ordinateur est utilisé. + + + L’horloge de l’imprimante ne peut pas être utilisée dans un code à barres ; la variable « {0} » utilise donc l’horloge de l’ordinateur. + + + Ce champ utilise déjà ses propres marqueurs d’horloge et aucun n’est libre pour la variable « {0} » ; l’horloge de l’ordinateur est donc utilisée. + + + Aucun caractère marqueur d’horloge n’est libre dans ce champ pour la variable « {0} » ; l’horloge de l’ordinateur est donc utilisée. + + + + Le compteur « {0} » a un réglage non valide : {1}. Corrigez-le dans les réglages de la variable. + + + Le compteur « {0} » a dépassé son plus grand nombre et s’arrête au lieu de recommencer. Autorisez plus de chiffres ou réinitialisez le compteur. + + + Le départ, le pas, le type ou le remplissage du compteur « {0} » a été modifié alors que des numéros avaient déjà été utilisés (dernier numéro : {1}). L’impression est bloquée jusqu’à ce que vous acceptiez la modification ou réinitialisiez le compteur. + + + Une impression précédente a réservé les numéros {1} à {2} du compteur « {0} » sans jamais se terminer ; ces numéros ont donc été sautés. Si rien n’a été imprimé avec eux, vous pouvez les rendre dans les réglages du compteur. + + + Le fichier des compteurs « {0} » est endommagé. Rien n’a été modifié. Récupérez les compteurs (le fichier endommagé est mis de côté) ou restaurez une copie de sauvegarde, puis réessayez. + + + Le fichier des compteurs « {0} » a été écrit par une version plus récente de Rotula (version de fichier {1}). Mettez Rotula à jour pour l’utiliser. Rien n’a été modifié. + + + Le fichier des compteurs « {0} » est utilisé par un autre programme ou ne peut pas être écrit. Fermez l’autre programme, vérifiez que vous pouvez écrire dans ce dossier, puis réessayez. + + + Le compteur « {0} » mémorise son numéro d’une impression à l’autre ; l’étiquette doit donc d’abord être enregistrée. Enregistrez l’étiquette et réessayez. + + + Le compteur « {0} » devrait mémoriser son numéro, mais les compteurs ne peuvent pas être stockés pour l’instant ; il ne peut donc compter qu’à partir de son premier numéro. Pour éviter de répéter des numéros, l’impression a besoin que le fichier des compteurs soit disponible. + + + + Entrez une valeur pour « {0} ». + + + La réponse pour « {0} » est trop courte. Entrez au moins {1} caractères. + + + La réponse pour « {0} » est trop longue. Entrez au plus {1} caractères. + + + La réponse pour « {0} » n’est pas dans le format attendu. Vérifiez-la et réessayez. + + + La règle de format définie pour la question « {0} » n’est pas valide. Corrigez la règle dans les réglages de la variable. + + + La réponse pour « {0} » doit être un nombre. + + + La réponse pour « {0} » doit être une date. + + + Choisissez l’une des options proposées pour « {0} ». + + + La question « {0} » n’a aucun choix proposé. Ajoutez au moins un choix dans les réglages de la variable. + + + Aucune réponse n’a été donnée pour « {0} ». Répondez à toutes les questions avant d’imprimer. + + + + La formule de la variable « {0} » est vide. Écrivez une formule ou retirez la variable. + + + La formule de la variable « {0} » est trop longue (maximum {2} caractères). Raccourcissez-la. + + + La formule de la variable « {0} » est trop imbriquée près de la position {1} (maximum {2} niveaux). Simplifiez-la. + + + La formule de la variable « {0} » comporte trop de parties (maximum {2}). Divisez-la en deux variables. + + + La formule de la variable « {0} » contient un caractère non permis à la position {1} : {2}. Retirez-le. + + + Autre chose était attendu à la position {1} de la formule de « {0} », mais {2} a été trouvé. Vérifiez la formule. + + + La formule de la variable « {0} » se termine trop tôt (position {1}). Vérifiez s’il manque une valeur ou une parenthèse fermante. + + + Un texte entre guillemets à la position {1} de la formule de « {0} » n’est pas fermé. Ajoutez le guillemet fermant. + + + Le nombre « {2} » à la position {1} de la formule de « {0} » n’est pas valide. Vérifiez son écriture. + + + La fonction « {2} » à la position {1} de la formule de « {0} » n’existe pas. Vérifiez l’orthographe. + + + La fonction {2} à la position {1} de la formule de « {0} » accepte de {3} à {4} valeurs. Ajoutez ou retirez des valeurs. + + + La valeur numéro {3} de la fonction {2} dans la formule de « {0} » n’est pas permise (par exemple une longueur négative). Modifiez-la. + + + La formule de la variable « {0} » utilise « {2} », qui n’est pas une variable déclarée et n’a pas de données. Créez la variable ou corrigez le nom. + + + Des variables calculées dépendent les unes des autres en boucle ({2}). Modifiez l’une des formules pour briser la boucle. + + + Un résultat de la formule de « {0} » dépasserait {2} caractères et a été tronqué. Raccourcissez la formule. + + + La fonction {2} dans la formule de « {0} » n’a pas pu lire « {3} » comme un nombre. Vérifiez la valeur. + + + La fonction {2} dans la formule de « {0} » n’a pas pu lire « {3} » comme une date. Écrivez les dates sous la forme AAAA-MM-JJ ou AAAAMMJJ. + + + Le format « {3} » utilisé par la fonction {2} dans la formule de « {0} » ne peut pas être utilisé. Vérifiez le format. + + + + Les données ne tiennent pas dans un code QR au niveau de correction d’erreurs « {2} » : elles comptent {0} caractères et le maximum possible est de {1}. Raccourcissez les données ou choisissez un niveau de correction d’erreurs plus faible. + + + Le mode d’entrée des données QR n’est pas un mode connu de Rotula. Choisissez Automatique ou l’un des modes manuels. + + + Le mode alphanumérique manuel n’accepte que les chiffres, les lettres majuscules de A à Z, les espaces et les symboles $ % * + - . / : . Modifiez les données ou choisissez un autre mode d’entrée. + + + Les données sont trop longues pour le mode octet manuel (maximum {0} octets). Raccourcissez les données. + + + Le mode numérique manuel n’accepte que les chiffres de 0 à 9. Modifiez les données ou choisissez un autre mode d’entrée. + + + Le motif de masque QR doit être un nombre de {0} à {1}, ou rester automatique. + + + faible (7 %) + + + moyen (15 %) + + + quartile (25 %) + + + élevé (30 %) + + + Ce Code 128 ({0}) contient des données GS1 que Rotula ne peut pas réécrire exactement telles quelles. Il a été conservé en texte ZPL et s’imprimera quand même, mais il ne peut pas être modifié. + + + + Il n’y a aucune donnée GS1. Entrez au moins un identifiant avec sa valeur, par exemple (01)09506000134352. + + + Les données GS1 sont mal écrites près du caractère {0}. Chaque élément doit avoir la forme (01)09506000134352. + + + L’identifiant ({0}) a une longueur fixe ; il ne doit donc pas être suivi d’un caractère séparateur. Retirez le séparateur. + + + Les données se terminent avant que la valeur de ({0}) soit complète. Vérifiez que la valeur comporte tous ses caractères. + + + Les données se terminent par un séparateur qui n’est suivi de rien. Retirez-le. + + + L’identifiant qui commence par « {0} » n’est pas un identifiant GS1 connu. Vérifiez le numéro. + + + L’identifiant ({0}) n’existe pas dans le système GS1. Vérifiez le numéro entre parenthèses. + + + La valeur de ({0}) est trop longue (maximum {1} caractères). Raccourcissez-la. + + + La valeur de ({0}) est trop courte (minimum {1} caractères). Complétez-la. + + + L’identifiant ({0}) apparaît plus d’une fois avec des valeurs différentes. N’en gardez qu’un. + + + L’identifiant ({0}) apparaît plus d’une fois avec la même valeur. Retirez la répétition. + + + Les identifiants ({0}) et ({1}) ne peuvent pas être utilisés ensemble. Retirez l’un d’eux. + + + L’identifiant ({0}) s’utilise normalement avec d’autres identifiants : {1} (une virgule sépare les solutions de rechange, un signe plus signifie tous). Ajoutez-les ou vérifiez vos données. + + + Un Data Matrix GS1 ne peut pas aussi être un GS1 Digital Link. Désactivez l’une des deux options. + + + L’option de chiffre de contrôle n’est pas nécessaire : le GS1-128 ajoute son propre chiffre de contrôle. Désactivez l’option. + + + Une valeur contient une parenthèse, que l’imprimante supprime en mode GS1-128. Retirez la parenthèse de la valeur. + + + Les données GS1-128 comptent {0} caractères ; le maximum est de {1} (les identifiants et les séparateurs comptent). Raccourcissez les données. + + + Un code ITF-14 nécessite exactement 14 chiffres. + + + Le chiffre de contrôle du code ITF-14 est erroné. Il devrait être {0}. + + + La partie linéaire « {0} » du code composite n’est pas un GTIN valide (8, 12, 13 ou 14 chiffres avec un chiffre de contrôle correct). Corrigez-la. + + + L’identifiant ({0}) apparaît à la fois dans la partie linéaire et dans la partie 2D du code composite. Ne le gardez que dans l’une des deux. + + + + Le texte n’est pas une adresse Web valide. Un GS1 Digital Link doit commencer par https:// (ou http://). + + + Le lien n’a pas de clé GS1. Un Digital Link doit avoir une clé, comme le GTIN (01), suivie de sa valeur. + + + Le lien a plus d’une clé GS1. Un Digital Link nécessite exactement une clé, comme le GTIN. Retirez les autres. + + + L’identifiant ({0}) ne peut pas être utilisé dans un GS1 Digital Link. Retirez-le. + + + Le chemin du lien est incomplet : il doit être formé de paires, un identifiant suivi de sa valeur. Vérifiez le lien. + + + Les qualificatifs ({1}) ne forment pas une combinaison permise pour la clé ({0}). Retirez-en un ou vérifiez les règles GS1. + + + Les qualificatifs ({1}) de la clé ({0}) ne sont pas dans un ordre permis. Placez-les dans l’ordre défini par GS1 pour cette clé. + + + + La valeur de ({0}) ne doit contenir que des traits d’union (-). + + + La valeur de ({0}) doit contenir au moins un caractère qui n’est pas un chiffre. + + + La valeur de ({0}) doit être composée uniquement de zéros. + + + La valeur de ({0}) doit être 0, 1 ou 9. + + + La valeur de ({0}) doit être 0 ou 1. + + + La valeur de ({0}) doit être 0, 1, 2 ou 9. + + + La valeur « {1} » de ({0}) n’est pas un code de pays à 3 chiffres valide (ISO 3166). Vérifiez-la. + + + La valeur « {1} » de ({0}) n’est pas un code de pays. Utilisez un code ISO 3166 à 3 chiffres ou 999. + + + La valeur « {1} » de ({0}) n’est pas un code de pays à 2 lettres valide (ISO 3166). Vérifiez-la. + + + La valeur « {1} » de ({0}) n’est pas un code numérique de devise valide (ISO 4217). Vérifiez-la. + + + La valeur « {1} » de ({0}) n’est pas un code de type d’emballage valide. Vérifiez-la. + + + La valeur de ({0}) doit être une seule lettre, un seul chiffre, un trait d’union ou un trait de soulignement. + + + La valeur de ({0}) doit compter 10 chiffres et représenter une latitude entre -90 et 90 degrés. + + + La valeur de ({0}) doit compter 10 chiffres et représenter une longitude entre -180 et 180 degrés. + + + Le caractère « {1} » dans la valeur de ({0}) n’est pas un chiffre. Cet identifiant n’accepte que des chiffres. + + + Le caractère « {1} » dans la valeur de ({0}) ne peut pas être utilisé dans des données GS1. Utilisez seulement les lettres, les chiffres et les symboles permis par GS1. + + + Le caractère « {1} » dans la valeur de ({0}) n’est pas permis. Utilisez seulement des lettres majuscules, des chiffres, # - et /. + + + Le caractère « {1} » dans la valeur de ({0}) n’est pas permis dans un texte Base64. Utilisez des lettres, des chiffres, - et _. + + + Le remplissage « = » à la fin de la valeur de ({0}) n’est pas correct. + + + Le chiffre de contrôle de ({0}) est erroné ou absent (attendu : {1}). Corrigez-le. + + + Les deux caractères de contrôle à la fin de la valeur de ({0}) sont erronés. Corrigez-les. + + + La valeur de ({0}) doit contenir un préfixe d’entreprise GS1 d’au moins 4 chiffres à l’endroit attendu. + + + La date de ({0}) doit s’écrire sous la forme {1}. + + + Le mois « {1} » dans la date de ({0}) doit être de 01 à 12. + + + Le jour « {1} » dans la date de ({0}) n’est pas valide pour ce mois. + + + L’heure de ({0}) doit s’écrire sous la forme {1}. + + + L’heure « {1} » dans ({0}) doit être de 00 à 23. + + + Les minutes « {1} » dans ({0}) doivent être de 00 à 59. + + + Les secondes « {1} » dans ({0}) doivent être de 00 à 59. + + + La valeur de ({0}) ne doit pas être zéro. + + + La valeur de ({0}) ne doit pas commencer par un zéro. + + + La valeur de ({0}) doit être formée de deux nombres de même longueur : le numéro de pièce et le total (par exemple 0204). + + + Ni le numéro de pièce ni le total de ({0}) ne peuvent être zéro. + + + Le numéro de pièce de ({0}) est supérieur au total. Vérifiez les deux nombres. + + + La valeur de ({0}) doit être formée de deux nombres séparés par une barre oblique (par exemple 2/5). + + + Le premier nombre de ({0}) est supérieur au second. Vérifiez les deux nombres. + + + La valeur de ({0}) contient un code % incomplet ou non valide. + + + Le type de support « {1} » de ({0}) n’est pas valide. Utilisez de 01 à 10 ou de 80 à 99. + + + La valeur de ({0}) n’est pas un IBAN valide. Vérifiez-la. + + + + Seuls les chiffres sont permis. + + + Le chiffre de contrôle est erroné. Il devrait être {0}. + + + Un GTIN compte 8, 12, 13 ou 14 chiffres. + + + Le chiffre indicateur doit être de 0 à 9. + + + Un préfixe d’entreprise GS1 compte de 4 à 12 chiffres. + + + La référence ne doit pas être vide et doit être composée de chiffres. + + + Le préfixe d’entreprise est trop long : il ne laisse pas de place pour la référence. + + + La référence est trop longue. Au plus {0} caractères sont permis. + + + Le chiffre d’extension doit être de 0 à 9. + + + Le caractère « {0} » n’est pas permis dans des données GS1. + + + L’identifiant ({0}) n’est pas une mesure avec une position de virgule décimale. + + + La mesure ne peut pas être négative et peut compter au plus {0} chiffres. + + + + Le code QR est rempli à {0} % (version {1}). Il pourrait être difficile à lire. Raccourcissez le contenu ou choisissez un niveau de correction d’erreurs plus faible. + + + Le contenu ne tient pas dans un code QR au niveau de correction d’erreurs « {2} » : il nécessite {0} et la limite est de {1}. + + + Raccourcissez le contenu pour qu’il tienne. + + + Le niveau de correction d’erreurs « {0} » permettrait au contenu de tenir. + + + L’adresse courriel n’est pas valide. Vérifiez-la (par exemple nom@exemple.com). + + + Entrez au moins une adresse courriel. + + + L’événement se termine avant de commencer. Vérifiez les dates de début et de fin. + + + La date n’a pas de fuseau horaire ; elle est donc lue en heure UTC et pourrait paraître décalée dans un calendrier. Ajoutez le fuseau, par exemple -05:00. + + + Entrez un titre pour l’événement. + + + L’altitude n’est pas un nombre valide. + + + La latitude doit être un nombre entre -90 et 90. + + + La longitude doit être un nombre entre -180 et 180. + + + Entrez un prénom ou un nom de famille pour le contact. + + + Entrez un prénom, un nom de famille ou une organisation pour le contact. + + + Le numéro de téléphone n’est pas valide. Utilisez des chiffres, éventuellement avec + et l’indicatif du pays (par exemple +15145551234). + + + Le message dépasse {0} caractères ; il pourrait donc être envoyé en plusieurs messages. + + + Entrez l’adresse Web. + + + L’adresse Web n’est pas valide. Écrivez-la au complet, sans espaces (par exemple https://exemple.com). + + + L’adresse commence par « {0} », que bien des appareils photo de téléphone n’ouvriront pas. Utilisez https:// si possible. + + + Ce réseau n’a aucune sécurité ; le mot de passe est donc ignoré. Effacez le mot de passe ou choisissez un type de sécurité. + + + La longueur du mot de passe n’est pas valide pour ce type de sécurité. Il doit compter {0} caractères. + + + Entrez le mot de passe du réseau. + + + Entrez le nom du réseau Wi-Fi. + + + Le nom du réseau est trop long (maximum {0} octets ; les caractères accentués en utilisent plus d’un). + + + Ce champ est obligatoire. + + + Ce texte est trop long (maximum {1} caractères). Raccourcissez-le. + + + Choisissez l’une des valeurs proposées. + + + Entrez un nombre valide (utilisez un point pour les décimales). + + + Entrez une date valide, ou une date et une heure avec leur fuseau (par exemple 2026-03-14T09:00:00-05:00). + + + + Les données sont GS1 (GS1 DataMatrix) + + + Les données sont un GS1 Digital Link + + + Motif de masque + + + Mode d’entrée des données + + + Contenu structuré + + + Faible (7 %) + + + Moyen (15 %) + + + Quartile (25 %) + + + Élevé (30 %) + + + Modèle 1 (original) + + + Modèle 2 (recommandé) + + + Automatique (l’imprimante choisit) + + + Motif 0 + + + Motif 1 + + + Motif 2 + + + Motif 3 + + + Motif 4 + + + Motif 5 + + + Motif 6 + + + Motif 7 + + + Automatique + + + Manuel : chiffres seulement + + + Manuel : majuscules et chiffres + + + Manuel : tout texte (octets) + + + Manuel : kanji + + + + Adresse Web + + + Texte + + + Wi-Fi + + + Contact (vCard) + + + Contact (MeCard) + + + GS1 Digital Link + + + Courriel + + + Appel téléphonique + + + Message texte (SMS) + + + Emplacement + + + Événement d’agenda + + + + Adresse Web + + + Retirer les paramètres de suivi + + + Texte + + + Nom du réseau + + + Mot de passe + + + Sécurité + + + Réseau masqué + + + Prénom + + + Nom de famille + + + Organisation + + + Titre + + + Téléphone + + + Cellulaire + + + Courriel + + + Rue + + + Ville + + + Province, État ou région + + + Code postal + + + Pays + + + Remarque + + + Préfixe de l’adresse Web + + + GTIN + + + Lot + + + Numéro de série + + + Date d’expiration (AAMMJJ) + + + Meilleur avant (AAMMJJ) + + + Date de production (AAMMJJ) + + + À + + + Objet + + + Corps du message + + + Numéro de téléphone + + + Message + + + Latitude + + + Longitude + + + Nom du lieu + + + Début + + + Fin + + + Lieu + + + Description + + + Toute la journée + + + + WPA / WPA2 + + + WPA3 + + + WEP (ancien) + + + Aucune (réseau ouvert) + + + + Le serveur a fermé la connexion pendant l’ouverture de session. Le nom d’utilisateur ou le mot de passe est probablement erroné. Si vous utilisez une connexion chiffrée, le certificat du serveur peut aussi être en cause. + + + La base de données ne peut pas déterminer le type de ce paramètre. + + + Indiquez le type du paramètre dans la requête, par exemple @x::text. + + + Le paramètre est d’un type différent de celui de la colonne à laquelle il est comparé. + + + Passez une valeur du type de la colonne, ou convertissez le paramètre, par exemple @x::numeric ou @x::date. + + + Accepte un certificat qui ne peut pas être vérifié. Avec MariaDB et un compte à mot de passe, le certificat est vérifié par son empreinte ; un serveur à certificat autosigné se connecte donc habituellement sans cette option. + + + Nom de la table + + + + Fichier DuckDB + + + Firebird + + + Oracle Database + + + ClickHouse + + + Fichier de base de données DuckDB + + + Le fichier .duckdb sur cet ordinateur. + + + Limite de mémoire (Mo) + + + Limite facultative de la mémoire qu’une requête peut utiliser. + + + Chemin ou alias de la base de données sur le serveur + + + Le chemin tel que le serveur le voit (C:\donnees\ventes.fdb) ou un alias du fichier databases.conf du serveur. + + + Chiffre toute la connexion. Avec un nom d’utilisateur et un mot de passe, c’est obligatoire : si le serveur ne peut pas chiffrer, la connexion échoue. + + + Rôle SQL (facultatif) + + + Utilisez un rôle qui peut seulement lire (SELECT). + + + Nom du service + + + Le service offert par l’écouteur (par exemple ORCLPDB1). + + + Utilise TLS. Désactivez-le seulement pour un écouteur sans TLS. + + + Ne pas vérifier le nom du serveur dans le certificat + + + Garde TLS activé, mais saute la vérification du nom d’hôte. Réduit la sécurité. + + + Le nom ci-dessus est + + + Un nom de service + + + Un SID (ancienne méthode) + + + Un nom de tnsnames.ora + + + Dossier contenant tnsnames.ora + + + Seulement pour les noms de tnsnames.ora. + + + Dossier du portefeuille (certificats TLS) + + + Un portefeuille Oracle contenant les certificats de confiance, lorsque le magasin de certificats de Windows ne suffit pas. + + + Laisser le serveur arrêter les longues requêtes (nécessite un compte autorisé à modifier les réglages) + + + Désactivé par défaut : les comptes à profil en lecture seule sont refusés lorsqu’un réglage change. + + + Assistant de contenu... + + + Assistant GS1... + + + Créé avec l’assistant de contenu : {0}. Modifier le texte à la main supprime cette définition. + + + La définition de contenu enregistrée ne peut pas être lue. Ouvrez l’assistant de contenu pour la recréer. + + + Vous avez modifié le texte à la main ; la définition de l’assistant de contenu a donc été supprimée. Ouvrez l’assistant de contenu pour recommencer à partir d’un formulaire. + + + l’assistant de contenu QR + + + l’assistant GS1 + + + Rien à mesurer pour l’instant. L’indicateur apparaît quand le code a du contenu. + + + Tient : {0} % de la capacité au niveau {1}, version {2} + + + Tient : {0} % de la capacité au niveau {1} (modèle 1) + + + Presque plein : {0} % de la capacité au niveau {1}, version {2}. Baissez le niveau ou raccourcissez le texte pour faciliter la lecture. + + + Presque plein : {0} % de la capacité au niveau {1} (modèle 1). Baissez le niveau ou raccourcissez le texte pour faciliter la lecture. + + + Ne tient pas au niveau {0} : il faut {1} et la limite est de {2}. Baissez le niveau ou raccourcissez le texte. + + + Au niveau {0}, le contenu tiendrait. + + + Même le niveau le plus bas ne suffit pas : raccourcissez le texte. + + + Niveau suggéré pour ce contenu : {0}. + + + Mesuré sur le plus long des {0} enregistrements chargés. + + + Taille imprimée : {0} mm ({1} po) de côté à {2} ppp (version {3}). + + + Chaque module mesure {0} mm de largeur à {1} ppp. + + + Trop petit pour être lu : chaque module mesure {0} mm de largeur. Utilisez au moins {1} points (environ {2} mm) par module à {3} ppp. + + + Assistant de contenu QR + + + Appliquer au code + + + Que doit faire le code quand on le lit ? + + + Aperçu + + + Texte dans le code + + + Capacité + + + À corriger + + + Utiliser plutôt une valeur fixe + + + {0} caractères + + + Le mot de passe est imprimé en clair dans le code : quiconque le lit peut le voir. Il est affiché ici volontairement, pour que vous puissiez le vérifier. + + + Exemple : {0} + + + Vous pouvez écrire plusieurs lignes. Exemple : {0} + + + Un numéro de téléphone avec l’indicatif du pays. Exemple : {0} + + + Une adresse courriel. Exemple : {0} + + + Une adresse Web complète qui commence par https://. Exemple : {0} + + + Un nombre ; utilisez un point pour les décimales. Exemple : {0} + + + Chiffres seulement. Exemple : {0} + + + Choisissez la date, écrivez l’heure en heures et minutes (09:30), ou liez un champ. Exemple : {0} + + + Remplissez le formulaire et le code apparaîtra ici. + + + Le code apparaît une fois les problèmes de droite corrigés. + + + Les champs liés affichent la valeur de l’enregistrement affiché dans la zone de conception, ou leur exemple. + + + Remplissez les champs marqués d’un * pour continuer. + + + Assistant GS1-128 + + + Assistant GS1 DataMatrix + + + Appliquer au code à barres + + + Valeur + + + Valeur + + + Identifiant inconnu + + + Format {0}, jusqu’à {1} caractères. + + + Expédition (SSCC) + + + Une palette ou un colis : son code séquentiel de colis (SSCC). + + + Produit (GTIN, lot, expiration) + + + Le code du produit avec son lot et sa date d’expiration. + + + Sérialisation (GTIN, numéro de série) + + + Le code du produit avec un numéro de série pour chaque article. + + + Identifiants courants. Tapez pour les rechercher tous. + + + {0} identifiants trouvés + + + {0} caractères sur {1} + + + Environ {0} caractères sur {1} (les champs liés sont estimés) + + + {0} caractères + + + Le message nécessite {0} caractères et le GS1-128 en contient au plus {1}. Retirez un identifiant ou utilisez des valeurs plus courtes. + + + Cette valeur provient d’un champ. Elle est vérifiée par rapport aux données à l’impression. + + + Choisissez un usage prédéfini ou ajoutez des identifiants de la liste pour commencer. + + + Remplissez la valeur de chaque identifiant pour continuer. + + + Monter + + + Descendre + + + Retirer cet identifiant + + + Partir d’un usage + + + Ajouter un identifiant + + + Rechercher des identifiants + + + Tapez le numéro d’un identifiant (10) ou des mots de son nom en anglais (expiry). + + + Entrée ou un double-clic ajoute l’identifiant. Flèche bas pour passer à la liste. + + + Identifiants correspondants + + + Le message + + + Aucun identifiant pour l’instant. Choisissez un usage à gauche, ou recherchez un identifiant et ajoutez-le. + + + Aperçu + + + Le symbole apparaît lorsque toutes les valeurs sont valides. + + + Forme lisible + + + Caractères encodés avec FNC1 + + + [FNC1] marque le caractère spécial qui commence le symbole et sépare les valeurs de longueur variable. + + + Longueur + + + Oui + + + Heure du jour en heures et minutes, par exemple 09:30 + + + Avant d’imprimer + + + Cette étiquette pose quelques questions. Vos réponses sont utilisées sur toutes les étiquettes de cette impression. + + + Continuer + + + Tapez la date sous la forme AAAA-MM-JJ, par exemple 2026-03-07. + + + Tapez un nombre, par exemple 12 ou 3.5. + + + Afficher ou masquer la liste des objets + + + Le texte ne tient pas dans sa zone : une partie est coupée. Agrandissez la zone, permettez plus de lignes, utilisez une taille plus petite ou raccourcissez le texte. + + + Le texte risque de ne pas tenir dans sa zone. L’imprimante n’indique pas la largeur de ses lettres ; il s’agit donc d’une estimation : imprimez une étiquette d’essai pour vérifier, ou agrandissez la zone ou permettez plus de lignes. + + + Le texte utilise la police Windows « {0} », qui doit être dessinée comme une image, mais ce programme ne peut pas la dessiner ici ; le texte n’a donc pas été imprimé. Choisissez plutôt une police de l’imprimante. + + + Un texte en police Windows est imprimé comme une image ; l’imprimante ne peut donc pas le numéroter, le tirer d’un champ enregistré ni y intégrer des caractères. Retirez ces options de champ ou choisissez une police de l’imprimante. + + + La date « {0} » utilise l’horloge de l’imprimante, qui ne peut pas servir dans une image de texte. L’horloge de l’ordinateur a été utilisée à la place. + + + La direction et l’espacement des caractères d’un texte sont ignorés quand il utilise une police Windows. + + + Le texte est trop grand pour être imprimé comme une image. Utilisez une taille plus petite ou une zone plus étroite. + + + Cet ordinateur n’a aucune police nommée « {0} » ; « {1} » est donc utilisée à la place. Installez la police ou choisissez-en une autre, sinon le texte imprimé aura une apparence différente. + + + La police « {0} » ne peut pas être utilisée : il lui faut un nom et une taille de 1 à 1000 points. + + + Le gras, l’italique et le soulignement n’existent pas dans les polices propres à l’imprimante et ne sont pas imprimés. Choisissez une police Windows pour les utiliser. + + + « {0} » n’est pas un nom de fichier de police accepté par l’imprimante. Utilisez un nom comme E:ARIAL.TTF. + + + Cette étiquette a un compteur ; chaque copie a donc besoin de son propre numéro, ce qui est limité à {0} copies par impression. Imprimez moins de copies à la fois, ou utilisez la fenêtre Fusion de données. + + + Mise en forme + + + Retour à la ligne automatique + + + Taille en points de l’imprimante (avancé) + + + Mise en forme du texte + + + Police + + + Taille (pt) + + + Taille en points typographiques (1 point vaut 1/72 de pouce), comme dans un traitement de texte. Choisissez une taille ou tapez la vôtre. + + + Choisissez une police de l’imprimante (imprimée comme du vrai texte) ou une police Windows (imprimée comme une image). Tapez pour rechercher. + + + Polices de l’imprimante + + + Polices Windows + + + Police de l’imprimante {0} (vectorielle) + + + Police de l’imprimante {0} (forme fixe) + + + Police de l’imprimante {0} + + + Police enregistrée dans l’imprimante ({0}) + + + {0} (non installée) + + + Gras + + + Italique + + + Souligné + + + Gras (Ctrl+B) + + + Italique (Ctrl+I) + + + Souligné (Ctrl+U) + + + Le gras, l’italique et le soulignement n’existent pas dans les polices propres à l’imprimante. Choisissez une police Windows dans la liste Police pour les utiliser. + + + L’alignement fonctionne dans une zone de texte. Activez le retour à la ligne automatique pour l’utiliser. + + + Aligner à gauche + + + Centrer + + + Aligner à droite + + + Justifier + + + Ce texte est imprimé comme une image. Il reste net à la résolution de l’imprimante, mais l’imprimante ne peut pas le modifier ni le tirer de ses propres champs. Les polices de l’imprimante sont imprimées comme du vrai texte. + + + Compris + + + Police Windows : imprimée comme une image + + + Agrandir la zone + + + Réduire le texte + + + Cliquez sur l’un des boutons ci-dessous pour corriger le problème en une étape. + + + Cet ordinateur n’a aucune police nommée « {0} » ; « {1} » est donc utilisée à la place. Installez la police ou choisissez-en une autre, sinon le texte imprimé aura une apparence différente. + + + Le gras, l’italique et le soulignement nécessitent une police Windows. Choisissez-en une dans la liste Police. + + + Largeur de la zone : {0} points ({1} mm) + + + Le texte tient maintenant dans sa zone. + + + Le texte ne tient toujours pas ; essayez une zone plus grande ou une taille plus petite. + + + le changement de police + + + le style du texte + + + la taille de police + + + l’alignement + + + le retour à la ligne + + + l’ajustement du texte à sa zone + + + L’aperçu ne peut pas être dessiné pour l’instant. Faites un zoom arrière ou réduisez l’étiquette, puis réessayez ; votre étiquette n’est pas touchée. + + + Ajustement du texte + + + Quand le texte est trop long + + + Laisser tel que conçu + + + Réduire pour ajuster + + + Augmenter la hauteur + + + La zone reste telle que conçue. Le texte qui ne tient pas est coupé à l’impression. + + + Chaque étiquette utilise la plus grande taille, entre la plus petite et la plus grande ci-dessous, à laquelle tout le texte tient dans la zone. Avec des données, chaque enregistrement a sa propre taille. + + + La taille reste la même et la zone reçoit autant de lignes que le texte en a besoin, jusqu’à la limite ci-dessous. Les éléments placés dessous ne bougent pas. + + + Plus petite taille (pt) + + + Plus grande taille (pt) + + + Nombre maximal de lignes + + + Ne pas avertir quand le texte ne tient pas + + + Utilisez cette option quand vous voulez que le texte soit coupé. + + + La plus petite taille ne peut pas être supérieure à la plus grande taille. + + + le changement d’ajustement du texte + + + le changement de limite d’ajustement du texte + + + Impression + + + Ne pas imprimer cet objet + + + L’objet reste sur l’étiquette dans le concepteur, par exemple comme note ou repère, mais il n’est jamais imprimé ni exporté. + + + Imprimer seulement si + + + Omet cet objet sur les étiquettes où la règle ci-dessous n’est pas vraie, par exemple pour imprimer un avertissement seulement pour certains enregistrements. + + + Champ à vérifier + + + La colonne de données ou la variable dont la valeur décide si cet objet est imprimé. + + + Choisir un champ + + + Comparaison + + + Valeur à comparer + + + Les majuscules et les espaces autour du texte n’ont pas d’importance : Oui, oui et " OUI " sont identiques. + + + Règle d’impression + + + Verrouiller la position + + + Un objet verrouillé ne peut pas être déplacé, redimensionné ni supprimé avec la souris ou le clavier tant que vous ne le déverrouillez pas. Vous pouvez quand même le sélectionner et modifier ses propriétés ici. Le verrouillage ne change pas ce qui est imprimé. + + + est égal à + + + est différent de + + + est vide + + + n’est pas vide + + + contient + + + S’imprime seulement si {0} est égal à « {1} ». + + + S’imprime seulement si {0} est différent de « {1} ». + + + S’imprime seulement si {0} est vide. + + + S’imprime seulement si {0} n’est pas vide. + + + S’imprime seulement si {0} contient « {1} ». + + + Choisissez le champ à vérifier. D’ici là, cet objet est toujours imprimé. + + + Cet objet n’est jamais imprimé, quelle que soit la règle. + + + Choisissez le champ à vérifier. + + + Aucun champ ni aucune variable nommé « {0} » n’existe dans les données chargées ou dans l’étiquette. Il est traité comme vide ; vérifiez donc le nom. + + + La règle d’impression de cet objet vérifie « {0} », mais aucune colonne de données ni variable ne porte ce nom. Il est traité comme vide ; vérifiez donc le nom. + + + Les objets verrouillés n’ont pas été modifiés. Décochez « Verrouiller la position » dans les propriétés pour les modifier. + + + Non imprimé + + + Marqué « Ne pas imprimer » : cet objet n’est jamais imprimé ni exporté. + + + Masqué + + + La règle d’impression de cet objet n’est pas vraie pour l’enregistrement affiché ; il est donc omis de cette étiquette. + + + Verrouillé + + + Verrouillé : cet objet ne peut pas être déplacé, redimensionné ni supprimé tant que vous ne le déverrouillez pas. + + + Rien n’a été envoyé : l’imprimante « {0} » signale que sa tête d’impression est ouverte. Fermez la tête jusqu’au déclic, puis imprimez de nouveau. + + + Rien n’a été envoyé : l’imprimante « {0} » signale qu’elle n’a plus d’étiquettes. Chargez des étiquettes, fermez la tête, appuyez une fois sur Feed, puis imprimez de nouveau. + + + Rien n’a été envoyé : l’imprimante « {0} » signale que le ruban est épuisé. Installez un nouveau ruban, fermez la tête, puis imprimez de nouveau. + + + Rien n’a été envoyé : l’imprimante « {0} » signale que sa tête d’impression est trop chaude. Attendez quelques minutes qu’elle refroidisse, puis imprimez de nouveau. + + + Rien n’a été envoyé : l’imprimante « {0} » est en mode de diagnostic des communications et imprimerait les données comme du texte au lieu d’étiquettes. Désactivez ce mode sur l’imprimante (ou redémarrez-la), puis imprimez de nouveau. + + + Rien n’a été envoyé : l’imprimante « {0} » signale que sa mémoire pour les travaux reçus est pleine. Attendez qu’elle ait imprimé ce qu’elle a déjà, puis imprimez de nouveau. + + + Rien n’a été envoyé : l’imprimante « {0} » est en pause. Appuyez sur Pause sur l’imprimante pour reprendre, puis imprimez de nouveau. + + + L’imprimante signale aussi : {0}. + + + L’état de l’imprimante n’a pas pu être lu avant l’impression ({0}) ; les étiquettes ont donc été envoyées sans cette vérification. + + + L’imprimante signale : {0}. Les étiquettes ont été envoyées ; vérifiez la qualité d’impression. + + + Imprimé et vérifié : l’imprimante signale qu’elle a terminé le travail sans problème. + + + Les étiquettes ont été envoyées, mais l’imprimante a ensuite signalé un problème : {0}. Certaines étiquettes n’ont peut-être pas été imprimées. Réglez le problème de l’imprimante et vérifiez les étiquettes avant d’imprimer de nouveau. + + + Les étiquettes ont été envoyées, mais Rotula n’a pas pu confirmer qu’elles ont été imprimées ({0}). Vérifiez l’imprimante. + + + cette imprimante ou son pilote Windows ne répond pas aux demandes d’état, ce qui est courant avec les pilotes USB + + + Windows indique que l’imprimante est hors ligne + + + l’imprimante n’a pas pu être jointe + + + l’imprimante n’a pas répondu à temps + + + la réponse de l’imprimante n’était pas une réponse d’état ZPL + + + elle imprimait encore quand Rotula a cessé d’attendre + + + la vérification a été arrêtée + + + la raison est inconnue + + + tête d’impression ouverte + + + plus d’étiquettes + + + ruban épuisé + + + tête d’impression trop chaude + + + mode de diagnostic des communications activé + + + mémoire pour les travaux reçus pleine + + + en pause + + + tête d’impression trop froide (les étiquettes peuvent sortir plus pâles) + + + réglages perdus (l’imprimante utilise ses valeurs par défaut) + + + un travail est encore en cours de réception + + + une étiquette imprimée attend d’être retirée + + + inconnu + + + réenroulement + + + décollage (peel-off) + + + détachement manuel (tear-off) + + + coupe + + + applicateur + + + coupe différée + + + décollage sans support + + + réenroulement sans support + + + coupe partielle + + + RFID + + + kiosque + + + continu + + + Vérifier l’état de l’imprimante + + + Demande à l’imprimante son état (papier, tête, ruban, pause) avec une petite commande ZPL. Rien n’est imprimé sur une imprimante compatible ZPL. + + + Demande de l’état de l’imprimante... + + + L’imprimante est prête. + + + L’imprimante ne peut pas imprimer pour l’instant : {0}. + + + L’imprimante signale : {0}. + + + L’état de l’imprimante n’a pas pu être lu : {0}. Vous pouvez quand même imprimer. + + + Seule une partie de l’état a été reçue ; certains problèmes peuvent donc manquer dans la liste. + + + Étiquettes restant à imprimer : {0}. Travaux en attente dans l’imprimante : {1}. + + + Mode d’impression : {0}. {1}. Longueur d’étiquette : {2} points. + + + Transfert thermique (avec ruban) + + + Thermique direct (sans ruban) + + + Vérifier l’imprimante avant et après l’impression + + + Avant chaque travail, Rotula demande son état à l’imprimante et n’envoie rien tant qu’elle n’a plus d’étiquettes, qu’elle est ouverte, en pause ou sans ruban. Après le travail, il attend que l’imprimante signale qu’elle a terminé. Pour les imprimantes compatibles ZPL ; beaucoup de pilotes USB ne peuvent pas répondre, et l’impression se fait alors sans la vérification. + + + Ouvrir le poste d’impression... + + + Créer le NIP du poste d’impression... + + + Poste d’impression + + + Choisissez le dossier du poste d’impression (avec son station.json, ou avec les étiquettes à proposer) + + + Le poste d’impression ne peut pas s’ouvrir. + + + Quitter + + + Toutes les étiquettes + + + Recherchez ou scannez une étiquette + + + Tapez un nom ou scannez le code de l’étiquette, puis appuyez sur Entrée. Ou touchez une étiquette ci-dessous. + + + Aucune étiquette ne correspond à la recherche. + + + Aucune étiquette n’a le code « {0} ». Scannez-le de nouveau ou choisissez une étiquette dans la liste. + + + Certaines étiquettes ne sont pas disponibles : + + + L’étiquette « {0} » ne peut pas être utilisée : {1} + + + Imprimante : {0} + + + Aucune imprimante n’est configurée pour cette étiquette. Demandez à la personne qui a configuré le poste d’en ajouter une dans station.json. + + + Quantité + + + De 1 à {0} étiquettes + + + Entrez une quantité de 1 à {0}, puis appuyez de nouveau sur Imprimer. + + + Imprimer + + + Imprimer tous les enregistrements + + + {0} enregistrements des données de l’étiquette sont prêts à imprimer. + + + Les données de l’étiquette n’ont pas pu être lues : {0} + + + les données ne contiennent aucun enregistrement. + + + Cette étiquette ne pose aucune question. Indiquez la quantité et appuyez sur Imprimer. + + + Certaines réponses ne sont pas acceptées. Corrigez les champs marqués en rouge, puis imprimez de nouveau. + + + Aperçu + + + Vérification de l’imprimante... + + + Imprimante prête + + + État de l’imprimante inconnu + + + L’imprimante demande votre attention + + + Impression... + + + Rotula vérifie l’imprimante avant l’envoi et attend qu’elle signale avoir terminé. + + + Non imprimé : l’imprimante demande votre attention + + + Envoyé à l’imprimante + + + Imprimé et vérifié + + + Envoyé, mais non confirmé + + + Envoyé, mais l’imprimante signale un problème + + + Non imprimé + + + Entrez le NIP pour quitter le poste d’impression + + + NIP + + + Quitter le poste + + + Ce NIP n’est pas le bon. Réessayez. + + + Trop de NIP erronés. Attendez {0} secondes, puis réessayez. + + + Créer un NIP pour le poste d’impression + + + Le NIP est nécessaire pour quitter le poste d’impression. Rotula ne conserve qu’un code protégé créé à partir de celui-ci, jamais le NIP lui-même. Copiez la ligne dans le station.json du poste. + + + Nouveau NIP + + + Tapez le NIP de nouveau + + + Créer + + + Ligne pour station.json + + + Copier + + + Copié. Collez-le dans station.json, à côté des autres réglages. + + + Un NIP doit compter de {0} à {1} caractères. + + + Les deux NIP ne sont pas identiques. Tapez-les de nouveau. + + + « {0} » est introuvable. Vérifiez le chemin donné au poste d’impression. + + + « {0} » n’a pas pu être lu. Vérifiez que le fichier n’est pas verrouillé et que cet utilisateur a le droit de le lire. + + + « {0} » contient une erreur près de la ligne {1} ({2}) : un réglage est mal orthographié, a une valeur du mauvais type, ou il manque une virgule ou un guillemet. + + + Les réglages du poste ont été écrits pour une version plus récente de Rotula (format {0}). Mettez Rotula à jour sur cet ordinateur. + + + Il n’y a aucune étiquette à proposer. Placez des fichiers .rotula dans le dossier du poste ou énumérez-les sous "labels" dans station.json. + + + L’étiquette « {0} » indiquée dans station.json est introuvable et est omise. + + + « {0} » n’est pas une étiquette Rotula (fichier .rotula). Corrigez-le dans station.json. + + + L’entrée d’étiquette {0} de station.json n’a pas de "file". Ajoutez le nom du fichier de l’étiquette. + + + Deux étiquettes utilisent le code « {0} ». Donnez à chaque étiquette son propre code, pour qu’un scan ouvre la bonne. + + + L’imprimante « {0} » n’est pas définie sous "printers" dans station.json. + + + L’imprimante « {0} » a besoin soit de "windowsPrinter" (une imprimante installée), soit de "host" (une adresse réseau), mais pas des deux. + + + Le port de l’imprimante « {0} » doit être compris entre 1 et 65535 (habituellement 9100). + + + La résolution de l’imprimante « {0} » doit être 203, 300 ou 600. + + + L’imprimante « {0} » a un contraste hors de 0 à 30 ou une vitesse hors de 1 à 14. + + + Les réglages de copies du poste ne sont pas valides : "maxCopies" doit être compris entre 1 et 99999999 et "defaultCopies" entre 1 et "maxCopies". + + + Les réglages de copies de l’étiquette « {0} » ne sont pas valides : "maxCopies" doit être compris entre 1 et 99999999 et "defaultCopies" entre 1 et "maxCopies". + + + "exitPinHash" n’est pas un code de NIP créé par Rotula. Créez-le avec Fichier > Créer le NIP du poste d’impression (un NIP n’est jamais enregistré en clair). + + + + Géré par votre organisation + + + Vérifier les mises à jour une fois par jour + + + Lorsque cette option est activée, Rotula demande une fois par jour à la page du projet sur GitHub si une version plus récente existe. Rien sur vous ni sur cet ordinateur n’est envoyé, et rien n’est téléchargé ni installé. + + + Rotula {0} est disponible. Vous pouvez le télécharger à partir de la page de la version. + + + Mise à jour disponible + + + Voir la version + + + Fermer cet avis + + + Le navigateur n’a pas pu être ouvert. La page de la version est {0} + + + Afficher l’étiquette telle qu’imprimée ou telle que conçue + + + Telle qu’imprimée + + + Affiche l’étiquette comme elle sort de l’imprimante, tournée ou inversée en miroir selon les réglages d’impression de l’étiquette. + + + Telle que conçue + + + Affiche l’étiquette à l’endroit, comme vous la modifiez dans la zone de conception. + + + Affichée telle qu’imprimée : l’orientation d’impression de cette étiquette est inversée ; l’imprimante l’imprime donc à l’envers. + + + Affichée telle qu’imprimée : cette étiquette est réglée pour s’imprimer en miroir ; la gauche et la droite sont donc inversées. + + + Affichée telle qu’imprimée : cette étiquette est réglée pour s’imprimer à l’envers et en miroir. + + + Affichée telle que conçue, comme vous la modifiez. Choisissez « Telle qu’imprimée » pour voir comment elle sort de l’imprimante. + + + S’imprime à l’envers + + + S’imprime en miroir + + + S’imprime à l’envers, en miroir + + + Orientation d’impression + + + La zone de conception affiche l’étiquette à l’endroit pour que vous puissiez la modifier. Ouvrez l’aperçu pour la voir telle qu’imprimée. Modifiez ce réglage dans les propriétés de l’étiquette. + + + Feuilles d’étiquettes (PDF)... + + + Feuilles d’étiquettes (PDF)... + + + Place les étiquettes sur des feuilles pour une imprimante de bureau (laser ou jet d’encre) et les enregistre en PDF. + + + Feuilles d’étiquettes + + + Imprimez des étiquettes sur des feuilles avec une imprimante de bureau (laser ou jet d’encre). Rotula enregistre les feuilles en PDF à leur taille réelle ; imprimez-le en taille réelle (100 %). + + + Des données sont chargées : les enregistrements cochés vont sur les feuilles, avec leurs copies et leurs numéros de série, comme à l’impression. + + + Feuille + + + Les feuilles de fournisseurs différents ayant la même taille d’étiquette peuvent varier d’un millimètre. Imprimez une page sur du papier ordinaire, superposez-la à une feuille d’étiquettes et ajustez les marges au besoin. + + + Disposition personnalisée + + + {0}, {1} par feuille ({2} x {3}), {4} + + + {0} x {1} po + + + {0} x {1} mm + + + Papier + + + Tailles en pouces + + + Toutes les tailles en millimètres + + + Toutes les tailles en pouces + + + Largeur du papier + + + Hauteur du papier + + + Largeur de l’étiquette + + + Hauteur de l’étiquette + + + Étiquettes en largeur + + + Étiquettes en hauteur + + + Marge du haut + + + Marge de gauche + + + En largeur : d’un bord à l’autre + + + En hauteur : d’un bord à l’autre + + + La distance entre le bord gauche (ou supérieur) d’une étiquette et le même bord de la suivante : la taille de l’étiquette plus l’espace entre les étiquettes. + + + Commencer à la position + + + Sur une feuille déjà entamée, la première position libre. Les positions sont comptées par rangées à partir du coin supérieur gauche. Vous pouvez aussi cliquer sur une position dans l’aperçu. + + + Ordre de remplissage + + + Repères + + + Imprimez le PDF en taille réelle (100 %), sans « Ajuster à la page », sinon les étiquettes ne seront pas alignées sur la feuille. + + + Aperçu de la feuille + + + Cliquez sur une position pour commencer à cet endroit. Les positions grises sont déjà utilisées ; les numéros indiquent où vont les premières étiquettes. + + + Position {0} : déjà utilisée + + + Position {0} : étiquette {1} + + + Position {0} : vide + + + Enregistrer en PDF... + + + Étiquettes : {0}. Feuilles : {1} ({2} par feuille). + + + {0} par feuille. Les copies proviennent d’une colonne des données ; le nombre de feuilles n’est donc connu qu’à la création du PDF. + + + {0} : tapez un nombre. + + + Copies : tapez un nombre entier de 1 à {0}. + + + Création des feuilles... + + + {2} enregistré (étiquettes : {0}, feuilles : {1}). Imprimez-le en taille réelle (100 %). + + + Cela dépasse {0} étiquettes, c’est trop pour un seul PDF. Cochez moins d’enregistrements ou réduisez les copies, et enregistrez le reste dans un autre fichier. + + + Lettre (8,5 x 11 po) + + + A4 (210 x 297 mm) + + + Légal (8,5 x 14 po) + + + Taille personnalisée + + + Par rangées, puis vers le bas + + + Par colonnes, puis vers la droite + + + Aucun + + + Traits de coupe dans les marges + + + Contour de chaque étiquette (essai sur papier ordinaire) + + + Le papier doit mesurer entre {0} et {1} mm de chaque côté. + + + Les étiquettes doivent mesurer au moins {0} mm de largeur et de hauteur. + + + Une feuille peut avoir de 1 à {0} étiquettes en largeur et de 1 à {1} en hauteur. + + + Les marges ne peuvent pas être négatives. + + + Les étiquettes se chevauchent en largeur : d’un bord d’étiquette au suivant, il y a {0} mm, mais une étiquette mesure {1} mm de largeur. La distance doit être au moins égale à la largeur de l’étiquette. + + + Les étiquettes se chevauchent en hauteur : d’un bord d’étiquette au suivant, il y a {0} mm, mais une étiquette mesure {1} mm de hauteur. La distance doit être au moins égale à la hauteur de l’étiquette. + + + Les étiquettes ne tiennent pas dans la largeur du papier : la dernière colonne se termine à {0} mm et le papier mesure {1} mm de largeur. Réduisez la marge de gauche, la distance ou le nombre d’étiquettes en largeur. + + + Les étiquettes ne tiennent pas dans la hauteur du papier : la dernière rangée se termine à {0} mm et le papier mesure {1} mm de hauteur. Réduisez la marge du haut, la distance ou le nombre d’étiquettes en hauteur. + + + Certaines étiquettes arrivent à moins de {0} mm du bord du papier. La plupart des imprimantes de bureau ne peuvent pas imprimer à cet endroit ; gardez donc le texte et les codes à barres loin de ces bords d’étiquette. + + + La position de départ doit être un nombre de 1 à {0}. + + + Le dessin de l’étiquette mesure {0} x {1} mm, soit plus que les étiquettes de {2} x {3} mm de cette feuille. Il est imprimé à sa propre taille, centré, et coupé aux bords de l’étiquette. Choisissez une feuille avec de plus grandes étiquettes ou redimensionnez le dessin. + + + Le dessin de l’étiquette mesure {0} x {1} mm, soit moins que les étiquettes de {2} x {3} mm de cette feuille. Il est imprimé à sa propre taille, centré sur chaque étiquette. + + + {0} attend NOM=VALEUR, mais a reçu « {1} ». + + + « {1} » n’est pas une valeur valide pour {0}. + + + rotula {0} : imprime des étiquettes Rotula sans le concepteur, pour les scripts, les planificateurs de tâches et les systèmes d’entrepôt. + +Commandes : + rotula print ÉTIQUETTE.rotula --printer NOM | --host ADRESSE [options] + Imprime une étiquette. Options : + --port N port TCP de --host (9100 par défaut) + --copies N copies de l’étiquette, ou de chaque enregistrement + --data FICHIER enregistrements d’un fichier .csv, .txt, .xlsx ou .json (une étiquette par enregistrement) + --sheet NOM la feuille Excel à lire + --set NOM=VALEUR la valeur d’un champ (répétez pour d’autres champs) + --answer NOM=VALEUR la réponse à une question que l’étiquette pose à l’impression + --dpi 203|300|600 résolution de la tête d’impression (par défaut : celle indiquée par le pilote Windows) + --darkness 0-30 noirceur envoyée avant les étiquettes + --speed 1-14 vitesse envoyée avant les étiquettes + --no-status ne demande pas l’état de l’imprimante avant chaque travail + --no-verify n’attend pas que l’imprimante indique qu’elle a terminé + --dry-run affiche le ZPL sans imprimer (--output FICHIER l’enregistre dans un fichier) + --reference TEXTE votre propre référence, reprise dans le résultat + --counter-file FICHIER où les numéros de série sont mémorisés (par défaut : le même fichier que l’application) + rotula status --printer NOM | --host ADRESSE [--port N] + Demande à l’imprimante si elle peut imprimer maintenant. + rotula printers + Liste les imprimantes Windows. + rotula serve [--settings FICHIER] [--urls URL] + Lance l’API REST. « rotula serve --init [--label-folder DOSSIER] » crée un fichier de paramètres de départ avec une nouvelle clé. + rotula new-key + Crée une nouvelle clé d’API et le hachage à mettre dans le fichier de paramètres. + +Toutes les commandes acceptent --json (sortie pour les programmes) et --lang en|es|pt-BR|fr-CA. +Codes de sortie : 0 terminé, 1 échec de l’impression, 2 ligne de commande incorrecte, 3 erreurs dans l’étiquette, 4 imprimante pas prête, 5 bloqué par une stratégie, 6 annulé. + + + Le fichier de paramètres {0} a été créé. Ajoutez vos imprimantes dans allowedPrinters, puis lancez « rotula serve ». + + + Le fichier de paramètres {0} existe déjà ; il n’a pas été modifié. + + + L’option {0} doit être suivie d’une valeur. + + + Nouvelle clé d’API (affichée une seule fois ; remettez-la au système qui appelle le serveur) : {0} +Hachage à ajouter dans apiKeyHashes du fichier de paramètres : {1} + + + Indiquez exactement un fichier d’étiquette, par exemple : rotula print expedition.rotula --printer "Quai 1" + + + Enregistrement {0} : + + + Lancez « rotula help » pour voir les commandes et les options. + + + Commande inconnue « {0} ». + + + Option inconnue {0}. + + + Avertissement : + + + Des réponses aux questions de l’étiquette manquent ou ne sont pas valides. L’étiquette demande : {0}. Les questions marquées * sont obligatoires; les autres prennent leur valeur par défaut. + + + Une clé d’API valide est requise. Envoyez-la dans l’en-tête X-Api-Key. + + + La requête n’a pas pu être lue : {0} + + + Le nombre de copies doit être un entier de 1 à {0}. + + + Le fichier de données {0} n’a pas pu être lu : {1} + + + Le fichier de données {0} n’existe pas.{1} + + + Les données de {0} doivent être une liste d’enregistrements (objets de noms de champs et de valeurs) ou un seul enregistrement. {1} + + + La résolution doit être de 203, 300 ou 600 ppp. + + + Ce serveur n’accepte pas les étiquettes envoyées comme contenu (allowInlineLabels est désactivé). Indiquez un fichier d’étiquette situé dans un de ses dossiers d’étiquettes. + + + Envoyez soit le nom d’un fichier d’étiquette (label), soit le contenu de l’étiquette (labelContent), pas les deux. + + + L’étiquette {0} est introuvable. + + + L’étiquette {0} n’est pas un fichier .rotula situé dans un des dossiers d’étiquettes de ce serveur. Indiquez-la par rapport à un dossier d’étiquettes, sans lettre de lecteur ni « .. ». + + + Indiquez un fichier d’étiquette (label) ou envoyez le contenu de l’étiquette (labelContent). + + + L’étiquette {0} dépasse la limite de {1} Ko. + + + Les données ne contiennent aucun enregistrement; rien n’a été imprimé. + + + Aucun enregistrement n’a produit d’étiquette; rien n’a été envoyé. Voyez les problèmes indiqués dans le résultat. + + + Le fichier {0} n’a pas pu être écrit : {1} + + + Indiquez soit une imprimante Windows, soit l’adresse d’une imprimante réseau, pas les deux. + + + L’imprimante {0} ne figure pas dans la liste des imprimantes autorisées de ce serveur (allowedPrinters). Rien n’a été envoyé. + + + Indiquez une imprimante : une imprimante Windows (printer) ou l’adresse d’une imprimante réseau (host). + + + L’impression avec le moteur d’impression est désactivée par la stratégie de votre organisation sur cet ordinateur. Adressez-vous à votre service informatique. + + + Une requête peut contenir au plus {0} enregistrements. + + + Envoyez soit les valeurs d’une étiquette, soit une liste d’enregistrements, pas les deux. + + + [par défaut] + + + Aucune imprimante n’est installée dans Windows. + + + [hors ligne] + + + Rien n’a été imprimé (essai). Voici le ZPL qui serait envoyé à {0}. + + + {0} étiquette(s) envoyée(s) à {1}. + + + {0} enregistrement(s), {1} étiquette(s), envoyé(s) à {2} en {3} travail(aux). + + + Le serveur n’a pas pu écouter sur {0} : {1} + + + Moteur d’impression Rotula à l’écoute sur {0}. Paramètres : {1}. Appuyez sur Ctrl+C pour l’arrêter. + + + « {0} » dans apiKeyHashes n’est pas un hachage de clé (64 caractères hexadécimaux). Créez-en un avec « rotula new-key ». + + + Le dossier d’étiquettes {0} n’existe pas. + + + Le fichier de paramètres {0} ne peut pas être utilisé : {1} + + + Le fichier de paramètres {0} n’existe pas. Créez-en un avec « rotula serve --init ». + + + Aucun dossier d’étiquettes n’est indiqué (labelFolders) et les étiquettes envoyées comme contenu sont désactivées; aucune étiquette ne peut donc être imprimée. + + + Le fichier de paramètres {0} ne contient aucune clé d’API (apiKeyHashes). Créez-en une avec « rotula new-key ». + + + Aucune imprimante n’est encore autorisée (allowedPrinters); toute demande d’impression sera donc refusée. + + + Le serveur accepte les appels d’autres ordinateurs en http non chiffré : les clés d’API et les données des étiquettes circulent sur le réseau sans chiffrement. Utilisez https. + + diff --git a/src/Rotula.App/Resources/Strings.pt-BR.resx b/src/Rotula.App/Resources/Strings.pt-BR.resx new file mode 100644 index 0000000..a31bf0d --- /dev/null +++ b/src/Rotula.App/Resources/Strings.pt-BR.resx @@ -0,0 +1,6687 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Clique em um elemento para selecioná-lo. Arraste para movê-lo. As teclas de seta movem a seleção um ponto; com Shift, oito pontos. Pressione Delete para removê-lo. + + + Área de design + + + Cancelar + + + Criar + + + O Rotula não pôde ser iniciado. Nada foi alterado. Os detalhes foram salvos em {0}. Informe o problema anexando esse arquivo. + + + Algo inesperado deu errado na última ação. Sua etiqueta continua aberta; por segurança, salve-a com outro nome. Os detalhes foram salvos em {0}. + + + Não foi possível exportar + + + A exportação terminou com avisos + + + Corrija os erros primeiro + + + A etiqueta tem erros, por isso ainda não pode ser convertida em ZPL nem impressa. Corrija estes problemas e tente novamente: + + + Não foi possível usar a imagem + + + Não foi possível importar o ZPL + + + Algumas partes não foram importadas + + + Tamanho da etiqueta + + + Nova etiqueta + + + Não foi possível abrir o arquivo + + + A etiqueta não foi impressa + + + "{0}" tem alterações não salvas. Deseja salvá-las antes? + + + Alterações não salvas + + + Não foi possível salvar a etiqueta + + + Sem título + + + Retângulo + + + Círculo + + + Campo de dados + + + Exemplo + + + Texto + + + Imagem + + + Linha + + + Texto + + + Windows Latin-1 (1252) + + + Página de código 850 (impressoras antigas) + + + UTF-8 (recomendado) + + + Code 128 + + + Code 39 + + + Code 93 + + + Data Matrix + + + EAN-13 + + + Intercalado 2 de 5 + + + PDF417 + + + Código QR + + + UPC-A + + + Automático + + + EAN-128 + + + Nenhum + + + Modo UCC + + + Canto superior esquerdo (^FO) + + + Linha de base / inferior esquerdo (^FT) + + + 180° (de cabeça para baixo) + + + 0° (normal) + + + 270° no sentido horário + + + 90° no sentido horário + + + Preto + + + Branco + + + Acima das barras + + + Abaixo das barras + + + Floyd-Steinberg (fotos) + + + Pontilhado ordenado + + + Limiar fixo + + + De cabeça para baixo + + + Normal + + + Pontos + + + Polegadas + + + Milímetros + + + Horizontal + + + Vertical + + + Contínuo + + + Marca preta + + + Sensor de espaçamento (gap) + + + Térmica direta + + + Transferência térmica (ribbon) + + + 203 DPI + + + 300 DPI + + + 600 DPI + + + Alta (30%) + + + Baixa (7%) + + + Média (15%) + + + Quartil (25%) + + + Centro + + + Justificado + + + Esquerda + + + Direita + + + {0} está danificado ou foi editado manualmente e não pode ser lido. Abra uma cópia de segurança, se tiver uma. + + + {0} foi salvo por uma versão mais recente do Rotula (formato de arquivo {1}). Atualize o Rotula para abri-lo. + + + {0} não é um arquivo de etiqueta do Rotula. Escolha um arquivo que termine em .rotula. + + + Não foi possível ler {0}. Verifique se o arquivo ainda existe e se você tem permissão para abri-lo. + + + Não foi possível gravar o arquivo {0}. Verifique se a pasta existe, se há espaço livre em disco e se você tem permissão para gravar nela; depois tente novamente ou escolha outro local. + + + Todos os arquivos + + + Imagens + + + Documento PDF + + + Imagem JPEG + + + Imagem PNG + + + Etiquetas do Rotula + + + Arquivos ZPL + + + {0} não é uma imagem que o Rotula consiga ler. Escolha um arquivo PNG, JPG, BMP ou GIF. + + + Não foi possível interpretar o ZPL. Verifique se o arquivo contém uma etiqueta que começa com ^XA e termina com ^XZ. + + + A importação de ZPL ainda não está disponível nesta versão. Você pode criar a etiqueta aqui e exportar o ZPL dela. + + + Linha {0}: {1} + + + o nome do contador está faltando ou não é válido (use letras, dígitos e sublinhados) + + + o preenchimento com zeros deve estar entre 0 e 18 + + + o valor inicial não é válido + + + par + + + ímpar + + + O Code 128 só codifica caracteres comuns do teclado. Remova os acentos e símbolos especiais dos dados. + + + O caractere {0} não pode ser usado nos dados do Code 128 porque a impressora o interpreta como um comando. Remova-o. + + + O Code 39 aceita somente letras maiúsculas, dígitos e - . espaço $ / + %. Altere os dados para usar apenas esses caracteres. + + + O Code 93 só codifica caracteres comuns do teclado. Remova os acentos e símbolos especiais dos dados. + + + O número de colunas deve estar entre {0} e {1}. + + + O código de barras não tem dados. Digite o texto ou número a codificar. + + + Os dados do Data Matrix são longos demais (máximo de {0} caracteres). Encurte o texto. + + + Este tipo de código de barras aceita somente dígitos (0 a 9). Remova as letras ou símbolos. + + + O último dígito não é o dígito verificador correto do EAN-13. Ele deveria ser {0}; ou remova o último dígito e deixe a impressora adicioná-lo. + + + O EAN-13 precisa de {0} dígitos (o dígito verificador é adicionado automaticamente) ou de {1} dígitos incluindo o dígito verificador. + + + A altura das barras deve estar entre {0} e {1} pontos. + + + O Intercalado 2 de 5 precisa de uma quantidade {0} de dígitos com esta opção de dígito verificador. Adicione ou remova um dígito, ou altere a opção de dígito verificador. + + + O tamanho do módulo deve estar entre {0} e {1} pontos. + + + A largura da barra estreita deve estar entre {0} e {1} pontos. + + + O nível de segurança do PDF417 deve estar entre {0} e {1}. + + + Os dados do PDF417 são longos demais (máximo de {0} caracteres). Encurte o texto. + + + O modelo do QR deve ser 1 ou 2. Escolha 2, a menos que seu leitor precise do modelo antigo. + + + Os dados do código QR são longos demais (máximo de {0} caracteres). Encurte o texto. + + + O número de linhas deve ser 0 (automático) ou estar entre {0} e {1}. + + + Este tipo de código de barras não é compatível. Escolha outro tipo. + + + O último dígito não é o dígito verificador correto do UPC-A. Ele deveria ser {0}; ou remova o último dígito e deixe a impressora adicioná-lo. + + + O UPC-A precisa de {0} dígitos (o dígito verificador é adicionado automaticamente) ou de {1} dígitos incluindo o dígito verificador. + + + A proporção entre barra larga e estreita deve estar entre {0} e {1}. + + + Este elemento ultrapassa a etiqueta, por isso parte dele não será impressa. Mova-o para dentro da etiqueta. + + + A posição deve estar entre {0} e {1} pontos. Mova o elemento para dentro da etiqueta. + + + O tipo de elemento {0} não pode ser convertido em ZPL. Exclua-o. + + + Erro: + + + A altura da fonte deve estar entre {0} e {1} pontos. + + + O diâmetro do círculo deve estar entre {0} e {1} pontos. + + + O arredondamento dos cantos deve estar entre {0} e {1}. + + + O tamanho e a espessura devem estar entre {0} e {1} pontos. + + + O tamanho da imagem não é válido (largura até {0}, altura até {1} pontos). Reduza a imagem. + + + Esta imagem não tem nenhuma figura. Escolha um arquivo de imagem nas propriedades. + + + O limiar deve estar entre {0} e {1}. + + + Algumas barras parecem um código de barras desenhado com linhas. Verifique essa parte da etiqueta. + + + Este Code 128 usa códigos de invocação (>) que são mantidos como foram digitados. + + + O nível de qualidade {0} do Data Matrix não é compatível e foi ignorado. + + + Este código QR usa o modo manual de caracteres, que é mantido como foi digitado. + + + O comando {0} não é reconhecido pelo designer. Ele foi mantido como ZPL bruto e será impresso sem alterações. + + + A codificação de caracteres muda para ^CI{0} no meio da etiqueta. O texto a partir desse ponto é lido com essa codificação. + + + O conjunto de caracteres ^CI{0} é um conjunto nacional e foi apenas aproximado. + + + O remapeamento de caracteres em ^CI é ignorado. + + + O conjunto de caracteres ^CI{0} não é compatível, por isso alguns caracteres podem aparecer errados. + + + Um campo não tem ^FS no final. Ele foi fechado automaticamente. + + + Não foi possível ler o arquivo ({0}). Verifique se ele existe e se você tem permissão para abri-lo. + + + A etiqueta usa uma fonte personalizada (^A@). A visualização usa uma fonte substituta. + + + Um gráfico usa o formato {0}, que não é compatível. Ele foi mantido como ZPL bruto. + + + Um gráfico tem dados inválidos ou é grande demais (total de {0} bytes; o limite é 16 MB por arquivo). Ele foi mantido como ZPL bruto. + + + Um gráfico tem menos dados do que declara. Ele foi importado até onde os dados vão. + + + Salvar uma imagem na impressora (^IS {0}) não é compatível. Isso foi mantido como ZPL bruto. + + + Não foi possível interpretar parte do arquivo ({0}). O restante foi importado. + + + A origem da etiqueta (^LH) vem depois de alguns campos, por isso só afeta o que vem em seguida. + + + O arquivo contém {0} etiquetas. Somente a primeira foi aberta. + + + Um segundo ^XA começa antes do fim da etiqueta anterior. + + + A etiqueta não tem ^XZ no final. Ela foi fechada automaticamente. + + + O comprimento da etiqueta (^LL) está faltando, por isso foi estimado. Verifique-o nas propriedades da etiqueta. + + + O arquivo não começa com ^XA. Mesmo assim, ele foi lido como uma única etiqueta. + + + A largura da etiqueta (^PW) está faltando, por isso foi estimada. Verifique-a nas propriedades da etiqueta. + + + Um ^XZ sem o ^XA correspondente foi ignorado. + + + {0} tem um valor inválido ({1}), por isso foi usado o valor padrão. + + + {0} tem um valor fora do intervalo ({1}), por isso foi ignorado. + + + Um comando bruto contém caracteres que não são ASCII simples. Verifique se ele é impresso corretamente. + + + O arquivo recupera o formato armazenado {0}, que fica na impressora e não pode ser mostrado aqui. + + + O arquivo recupera o gráfico armazenado {0}, que fica na impressora e não pode ser mostrado aqui. + + + O arquivo recupera a imagem armazenada {0}, que fica na impressora e não pode ser mostrada aqui. + + + {0} foi aproximado como {1}. + + + A configuração {0} não é compatível com o designer. Ela foi mantida como ZPL bruto. + + + Armazenar um formato na impressora (^DF {0}) não é compatível. Isso foi mantido como ZPL bruto. + + + Não foi possível decodificar parte do texto com {0}. Verifique as letras acentuadas. + + + Texto fora de qualquer campo foi ignorado: {0} + + + As posições foram convertidas a partir de unidades {0} (fator {1}). + + + A altura da etiqueta deve estar entre {0} e {1} pontos. Altere a altura nas propriedades da etiqueta. + + + A largura da etiqueta deve estar entre {0} e {1} pontos. Altere a largura nas propriedades da etiqueta. + + + A coluna de dados {0} não é usada pela etiqueta. + + + A coluna de cópias {0} não foi encontrada nos dados. Escolha uma coluna existente. + + + O número de cópias ({0}) não é válido. Use um número inteiro a partir de 0. + + + O contador {0} tem uma configuração inválida ({1}). + + + o nome do contador está faltando ou não é válido (use letras, dígitos e sublinhados) + + + O contador {0} ultrapassou seu valor máximo. + + + o preenchimento com zeros deve estar entre 0 e 18 + + + o valor inicial não é válido + + + Um valor entre aspas no arquivo de dados tem aspas de abertura, mas não de fechamento, por isso os últimos valores podem estar errados. Corrija as aspas no arquivo e carregue-o novamente. + + + A variável {0} está associada à coluna {1}, que não está nos dados. + + + A variável {0} da etiqueta não tem uma coluna correspondente nos dados. + + + Não foi possível mesclar uma linha de dados ({0}). Verifique essa linha no arquivo de dados. + + + A quantidade para pausar deve estar entre {0} e {1}. + + + O número de cópias deve estar entre {0} e {1}. + + + O número de réplicas deve estar entre {0} e {1}. + + + O número máximo de linhas deve estar entre {0} e {1}. + + + A largura do bloco de texto deve estar entre {0} e {1} pontos. + + + O caractere {0} não pode ser impresso com a codificação de caracteres escolhida. Escolha UTF-8 nas propriedades da etiqueta ou remova o caractere. + + + Problema encontrado ({0}). Revise as configurações deste elemento. + + + A variável {0} não está declarada. Adicione-a às variáveis da etiqueta ou corrija o nome. + + + Aviso: + + + Descer + + + Subir + + + Sobre o Rotula + + + Alinhar + + + Inferior + + + Centralizar horizontalmente + + + Centralizar verticalmente + + + Esquerda + + + Direita + + + Superior + + + _Organizar + + + Avançar + + + Trazer para a frente + + + Copiar + + + Recortar + + + Excluir + + + Distribuir + + + Horizontalmente + + + Verticalmente + + + Duplicar + + + _Editar + + + Sair + + + Exportar + + + Documento PDF... + + + Imagem JPEG... + + + Imagem PNG... + + + Arquivo ZPL... + + + _Arquivo + + + Mostrar grade + + + Aj_uda + + + Importar ZPL... + + + Idioma + + + Igual ao Windows + + + Menu principal + + + Nova... + + + Abrir... + + + Colar + + + Imprimir... + + + Abrir recente + + + Refazer + + + Refazer {0} + + + Girar 90° no sentido anti-horário + + + Girar 90° no sentido horário + + + Salvar + + + Salvar como... + + + Selecionar tudo + + + Selecionar o próximo elemento + + + Selecionar o elemento anterior + + + Recuar + + + Enviar para trás + + + Alinhar à grade + + + Alinhar às guias + + + Desfazer + + + Desfazer {0} + + + Pontos + + + Polegadas + + + Milímetros + + + Unidades das réguas + + + E_xibir + + + Tamanho real (100%) + + + Ajustar a etiqueta à janela + + + Ampliar + + + Reduzir + + + Código de barras + + + Padrões e impressora + + + {0} + + + Imagem + + + Etiqueta + + + Camadas + + + Margens (pontos) + + + {0} elementos estão selecionados. Mova, alinhe ou organize todos juntos, ou selecione um único elemento para editar suas propriedades. + + + Posição + + + Trabalho de impressão + + + Propriedades + + + Seleção + + + Forma + + + Tamanho da etiqueta + + + Texto + + + Elementos + + + Variáveis + + + Código ZPL (somente leitura) + + + 100 x 150 mm + + + 100 x 50 mm + + + 2 x 1 pol + + + 4 x 3 pol + + + 4 x 6 pol + + + Tamanho personalizado + + + A visualização é aproximada: o texto é desenhado com uma fonte substituta, por isso o formato das letras e as quebras de linha podem diferir um pouco na etiqueta impressa. + + + Observação: a visualização é aproximada + + + A etiqueta não tem um tamanho válido, por isso não pode ser desenhada. Defina largura e altura maiores que zero nas propriedades da etiqueta. + + + A etiqueta é grande demais para ser desenhada com este zoom. Reduza o zoom e tente novamente. + + + Não foi possível imprimir a etiqueta. Verifique a conexão da impressora e tente novamente. + + + Altura das barras (pontos) + + + Quebra automática (ajustar o texto em uma caixa) + + + Alinhamento + + + Recuo deslocado (pontos) + + + Espaço extra entre linhas (pontos) + + + Máximo de linhas + + + Largura da caixa (pontos) + + + Adicionar dígito verificador + + + Escolher outra imagem... + + + Modo do Code 128 + + + Cor + + + Colunas (0 = automático) + + + Arredondamento dos cantos (0 a 8) + + + Dados (use {{nome}} para variáveis) + + + Altura padrão da fonte (pontos) + + + Largura padrão da fonte (pontos) + + + Rotação padrão + + + Diâmetro (pontos) + + + Direção + + + Conversão para preto e branco + + + Resolução da impressora (DPI) + + + Codificação de caracteres + + + Altura da fonte (pontos) + + + Fonte (deixe vazio para usar a padrão da etiqueta) + + + Uma única letra ou dígito. 0 é a fonte escalável. + + + Largura da fonte (pontos, 0 = automático) + + + Altura (pontos) + + + Imprimir o texto junto às barras + + + Posição do texto + + + Deixe vazio para usar o padrão da etiqueta. + + + Inverter preto e branco + + + Altura + + + Unidade + + + Largura + + + Comprimento (pontos) + + + Inferior + + + Esquerda + + + Direita + + + Superior + + + Detecção da mídia + + + Tipo de mídia + + + Tamanho do módulo (pontos) + + + Largura da barra estreita (pontos) + + + Nome + + + Orientação de impressão + + + A posição se refere a + + + Canto superior esquerdo, ou linha de base do texto / canto inferior esquerdo. + + + Pausar a cada N etiquetas (0 = nunca) + + + Nível de segurança (1 a 8) + + + PDF417 compacto (truncado) + + + Correção de erros + + + Modelo QR (1 ou 2) + + + Cópias + + + Réplicas de cada número de série + + + Impressão inversa (inverte o que está embaixo) + + + Rotação + + + Padrão da etiqueta + + + Linhas (0 = automático) + + + Tipo de código de barras + + + Texto + + + Espessura da linha (pontos) + + + Limiar (0 a 255) + + + Nome da variável + + + Variáveis (uma por linha: nome=valor de exemplo) + + + Use uma variável em um texto ou nos dados de um código de barras escrevendo {{nome}}. + + + Proporção larga/estreita (2,0 a 3,0) + + + Largura (pontos) + + + X (pontos) + + + Y (pontos) + + + Régua horizontal + + + Régua vertical + + + Redimensionar o painel de camadas + + + Adicionado: {0} + + + Barra de status + + + Exportado: {0} + + + Importado: {0}. Salve a etiqueta para mantê-la. + + + {0} elementos selecionados + + + Nova etiqueta criada + + + Nenhum problema encontrado + + + Nada selecionado + + + Não há nada para colar. Copie um elemento primeiro. + + + 1 elemento selecionado + + + Aberto: {0} + + + Enviado para a impressora + + + {0} erros, {1} avisos + + + Salvo: {0} + + + Selecionado: {0} + + + Zoom + + + ZPL copiado para a área de transferência + + + Não foi possível criar o código ZPL + + + Código de barras + + + Retângulo + + + Círculo + + + Campo de dados + + + Imagem + + + Linha + + + Código QR + + + Texto + + + Barra de edição + + + Barra de arquivo + + + Barra de exibição + + + adição + + + alinhamento + + + avanço de nível + + + trazer para a frente + + + exclusão + + + distribuição + + + duplicação + + + configurações da etiqueta + + + movimentação + + + colagem + + + alteração de propriedade + + + reordenação + + + redimensionamento + + + rotação + + + recuo de nível + + + enviar para trás + + + A fonte deve ser uma única letra ou dígito. + + + O tamanho da etiqueta não é válido. A largura pode ter no máximo {0} pontos e a altura no máximo {1} pontos. Digite um tamanho menor. + + + Isso não é um número inteiro. Digite apenas dígitos, por exemplo 120. + + + Isso não é um número. Digite um número como 12,5. + + + O valor deve estar entre {0} e {1}. Digite um valor nesse intervalo. + + + Este campo não pode ficar vazio. Digite um número. + + + O texto é longo demais. Use no máximo {0} caracteres. + + + Cada linha deve ter o formato nome=valor. Os nomes podem usar letras, dígitos e sublinhados, e cada nome só pode aparecer uma vez. + + + Copiar ZPL + + + Fechar + + + Parar + + + Interrompido. Nada mais foi feito. + + + Algo inesperado deu errado ({0}). Nada foi alterado; tente novamente. + + + O Windows não tem nenhuma impressora chamada "{0}". Atualize a lista de impressoras e escolha outra, ou verifique se a impressora está instalada. + + + Você não tem permissão para imprimir em "{0}". Peça acesso ao administrador ou escolha outra impressora. + + + A impressora "{0}" está offline. Ligue-a, verifique o cabo e confirme que ela não está pausada; depois tente novamente. + + + A impressora em {0}:{1} não respondeu a tempo. Verifique se ela está ligada e na mesma rede; depois tente novamente. + + + O dispositivo em {0}:{1} recusou a conexão. Verifique a porta (a maioria das impressoras de etiquetas usa a 9100) e se a porta de rede da impressora está ativada. + + + Não é possível alcançar {0}:{1}. Verifique o endereço, o cabo de rede ou o Wi-Fi, e se a impressora está ligada. + + + A impressão foi cancelada. Nada mais foi enviado. + + + A etiqueta tem erros, por isso nada foi enviado. Corrija os problemas abaixo e tente novamente. + + + As configurações da impressora estão fora do intervalo, por isso nada foi enviado. Corrija os valores e tente novamente. + + + A etiqueta contém caracteres que o conjunto de caracteres {0} não consegue imprimir. Altere o conjunto de caracteres da etiqueta para UTF-8 ou remova esses caracteres. + + + A impressão por impressoras do Windows só está disponível no Windows. Use uma impressora de rede ou salve o arquivo ZPL. + + + A impressora relatou um problema inesperado ({0}). Verifique a conexão e tente novamente, ou salve o arquivo ZPL e envie-o de outra forma. + + + A intensidade deve ser um número de 0 a 30. Altere-a ou deixe-a vazia para manter a configuração da própria impressora. + + + A velocidade deve ser um número de 1 a 14 polegadas por segundo. Altere-a ou deixe-a vazia para manter a configuração da própria impressora. + + + A largura da etiqueta está fora do intervalo: ela deve estar entre 1 e 9999 pontos na resolução escolhida. Verifique a largura e a resolução (DPI). + + + A altura da etiqueta está fora do intervalo: ela deve estar entre 1 e 32000 pontos na resolução escolhida. Verifique a altura e a resolução (DPI). + + + Imprimir + + + Escolha uma impressora, verifique as configurações dela e imprima a etiqueta. + + + Impressora + + + Configuração da impressora + + + Impressora do Windows (USB ou instalada) + + + Impressora de rede (TCP/IP) + + + Impressora + + + Atualizar a lista de impressoras + + + Procurando impressoras + + + O Windows não tem impressoras instaladas. Instale o driver da impressora ou mude para uma impressora de rede e informe o endereço dela. + + + A impressora "{0}" usada da última vez não está instalada neste computador. Escolha outra impressora. + + + (padrão) + + + (offline) + + + Nome do host ou endereço IP + + + Porta + + + Resolução + + + Detecção da mídia + + + Largura da etiqueta (mm) + + + Altura da etiqueta (mm) + + + Intensidade (0-30) + + + Velocidade (1-14 pol/s) + + + Deixe a intensidade e a velocidade vazias para manter as configurações armazenadas na impressora. + + + Cópias + + + Imprimir + + + Salvar como ZPL... + + + Imprimir etiqueta de teste + + + Imprime uma pequena etiqueta de exemplo para verificar a conexão, a largura e os acentos. + + + A etiqueta tem erros e ainda não pode ser impressa: + + + A etiqueta será impressa com os valores do registro de dados mostrado na área de design. + + + Registros a imprimir + + + Marque os registros que deseja imprimir. Cada registro marcado imprime sua própria etiqueta, na ordem dos dados, com a impressora e as cópias escolhidas aqui. + + + Nenhum registro está marcado, por isso não há nada para imprimir. Marque pelo menos um registro na lista acima. + + + A janela Dados define as cópias de cada registro, por isso essa configuração é usada no lugar deste número. + + + Enviando a etiqueta para a impressora... + + + Enviando a etiqueta de teste... + + + Salvando o arquivo ZPL... + + + Enviado para a impressora ({0} cópias). + + + A etiqueta de teste foi enviada para a impressora. + + + Salvo: {0}. + + + Nenhuma impressora está selecionada. Escolha uma impressora instalada ou mude para uma impressora de rede e informe o endereço dela. + + + Informe o nome do host ou o endereço IP da impressora de rede, por exemplo 192.168.1.50. + + + A porta deve ser um número inteiro de 1 a 65535 (a maioria das impressoras de etiquetas usa a 9100). + + + {0} deve ser um número. Digite apenas dígitos, por exemplo 12. + + + As cópias devem ser um número inteiro de 1 a 99999999. + + + Uma etiqueta com quantidade de impressão (^PQ) + + + Repetir a etiqueta uma vez por cópia + + + Ignorar o registro e continuar + + + Parar no registro + + + Números + + + Letras e dígitos + + + Recomeçar + + + Relatar um erro + + + Impressora do Windows + + + Impressora de rede (TCP/IP) + + + Arquivos CSV e Excel + + + Mesclar dados + + + Abrir arquivo de dados... + + + Recarregar + + + Remover os dados + + + Nenhum arquivo de dados aberto + + + Os dados ficam neste computador. Apenas as configurações abaixo são salvas no arquivo da etiqueta, nunca os dados em si. + + + Mescle uma planilha com a sua etiqueta + + + Abra um arquivo CSV ou Excel (.xlsx). Cada linha vira uma etiqueta, e cada coluna pode preencher uma {{variável}} da etiqueta. Depois você pode percorrer os registros e imprimir todos. + + + Esta etiqueta lembra o seu arquivo de dados, mas os dados não estão carregados. Escolha Recarregar para lê-lo novamente ou abra outro arquivo. + + + Dados + + + Variáveis + + + Contadores + + + Cópias e trabalhos + + + Verificar e imprimir + + + Planilha + + + Delimitador + + + Detectar automaticamente + + + vírgula (,) + + + ponto e vírgula (;) + + + tabulação + + + Delimitador detectado: {0} + + + Delimitador usado: {0} + + + A primeira linha contém os nomes das colunas + + + {0} registros, {1} colunas + + + Visualização dos dados + + + Mostrando os primeiros {0} registros. + + + Avisos sobre o arquivo de dados + + + {0} foi carregado: {1} registros. + + + Lendo {0}... + + + Não foi possível abrir {0}. Se ele estiver aberto no Excel ou em outro programa, feche-o lá e tente novamente; caso contrário, verifique se o arquivo ainda existe. + + + Não foi possível ler {0} como um arquivo CSV ou Excel (.xlsx). Abra-o no Excel, salve-o novamente como .xlsx ou .csv e tente outra vez. + + + {0} não tem colunas. Verifique se a primeira linha contém os nomes das colunas ou desmarque "A primeira linha contém os nomes das colunas". + + + Abra um arquivo de dados primeiro. + + + O arquivo de dados não tem registros (linhas) para mesclar. + + + Primeiro registro + + + Registro anterior + + + Próximo registro + + + Último registro + + + Registro atual + + + Ir para o registro + + + Ir + + + Digite um número de registro de 1 a {0}. + + + Nenhum registro + + + Registro {0} de {1} + + + Selecionado {0} de {1} (linha {2}) + + + Linha {0} (não selecionada; {1} selecionados) + + + Registro {0} de {1} (nenhum selecionado) + + + Registros a imprimir + + + Marque os registros a imprimir. Espaço marca a linha destacada, Shift+clique marca um intervalo e Ctrl+A marca todas as linhas mostradas. Somente os registros marcados são impressos, verificados ou exportados. + + + Nº + + + Número da linha + + + Selecionar ou desmarcar todos os registros mostrados + + + Selecionar o registro {0} + + + Selecionar todos + + + Selecionar os mostrados: {0} + + + Desmarcar todos + + + Desmarcar os mostrados: {0} + + + Inverter a seleção + + + Inverter os mostrados: {0} + + + {0} de {1} selecionados + + + Mostrando {0} de {1} registros + + + Nenhum registro corresponde ao filtro. Altere o texto ou limpe o filtro. + + + Filtro + + + Filtrar registros + + + Digite um texto para mostrar apenas os registros que o contêm (maiúsculas e minúsculas não importam). + + + Coluna para pesquisar + + + Todas as colunas + + + Mostrar apenas os selecionados + + + Limpar o filtro + + + Imprimir {0} etiquetas selecionadas + + + Imprimir 1 etiqueta selecionada + + + Imprimir todas as {0} etiquetas + + + Nenhum registro está selecionado, por isso não há nada para imprimir, verificar ou exportar. Selecione registros na guia Dados primeiro. + + + Nenhum registro está selecionado. Selecione pelo menos um registro na guia Dados. + + + Enviar os {0} registros selecionados (de {3}) para {1}? Isso resulta em {2} etiquetas, contando as cópias. As etiquetas começarão a ser impressas imediatamente. + + + Enviar {0} registros para {1}? Isso resulta em {2} etiquetas, contando as cópias. As etiquetas começarão a ser impressas imediatamente. + + + Mostrar este registro na área de design + + + Cada variável da etiqueta recebe o valor da coluna com o mesmo nome. Escolha outra coluna aqui se os nomes não corresponderem. + + + A etiqueta ainda não tem variáveis. Adicione um texto com {{nome}} (ou um campo de dados) e ele aparecerá aqui. + + + Mesmo nome (automático) + + + Preenchida pela coluna {0} + + + Nenhuma coluna correspondente: escolha uma + + + Preenchida pelo contador + + + Nenhum contador com este nome: adicione um na guia Contadores + + + Itens a verificar + + + Adicionar contador + + + Remover + + + Ainda não há contadores. Um contador gera um número de série como 0001, 0002, 0003 para cada registro. + + + (sem nome) + + + Nome + + + Digite isto na etiqueta + + + Conta com + + + Começar em + + + Incremento + + + Preenchimento com zeros (dígitos) + + + Prefixo + + + Sufixo + + + Quando o contador se esgotar + + + Primeiros valores + + + Configurações do contador: {0}. + + + O incremento deve ser um número inteiro, por exemplo 1 ou -1. + + + O preenchimento com zeros deve ser um número inteiro de 0 a 18. + + + Cópias de cada registro + + + O número de cópias vem de + + + A quantidade da própria etiqueta + + + O mesmo número para todos os registros + + + Uma coluna dos dados + + + Cópias + + + Coluna com as cópias + + + As cópias devem ser um número inteiro de 1 a 99999999. + + + Como as cópias são escritas + + + Uma quantidade de impressão (^PQ) é menor e mais rápida. Repetir a etiqueta só é necessário se a sua impressora ignorar o ^PQ. + + + Trabalhos e problemas + + + Um registro com erros + + + Registros por trabalho de impressão (0 = um único trabalho) + + + Contado em registros, não em cópias. Trabalhos menores pesam menos para a impressora e permitem parar no meio. + + + Os registros por trabalho devem ser um número inteiro de 0 a 1000000. + + + Verificar e imprimir os registros selecionados + + + Verifique primeiro para ver quais registros selecionados têm problemas. A impressão envia cada registro selecionado e válido para a impressora à esquerda, na ordem dos dados. Escolha os registros na guia Dados. + + + Verificar os registros selecionados + + + Exportar ZPL... + + + Imprimir tudo + + + Gravar um arquivo por trabalho ao exportar + + + Verificando todos os registros... + + + Enviando os registros para a impressora... + + + Gravando o ZPL... + + + {0} registros verificados: {1} prontos, {2} com erros. + + + Mostrando os primeiros {0} de {1} problemas. + + + Todos os {0} registros estão prontos para imprimir. + + + {0} registros têm erros e ficariam de fora. Veja a lista de cada problema e corrija os dados ou a etiqueta. + + + Imprimir todos os registros + + + Enviar {0} registros para {1}? As etiquetas começarão a ser impressas imediatamente. + + + {0} etiquetas enviadas para a impressora em {1} trabalho(s). + + + {0} etiquetas gravadas em um arquivo. + + + {0} etiquetas gravadas em {1} arquivos. + + + Substituir os arquivos existentes? + + + Já existem {0} arquivo(s) com os mesmos nomes, que serão substituídos. + + + {0} trabalho(s) já tinham sido enviados antes da interrupção; não os imprima de novo. + + + Interrompido. {0} trabalho(s) já foram enviados para a impressora. + + + {0} registro(s) ficaram de fora por causa de erros; veja a lista. + + + A impressão parou no registro {0} por causa da opção "parar". + + + Nenhuma etiqueta foi gerada porque todos os registros têm erros. Verifique os registros e a etiqueta. + + + Problemas por registro + + + Registro + + + Tipo + + + O que aconteceu + + + Erro + + + Aviso + + + Este arquivo de etiqueta indicava um arquivo de dados em um local que o Rotula não segue automaticamente (um caminho de rede, um endereço da web ou um caminho relativo), por isso ele foi ignorado para sua segurança. Escolha "Abrir arquivo de dados" para selecionar o arquivo você mesmo. + + + O arquivo de dados lembrado ({0}) não está em um disco local, por isso o Rotula não o abrirá automaticamente. Escolha "Abrir arquivo de dados" para selecioná-lo você mesmo. + + + O arquivo de dados {0} não foi encontrado. Ele pode ter sido movido ou renomeado; escolha "Abrir arquivo de dados" para localizá-lo. + + + Caminho do arquivo de dados lembrado + + + Minimizar + + + Maximizar + + + Restaurar + + + Fechar + + + Etiqueta + + + Propriedades da etiqueta + + + Impressora + + + Atualizar a lista de impressoras + + + Mesclar dados... + + + Abrir arquivo de dados... + + + Zoom + + + Etiqueta + + + Impressora + + + Escolha a impressora desta etiqueta. A configuração da impressora fica em Impressora > Imprimir. + + + Selecionar impressora + + + Visualizar + + + Mostrar a etiqueta maior, como ela será desenhada + + + Imprimir + + + Imprimir... (Ctrl+P) + + + Selecionar + + + Redimensionar o painel de propriedades + + + Objetos + + + Tipo do objeto selecionado + + + SAÍDA ZPL + + + Exportar .zpl + + + Tamanho, resolução e formato da etiqueta + + + {0} objetos + + + 1 objeto + + + nenhum selecionado + + + {0} selecionado(s) + + + Visualização da etiqueta + + + Visualização - {0} + + + Mostrar o código ZPL + + + Mostrar ou ocultar o painel do código ZPL + + + 1 erro + + + {0} erros + + + 1 aviso + + + {0} avisos + + + Visualização aproximada + + + Ainda não há objetos + + + Escolha uma ferramenta à esquerda para adicionar texto, um código de barras ou uma forma. + + + Tamanho personalizado... + + + Tamanhos comuns + + + Resolução de impressão + + + Escolha o tamanho da etiqueta e a resolução de impressão + + + tamanho da etiqueta + + + Tamanho, margens e padrões da impressora + + + Nada está selecionado, por isso estas são as configurações da etiqueta inteira. Clique em um objeto na área de design ou na lista para editá-lo. + + + {0} no total + + + Largura ({0}) + + + Altura ({0}) + + + mm + + + pol + + + pontos + + + {0} dpi detectados no driver da impressora + + + Não foi possível ler a resolução no driver; escolha-a manualmente. + + + Lendo o driver da impressora... + + + Usar o tamanho da impressora ({0}) + + + Preenche a largura e a altura da etiqueta com o tamanho de papel informado pelo driver da impressora + + + Esta etiqueta foi criada a {0} dpi. Ela será convertida automaticamente para {1} dpi para ser impressa no mesmo tamanho. + + + Mudar a etiqueta para {0} dpi + + + Converte o design aberto para a resolução da impressora. Você pode desfazer com Ctrl+Z. + + + Itens a verificar após a conversão ({0}) + + + Parte da etiqueta não pôde ser convertida com exatidão. Verifique o resultado impresso ou mude a etiqueta para a resolução da impressora e revise-a. + + + O código de barras "{0}" tem largura de barra de {1} pontos, que passa a {2} pontos na nova resolução. O código fica um pouco mais largo ou mais estreito do que antes. Verifique se ele ainda é lido. + + + Em "{0}", o valor "{1}" seria de {2} pontos na nova resolução, mas a impressora permite {3}. Ele foi definido como {3}. Verifique o elemento. + + + "{0}" contém ZPL bruto, que não pode ser redimensionado automaticamente. Ele mantém as posições originais em pontos e pode ser impresso menor ou maior do que o restante. Edite-o ou substitua-o por elementos comuns. + + + O gráfico armazenado "{0}" é um bitmap fixo e não pode ser redimensionado para a nova resolução. Ele será impresso com outro tamanho físico. Substitua-o por um gráfico feito para esta resolução. + + + Esta etiqueta contém um elemento que não pode ser convertido automaticamente de {0} dpi para {1} dpi. Nada foi alterado. + + + Não é possível mudar a etiqueta + + + OK + + + Salvar + + + Não salvar + + + Informação + + + Aviso + + + Erro + + + Pergunta + + + Sobre o Rotula + + + Versão {0} + + + Designer de etiquetas e códigos de barras para Windows (ZPL), gratuito e de código aberto (MIT) + + + Funciona offline. Sem telemetria, sem acesso à rede. + + + A visualização na tela é aproximada: o texto é desenhado com uma fonte substituta, por isso o formato das letras e as quebras de linha podem diferir um pouco na etiqueta impressa. + + + https://github.com/P4Software/Rotula + + + Abrir a página do projeto no navegador + + + Não foi possível abrir o navegador. A página do projeto é {0} + + + Avisos de terceiros + + + Avisos de terceiros + + + O arquivo de avisos não faz parte desta compilação. Ele está disponível em https://github.com/P4Software/Rotula, no arquivo THIRD-PARTY-NOTICES.md. + + + Bem-vindo ao Rotula + + + Crie uma etiqueta, confira-a na tela e imprima-a na sua impressora compatível com ZPL. + + + Nova etiqueta + + + Escolha um tamanho e comece a criar. + + + Abrir etiqueta + + + Continue um arquivo .rotula salvo. + + + Importar ZPL + + + Transforme um arquivo ZPL existente em um design editável. + + + Etiquetas recentes + + + As etiquetas que você salvar ou abrir aparecerão aqui. + + + Começar com uma etiqueta em branco + + + Dica: arraste os cantos para redimensionar, use as teclas de seta para mover e Ctrl+Z para desfazer qualquer etapa. + + + Tela inicial + + + Arquivos de banco de dados + + + Escolher um arquivo de banco de dados SQLite + + + Dados de + + + Arquivo (CSV ou Excel) + + + Banco de dados + + + Banco de dados + + + Banco de dados: nenhum dado lido ainda + + + Conexão de banco de dados: {0} + + + {0} linhas foram lidas da conexão de banco de dados "{1}". + + + {0} linhas, {1} colunas + + + Conexão salva + + + Nova conexão... + + + Gerenciar conexões... + + + Ainda não há conexões salvas. Escolha "Nova conexão..." para adicionar a primeira. + + + Escolha primeiro uma conexão salva ou crie uma com "Nova conexão...". + + + Alterar a fonte de dados... + + + Voltar aos dados carregados + + + Esta etiqueta usa a conexão salva "{0}". Conecte e recarregue os dados. + + + O Rotula não se conectou a nada. Ele só se conecta depois que você clica no botão, e só executa a consulta abaixo se ela passar na verificação de somente leitura. + + + Consulta salva nesta etiqueta + + + Conectar e recarregar os dados + + + Não há nenhuma conexão salva chamada "{0}" neste computador. Abra "Gerenciar conexões...", adicione uma conexão com exatamente o nome "{0}" e clique no botão novamente. + + + Tabela ou exibição + + + Consulta + + + Pesquisar tabelas e exibições + + + Tabelas e exibições + + + exibição + + + Não há tabelas nem exibições para mostrar. Altere a pesquisa ou verifique se esta conta pode ler o banco de dados. + + + Visualização + + + A visualização mostra as primeiras 100 linhas. "Usar estes dados" lê todas as linhas até o máximo. + + + Escolha uma tabela ou exibição para ver as linhas dela. + + + Consulta SELECT + + + Só é permitido um único SELECT. Escreva @nome onde um valor deve ser preenchido, por exemplo WHERE Id = @id. + + + Tudo certo: esta consulta apenas lê dados. + + + Parâmetros + + + Valores para os @nomes da sua consulta. Escolha Número ou Data quando a coluna não for de texto. + + + Tipo do parâmetro + + + Texto + + + Número + + + Data + + + O valor de {0} não é um número. Digite dígitos com um ponto decimal opcional, por exemplo 12.5. + + + O valor de {0} não é uma data. Digite-a assim: 2026-03-31. + + + Executar + + + Executa a consulta (somente leitura) e mostra as linhas abaixo. + + + Resultado + + + Detalhe técnico do banco de dados + + + Usar estes dados + + + Lê as linhas e as usa como registros desta etiqueta. + + + Máximo de linhas + + + São lidas no máximo esta quantidade de linhas (de 1 a 1.000.000). Um aviso aparece quando o resultado foi cortado. + + + O máximo de linhas deve ser um número inteiro de 1 a {0}. + + + Este banco de dados não tem tabelas nem exibições que esta conta possa ver. + + + Lendo a lista de tabelas... + + + Lendo {0}... + + + Executando a consulta... + + + Lendo todas as linhas de {0}... + + + Conexões de banco de dados + + + O Rotula apenas lê dados. Sempre que possível, use uma conta de banco de dados somente leitura. + + + Conexões salvas + + + Nada salvo ainda. Preencha o formulário e escolha Salvar. + + + Nova + + + Excluir + + + Detalhes da conexão + + + Nome + + + As etiquetas se referem a uma conexão por este nome, por isso escolha um nome curto que você vá manter. + + + O nome não pode ser alterado porque há etiquetas que se referem a ele. Para usar outro nome, crie uma nova conexão. + + + Digite um nome para esta conexão. + + + Já existe uma conexão chamada "{0}". Escolha outro nome ou selecione-a na lista para editá-la. + + + Tipo de banco de dados + + + SQL Server + + + PostgreSQL + + + MySQL / MariaDB + + + Arquivo SQLite + + + ODBC + + + Servidor + + + Porta + + + A porta deve ser um número inteiro de 1 a 65535, ou ficar vazia para usar a habitual. + + + Nome do banco de dados + + + Arquivo SQLite + + + O arquivo é aberto como somente leitura, por isso o Rotula nunca pode alterá-lo. + + + Procurar... + + + Carregar lista + + + Lendo a lista do servidor... + + + Escolha um banco de dados do servidor ou digite o nome dele. + + + Preencha primeiro o endereço do servidor e o login e depois carregue a lista. Você também pode simplesmente digitar o nome. + + + Digite a senha para carregar a lista ou simplesmente digite o nome do banco de dados. + + + Bancos de dados encontrados: {0}. Escolha um ou digite o nome dele. + + + O servidor tem mais bancos de dados do que é possível mostrar; estes são os primeiros {0}. Escolha um ou digite o nome dele. + + + Este login não pode abrir nenhum banco de dados no servidor. Digite o nome. + + + Este tipo de servidor não consegue listar seus bancos de dados. Digite o nome. + + + Interrompido. Pressione Carregar lista para tentar novamente ou digite o nome. + + + Digite a senha novamente para carregar a lista: a senha salva só é usada enquanto o servidor e o usuário continuarem os mesmos. + + + Não foi possível ler a lista: talvez este login não tenha permissão para ver outros bancos de dados. Digite o nome. + + + Não foi possível ler a lista. {0} Você ainda pode digitar o nome. + + + Escolha uma fonte de dados instalada neste computador ou digite o nome dela. + + + Não há fontes de dados ODBC instaladas neste computador. Digite o nome. + + + sistema + + + Nome da fonte de dados ODBC (DSN) + + + ou uma cadeia de conexão + + + Se a cadeia de conexão contiver uma senha, o Rotula a move para o armazenamento protegido e a remove do texto salvo. + + + Entrar com + + + Minha conta do Windows + + + Nome de usuário e senha + + + Nome de usuário + + + Senha + + + Há uma senha salva para esta conexão, e ela nunca é mostrada. Deixe este campo vazio para mantê-la ou digite uma nova para substituí-la. + + + A senha é armazenada de forma protegida somente para a sua conta do Windows e nunca é mostrada novamente. + + + Segurança + + + Criptografar a conexão + + + Mantenha esta opção ativada. Desative-a somente para um servidor confiável na sua própria rede. + + + A criptografia está desativada: qualquer pessoa no caminho da rede pode ler os dados e a senha. Use isto somente com um servidor local confiável. + + + Confiar no certificado deste servidor + + + Somente para um servidor que você conhece e controla, como um servidor de teste. Caso contrário, alguém poderia se passar pelo servidor. + + + O Rotula não verificará quem é o servidor, então alguém na rede poderia se passar por ele e ler os dados e a senha. Use isto somente com um servidor que você controla. + + + Testar conexão + + + A conexão funciona. + + + Você alterou esta conexão, e a senha salva não pode ser lida de volta para um teste. Digite a senha novamente para testá-la. + + + Salvar + + + A conexão "{0}" foi salva. + + + A conexão "{0}" foi excluída. + + + Excluir conexão + + + Excluir a conexão "{0}"? As etiquetas que a usam pedirão uma conexão com este nome na próxima vez que forem abertas. + + + Salvando... + + + Excluindo... + + + Testando a conexão... + + + Não foi possível verificar o certificado de segurança do servidor. Se este for o seu próprio servidor (por exemplo, um servidor de teste), você pode confiar nele para esta conexão. Caso contrário, peça ao administrador um certificado válido. + + + Confiar neste servidor e salvar + + + nome + + + tipo de banco de dados + + + método de login + + + servidor + + + porta + + + nome do banco de dados + + + nome de usuário + + + arquivo SQLite + + + cadeia de conexão + + + nome da fonte de dados + + + tempo limite do comando + + + tempo limite de conexão + + + Algo deu errado ao ler o banco de dados ({0}). Tente novamente e, se o problema continuar, verifique os detalhes da conexão. + + + O Rotula não conseguiu se conectar ao banco de dados. Verifique os detalhes da conexão e se o servidor está em execução; depois tente novamente. + + + O servidor não aceitou o nome de usuário ou a senha. Verifique-os em "Gerenciar conexões..." e tente novamente. + + + O banco de dados demorou demais para responder. Tente novamente, restrinja a consulta ou fale com o administrador do banco de dados. + + + Não foi possível alcançar o servidor. Verifique o nome do servidor e a porta, a sua rede ou VPN, e tente novamente. + + + O banco de dados não foi encontrado. Verifique o nome do banco de dados ou o local do arquivo e tente novamente. + + + Esta conta não tem permissão para ler esses dados. Peça acesso de leitura ao administrador do banco de dados. + + + O Rotula apenas lê dados, por isso esta consulta não foi executada. {0} + + + O banco de dados não conseguiu executar a consulta. Verifique os nomes das tabelas e colunas e a grafia da consulta; depois tente novamente. + + + Interrompido. Nada foi alterado. + + + O limite de linhas {0} ultrapassa o máximo de {1}. Escolha um limite menor. + + + O driver ou a fonte de dados ODBC não está instalado neste computador. Instale-o ou escolha outro nome de fonte de dados. + + + O certificado de segurança do servidor não é confiável. Se este for o seu próprio servidor, abra "Gerenciar conexões..." e escolha confiar nele para esta conexão. Caso contrário, peça ao administrador um certificado válido. + + + A configuração "{0}" está faltando ou não é permitida. Corrija-a em "Gerenciar conexões..." e tente novamente. + + + Não há nenhuma conexão salva chamada "{0}". Crie-a com exatamente esse nome em "Gerenciar conexões...". + + + Só é permitida uma única consulta SELECT. Reescreva-a como um SELECT. + + + Só é permitida uma instrução. Remova tudo o que vem depois do primeiro ponto e vírgula. + + + Digite primeiro uma consulta SELECT. + + + A palavra "{0}" não é permitida porque pode alterar dados. Se for o nome de uma coluna, coloque-a entre aspas. + + + SELECT ... INTO não é permitido porque cria tabelas ou arquivos. Remova a parte INTO. + + + Não é permitido bloquear linhas (FOR UPDATE, FOR SHARE, LOCK IN SHARE MODE). Remova essa parte. + + + A função "{0}" não é permitida porque pode ler arquivos, acessar outros servidores ou executar código. Remova-a. + + + Um comentário que começa com /*! é executado como SQL no MySQL, por isso não é permitido. Remova-o. + + + Há aspas ou um comentário sem fechamento. Feche-os e tente novamente. + + + A consulta é longa demais. Encurte-a. + + + O parâmetro @{0} não tem valor. Preencha-o em Parâmetros. + + + Somente as primeiras {0} linhas foram lidas. Aumente o máximo de linhas ou restrinja a consulta para ler o restante. + + + O arquivo com as conexões salvas está danificado e foi ignorado. Adicione suas conexões novamente; o arquivo danificado é mantido ao lado, com a extensão .damaged. + + + Você alterou onde ou como esta conexão faz login, por isso a senha salva não foi usada. Digite a senha novamente. + + + O tipo de fonte de dados "{0}" não está disponível. O complemento dele pode estar faltando ou desativado. + + + Esta consulta pode ser lida de mais de uma forma (por exemplo, uma barra invertida antes de uma aspa), por isso não foi executada. Use duas aspas simples ('') em vez da barra invertida. + + + provedor + + + serviço + + + arquivo da autoridade certificadora + + + opções adicionais + + + O arquivo com as conexões salvas é grande demais e foi ignorado. Adicione suas conexões novamente. + + + Não é possível ler a senha salva. Provavelmente ela foi salva por outro usuário do Windows ou em outro computador. Abra "Gerenciar conexões..." e digite-a novamente. + + + O Rotula não conseguiu salvar as conexões. Verifique se você pode gravar na sua pasta de usuário (AppData) e tente novamente. + + + A proteção de senhas não está disponível neste sistema, por isso não é possível salvar conexões que precisam de senha. Entre com a sua conta do Windows ou use um arquivo SQLite. + + + criptografia + + + confiar no certificado do servidor + + + Code 11 + + + Industrial 2 de 5 + + + Standard 2 de 5 + + + LOGMARS + + + Codabar + + + MSI + + + Plessey + + + EAN-8 + + + UPC-E + + + Extensão UPC/EAN + + + POSTNET + + + PLANET + + + GS1 DataBar + + + Código Aztec + + + MicroPDF417 + + + Codablock + + + MaxiCode + + + TLC39 + + + Code 49 + + + Omnidirecional + + + Truncado + + + Empilhado + + + Empilhado omnidirecional + + + Limitado + + + Expandido + + + Nenhum + + + Um dígito módulo 10 + + + Dois dígitos módulo 10 + + + Dígitos módulo 11 e módulo 10 + + + Codablock A (caracteres do Code 39) + + + Codablock E (Code 128, FNC1 automático) + + + Codablock F (Code 128) + + + Lineares + + + Matriciais 2D + + + Empilhados + + + Postais + + + Dígitos verificadores (1 ou 2) + + + Dígitos verificadores + + + Mostrar os dígitos verificadores no texto + + + Caractere inicial + + + Caractere final + + + Tipo de GS1 DataBar + + + Altura do separador (1-2) + + + Segmentos por linha (2-22) + + + Os dados têm códigos ECI + + + Correção de erros / tamanho + + + Símbolo de menu (configuração do leitor) + + + Símbolos do conjunto + + + ID do conjunto (opcional) + + + Modo (0-33) + + + Tipo de Codablock + + + Adicionar caracteres de verificação + + + Modo (2-6) + + + Número do símbolo (1-8) + + + Total de símbolos (1-8) + + + Módulo do MicroPDF417 (pontos) + + + Linha do MicroPDF417 (pontos) + + + Modo inicial + + + Caracteres por linha + + + Automático + + + Rotação (não pode ser impressa para este código de barras: escolha Padrão da etiqueta) + + + Sobre a visualização + + + A impressora desenha este código de barras; a visualização mostra um marcador de posição. + + + Este símbolo Aztec (Rune, menu, anexação estruturada ou ECI) é desenhado pela impressora; a visualização mostra um marcador de posição. + + + Os símbolos Rune, de menu, de anexação estruturada e com ECI são desenhados pela impressora; nesses casos, a visualização mostra um marcador de posição. + + + Na visualização, a proporção entre barras largas e estreitas é desenhada como 2:1. + + + Na visualização, a proporção entre barras largas e estreitas é desenhada como 3:1. + + + Na visualização, as larguras das barras e dos espaços seguem a largura do módulo; a impressora real usa as dimensões fixas do USPS. + + + A parte composta 2D do GS1 DataBar não é compatível; nos tipos empilhados, a altura da barra é a altura do símbolo inteiro. + + + O Code 11 aceita somente dígitos e o hífen (-). Remova qualquer outro caractere. + + + O Code 11 usa 1 ou 2 dígitos verificadores. Escolha um desses dois valores. + + + O LOGMARS aceita somente letras de A a Z, dígitos, espaço e - . $ / + %. Remova qualquer outro caractere. + + + A impressora sempre imprime o texto abaixo de um código LOGMARS, por isso desativá-lo não tem efeito. Ative o texto para corresponder ao que será impresso. + + + O Codabar aceita somente dígitos e - $ : / . + nos dados. As letras inicial e final, de A a D, são escolhidas nas opções, não digitadas nos dados. + + + Os caracteres inicial e final do Codabar devem ser A, B, C ou D. Escolha um deles. + + + Os dados MSI podem ter no máximo {0} dígitos com esta opção de dígito verificador. Encurte os dados. + + + Escolha uma das opções de dígito verificador do MSI. + + + O Plessey aceita somente os dígitos de 0 a 9 e as letras maiúsculas de A a F. Remova qualquer outro caractere. + + + O EAN-8 precisa de {0} dígitos (o dígito verificador é adicionado automaticamente) ou de {1} dígitos incluindo o dígito verificador. + + + O último dígito não é o dígito verificador correto do EAN-8. Ele deveria ser {0}; ou remova o último dígito e deixe a impressora adicioná-lo. + + + O UPC-E precisa do código de 10 dígitos de fabricante e produto que a impressora encurta, ou do código UPC-E de 6 dígitos (opcionalmente com o sistema numérico 0 na frente e um dígito verificador no final). Altere a quantidade de dígitos. + + + O UPC-E só pode usar o sistema numérico 0. Comece o código com 0. + + + Estes dígitos não podem ser encurtados para um código UPC-E, porque o UPC-E precisa de zeros em posições específicas. Altere os dígitos ou use UPC-A. + + + O último dígito não é o dígito verificador correto do UPC-E. Ele deveria ser {0}; ou remova o último dígito e deixe a impressora adicioná-lo. + + + Uma extensão UPC/EAN precisa de exatamente 2 ou 5 dígitos. + + + Os dados POSTNET devem ter uma destas quantidades de dígitos: {0}. A impressora adiciona o dígito verificador. + + + Os dados PLANET devem ter uma destas quantidades de dígitos: {0}. A impressora adiciona o dígito verificador. + + + Escolha um dos tipos de GS1 DataBar. + + + Este tipo de GS1 DataBar precisa de até {0} dígitos (zeros são adicionados na frente) ou de {1} dígitos incluindo o dígito verificador. Encurte os dados. + + + O último dígito não é o dígito verificador GS1 correto. Ele deveria ser {0}; ou remova o último dígito e deixe a impressora adicioná-lo. + + + Dados compostos (2D) depois de um | não são compatíveis. Remova o | e tudo o que vem depois dele. + + + O GS1 DataBar Expanded precisa de cadeias de elementos GS1 com os identificadores entre parênteses, por exemplo (01)12345678901231(10)ABC. Verifique os parênteses e os identificadores. + + + Uma cadeia de elementos GS1 só pode conter caracteres comuns do teclado. Remova os acentos e símbolos especiais. + + + A cadeia de elementos GS1 é longa demais (máximo de {0} caracteres). Encurte-a. + + + O elemento (01) precisa de um GTIN de 14 dígitos formado apenas por números. Corrija os dígitos dentro dos parênteses. + + + O GS1 DataBar Limited só pode codificar números até 1999999999999, por isso o primeiro dos seus 13 dígitos deve ser 0 ou 1. Altere o número ou escolha outro tipo. + + + Os segmentos por linha devem ser um número par entre {0} e {1}. + + + A altura do separador deve estar entre {0} e {1} módulos. + + + O valor de correção de erros do Aztec não é válido. Use 0 (automático), 1 a 99 (percentual), 101 a 104 (tamanho compacto), 201 a 232 (tamanho completo) ou 300 (Rune). + + + O número de símbolos em um conjunto de anexação estruturada deve estar entre {0} e {1}. + + + O ID do conjunto pode ter no máximo {0} caracteres. Encurte-o. + + + O ID do conjunto só pode usar caracteres comuns do teclado, e não vírgula, ^ ou ~. Remova-os. + + + Os dados do Aztec são longos demais (máximo de {0} caracteres). Encurte o texto. + + + O modo do MicroPDF417 deve estar entre {0} e {1}. + + + Os dados do MicroPDF417 são longos demais (máximo de {0} caracteres). Encurte o texto. + + + Escolha Codablock A, E ou F. + + + O Codablock A aceita somente letras maiúsculas, dígitos e - . espaço $ / + %. Altere os dados ou escolha Codablock E ou F. + + + O Codablock E e o F só codificam caracteres comuns do teclado. Remova os acentos e símbolos especiais. + + + Os caracteres por linha devem ser 0 (automático) ou estar entre {0} e {1}. + + + Para este tipo de Codablock, o número de linhas deve ser 0 (automático) ou estar entre {0} e {1}. + + + O modo do MaxiCode deve estar entre {0} e {1}. + + + O número total de símbolos MaxiCode deve estar entre {0} e {1}, e o número deste símbolo deve estar entre 1 e o total. + + + O modo {0} do MaxiCode precisa de uma mensagem de transporte: uma classe de serviço de 3 dígitos, um código de país de 3 dígitos e depois o código postal (9 dígitos no modo 2, 6 letras ou dígitos no modo 3). Complete o início dos dados. + + + Os dados do MaxiCode são longos demais (máximo de {0} caracteres). Encurte o texto. + + + O MaxiCode não pode ser girado em ZPL, por isso a rotação definida neste elemento não será impressa. Volte a rotação para o padrão da etiqueta. + + + Os dados do TLC39 começam com um número ECI de 6 dígitos, opcionalmente seguido de uma vírgula, do número de série e de outros campos. Corrija o início dos dados. + + + Cada campo do TLC39 depois do número ECI só pode ter letras e dígitos, até {0} caracteres. Verifique os campos entre as vírgulas. + + + Os dados do TLC39 são longos demais (máximo de {0} caracteres). Encurte o texto. + + + A largura do módulo do MicroPDF417 deve estar entre {0} e {1} pontos. + + + A altura da linha do MicroPDF417 deve estar entre {0} e {1} pontos. + + + O Code 49 só codifica caracteres comuns do teclado. Remova os acentos e símbolos especiais. + + + O modo inicial do Code 49 deve ser automático ou um número de 0 a 5. Escolha um deles. + + + Os dados do Code 49 são longos demais (máximo de {0} caracteres). Encurte o texto. + + + Este GS1 DataBar usa o tipo de simbologia {0}, que o Rotula não suporta (somente os tipos 1 a 6). Ele foi mantido como texto ZPL e continuará sendo impresso, mas não pode ser editado. + + + Este GS1 DataBar tem uma parte composta (2D), que o Rotula não suporta. Ele foi mantido como texto ZPL e continuará sendo impresso, mas não pode ser editado. + + + Este código de barras postal usa o tipo {0}, que o Rotula não suporta (somente POSTNET e PLANET). Ele foi mantido como texto ZPL e continuará sendo impresso, mas não pode ser editado. + + + Somente números pares. Com menos de 22, o símbolo é empilhado em várias linhas. + + + 0 = automático. 1 a 99 = correção de erros mínima em percentual. 101 a 104 = símbolo compacto de 1 a 4 camadas. 201 a 232 = símbolo completo de 1 a 32 camadas. 300 = Aztec Rune. + + + 1 = um único símbolo. De 2 a 26 divide a mensagem nessa quantidade de símbolos. + + + 0 = automático. + + + Os modos 2 e 3 levam uma mensagem de transporte estruturada (classe de serviço, país, código postal); os modos 4 a 6 levam qualquer texto. + + + O modo define o número de colunas e linhas e a correção de erros (consulte o guia de ZPL). + + + Um contador da impressora precisa de um valor inicial de 1 a 40 caracteres formado por letras, dígitos, espaços e - / . com pelo menos um dígito. Altere o texto deste campo. + + + A impressora não consegue contar em um código QR, porque os dados dele começam com um cabeçalho. Desative o contador da impressora para este código de barras. + + + A máscara do contador só pode usar as letras D, H, O, A, N e %, e o incremento só letras, dígitos e %. Corrija a máscara ou o incremento. + + + O caractere que marca as referências a números de campo deve ser um único caractere comum do teclado, e não ^ ou ~. Escolha outro. + + + Os caracteres marcadores do relógio devem ser caracteres comuns do teclado, diferentes entre si, e não ^, ~ ou vírgula. Altere-os. + + + O número do campo deve estar entre {0} e {1}. + + + O texto de solicitação do campo só pode usar letras, dígitos e espaços, até 255 caracteres. Altere o texto. + + + Elipse + + + Linha diagonal + + + Símbolo + + + Elipse + + + Linha diagonal + + + Símbolo + + + Símbolo + + + Inclinação + + + Símbolo + + + Altura do caractere (pontos) + + + Largura do caractere (pontos) + + + Direita (/ do canto inferior esquerdo ao superior direito) + + + Esquerda (\ do canto superior esquerdo ao inferior direito) + + + Marca registrada (R) + + + Direitos autorais (C) + + + Marca comercial (TM) + + + Underwriters Laboratories (UL) + + + Canadian Standards Association (CSA) + + + Horizontal + + + Vertical (de cima para baixo) + + + Invertida (da direita para a esquerda) + + + O deslocamento da etiqueta deve estar entre {0} e {1} pontos. Altere o deslocamento nas configurações da etiqueta. + + + A margem superior da etiqueta deve estar entre {0} e {1} linhas de pontos. Altere-a nas configurações da etiqueta. + + + A alteração de intensidade deve estar entre {0} e {1}. Escolha um valor nesse intervalo. + + + As velocidades de impressão devem estar entre {0} e {1} polegadas por segundo. Altere as velocidades de impressão, de avanço e de recuo para valores nesse intervalo. + + + A fonte {0} não pode usar o arquivo "{1}". O ID de uma fonte é uma letra ou um dígito, e o arquivo deve ter o formato E:NOME.TTF. Corrija o ID da fonte ou o nome do arquivo. + + + O gráfico armazenado {0} não pode ser usado. O nome dele deve ter o formato R:LOGO.GRF e os dados não podem passar de 4 MB. Corrija o nome ou use um gráfico menor. + + + A largura, a altura e a espessura de uma linha diagonal devem estar entre {0} e {1} pontos. Altere o tamanho. + + + A largura, a altura e a espessura de uma elipse devem estar entre {0} e {1} pontos. Altere o tamanho. + + + A altura e a largura de um símbolo devem estar entre {0} e {1} pontos. Altere o tamanho. + + + A largura e a altura de uma caixa de texto devem estar entre {0} e {1} pontos. Altere o tamanho da caixa. + + + Um campo de texto pode ser um bloco com quebra automática ou uma caixa de texto, mas não os dois. Desative um deles. + + + O espaço entre caracteres deve estar entre {0} e {1} pontos. Altere o espaço. + + + A opção {0} deste campo não pode ser usada com o campo ao qual pertence, por isso o campo inteiro foi mantido como ZPL bruto e continuará sendo impresso, mas não pode ser editado. + + + Este campo tem dados e também um contador da impressora, que não podem ser combinados. Ele foi mantido como ZPL bruto e continuará sendo impresso, mas não pode ser editado. + + + A atribuição de fonte {0} vem depois de alguns campos já terem sido definidos, por isso não pode virar uma configuração da etiqueta. Ela foi mantida como ZPL bruto no lugar original. + + + O arquivo de suprimentos "{0}" tem mais de {1} bytes, por isso não foi lido. Use um arquivo menor. + + + Não foi possível abrir o arquivo de suprimentos "{0}". Verifique se ele existe e se você tem permissão para lê-lo. + + + O arquivo de suprimentos "{0}" não é uma lista de suprimentos válida. Verifique o arquivo ou restaure-o a partir do original. + + + O arquivo de suprimentos "{0}" tem uma versão de formato ({1}) que esta versão do Rotula não entende. Atualize o Rotula ou use outro arquivo. + + + O arquivo de suprimentos "{0}" não tem entradas. Adicione pelo menos um suprimento. + + + O arquivo de suprimentos "{0}" tem mais de {1} entradas, por isso somente as primeiras {1} foram lidas. Divida o arquivo em arquivos menores. + + + A pasta de suprimentos tem mais de {0} arquivos, por isso os excedentes foram ignorados. Remova os arquivos de que não precisa. + + + O item {1} de "{0}" não é uma entrada de suprimento, por isso foi ignorado. + + + A entrada {1} de "{0}" não tem ID, por isso foi ignorada. Dê a cada entrada um ID único. + + + A entrada {1} de "{0}" tem o ID "{2}", que é longo demais ou usa caracteres não permitidos, por isso foi ignorada. Use letras, dígitos, pontos, sublinhados e hífens, até 64 caracteres. + + + O ID "{1}" aparece duas vezes em "{0}", por isso a segunda entrada foi ignorada. Dê a cada entrada um ID único. + + + O suprimento "{0}" não tem nome, por isso foi ignorado. Dê um nome a ele. + + + O campo "{1}" do suprimento "{0}" tem o tipo errado ou é longo demais, por isso o suprimento foi ignorado. Corrija o campo. + + + O suprimento "{0}" não tem largura e altura utilizáveis, por isso foi ignorado. Adicione o tamanho da etiqueta. + + + O valor de {1} do suprimento "{0}" ({2}) está fora dos tamanhos que uma impressora de etiquetas pode usar, por isso o suprimento foi ignorado. Verifique o número e a unidade. + + + O valor de {1} do suprimento "{0}" aparece como {2} mm e {3} pol, que não são o mesmo comprimento, por isso o suprimento foi ignorado. Corrija um dos dois valores. + + + O campo "{1}" do suprimento "{0}" tem o valor {2}, que está fora do intervalo permitido, por isso o suprimento foi ignorado. Corrija o valor. + + + O endereço de origem do suprimento "{0}" não é um endereço https, por isso o suprimento foi ignorado. Use um endereço que comece com https://. + + + O campo "{1}" do suprimento "{0}" parece um preço. O Rotula não guarda preços, por isso o campo foi ignorado. + + + O suprimento "{0}" está marcado como verificado, mas está faltando {1}. Adicione esse dado ou marque o suprimento como não verificado. + + + O suprimento "{0}" não diz de onde vêm os dados dele. Adicione um ID de fonte ou um endereço de origem https. + + + O bloco de descrição do arquivo de suprimentos está incompleto: está faltando {0}. Adicione esse dado. + + + O suprimento "{0}" cita a fonte "{1}", que o arquivo não descreve. Adicione essa fonte ao bloco de descrição. + + + O arquivo diz que {0} entradas estão verificadas, mas são {1}. Atualize as contagens no bloco de descrição. + + + O tamanho do suprimento "{0}" não foi verificado. Meça as etiquetas reais antes de imprimir uma tiragem grande. + + + O espaçamento entre as etiquetas do suprimento "{0}" não é conhecido. Calibre a impressora com este suprimento carregado. + + + O suprimento "{0}" é um rolo contínuo sem altura fixa de etiqueta, por isso a altura da etiqueta foi mantida como estava. Defina a altura de que precisa. + + + Este suprimento tem {0} etiquetas na largura e {1} no comprimento. Por enquanto, apenas uma etiqueta é diagramada, por isso posicione você mesmo as outras etiquetas, se precisar delas. + + + O suprimento "{0}" é uma folha recortada. O tamanho da etiqueta foi aplicado, mas não a disposição da folha. + + + A etiqueta já tem conteúdo, por isso a rotação padrão dos campos não foi alterada para o suprimento "{0}". Altere você mesmo a rotação, se o suprimento precisar. + + + O suprimento "{0}" é redondo ou oval, mas a etiqueta da impressora é retangular e o formato não é desenhado. Mantenha o conteúdo dentro do formato. + + + {0} elementos já estão na etiqueta e agora podem ficar fora do novo tamanho. Verifique se eles ainda cabem. + + + Arquivos do tipo "{0}" não podem ser importados. Salve a lista como .csv ou .xlsx e tente novamente. + + + A lista tem mais de {0} bytes e não foi lida. Use um arquivo menor. + + + Não foi possível ler a lista "{0}". Verifique se ela não está aberta em outro programa e se é um arquivo válido. + + + A lista não tem linhas de dados. Adicione os suprimentos abaixo da linha de cabeçalho. + + + A lista tem mais de {0} linhas, por isso somente as primeiras {0} foram lidas. Divida-a em listas menores. + + + Nenhuma coluna com códigos de peça foi encontrada, por isso nada foi importado. Adicione uma coluna chamada "Part number". + + + Nenhuma coluna com tamanhos de etiqueta foi encontrada, por isso nada foi importado. Adicione uma coluna de tamanho ou uma coluna de largura e outra de altura. + + + A coluna "{0}" parece um preço e foi ignorada. O Rotula não guarda preços. + + + A coluna de {0} foi escolhida pelo conteúdo porque nenhum cabeçalho correspondeu: "{1}". Verifique se é a coluna certa. + + + A linha {0} não tem código de peça, por isso foi ignorada. Adicione o código da peça. + + + A linha {0} ({1}) não tem um tamanho legível, por isso foi ignorada. Escreva o tamanho como largura x altura, com a unidade. + + + A linha {0} ({1}) informa o tamanho "{2}" sem dizer se é em polegadas ou milímetros, por isso foi ignorada. Adicione "in" ou "mm". + + + A linha {0} ({1}) informa dois tamanhos que não coincidem ("{2}"), por isso foi ignorada. Mantenha apenas o correto. + + + A linha {0} ({1}) tem o tamanho "{2}", que está fora dos tamanhos que uma impressora de etiquetas pode usar, por isso foi ignorada. Verifique o número e a unidade. + + + A linha {0} repete o código de peça {1}, por isso foi ignorada. Mantenha uma linha por código de peça. + + + Endereço do servidor + + + O nome ou endereço IP do servidor de banco de dados, por exemplo db.exemplo.com. O administrador do banco de dados pode informá-lo. + + + Porta + + + Nome do banco de dados + + + Nome de usuário + + + Arquivo de banco de dados + + + Nome da fonte de dados (DSN) + + + Cadeia de conexão + + + Criptografar a conexão + + + Mantenha esta opção ativada. Desative-a somente para um servidor confiável na sua própria rede. + + + Confiar no certificado deste servidor + + + Somente para um servidor que você conhece e controla, como um servidor de teste. Caso contrário, alguém poderia se passar pelo servidor. + + + Arquivo da autoridade certificadora (opcional) + + + O servidor está atrás de um pooler de conexões + + + Família do servidor + + + MySQL + + + MariaDB + + + Banco de dados por trás do driver + + + Se você sabe com qual banco de dados o driver ODBC se comunica, escolha-o para que o Rotula aplique as regras de segurança desse banco de dados. Isso nunca afrouxa as verificações. + + + Não tenho certeza + + + SQL Server + + + PostgreSQL + + + MySQL + + + MariaDB + + + SQLite + + + Oracle + + + DuckDB + + + BigQuery + + + Snowflake + + + ClickHouse + + + Informix + + + IBM Db2 + + + Firebird + + + Microsoft Access / Excel (Jet, ACE) + + + Se o certificado do servidor for emitido pela sua própria organização, escolha aqui o arquivo da autoridade certificadora. O administrador pode fornecê-lo a você. + + + Este serviço só aceita login do Microsoft Entra, que precisa de um complemento ainda não instalado. Não é possível conectar-se a ele com nome de usuário e senha. + + + O driver para este tipo de arquivo não foi encontrado neste computador. Instale o "Microsoft Access Database Engine" de 64 bits da Microsoft e reinicie o Rotula. + + + Este servidor é um pooler de conexões. Se algo não funcionar, tente o endereço de conexão direta. + + + Use uma conta de banco de dados que só possa ler (SELECT). Esta fonte de dados não consegue tornar uma sessão somente leitura por conta própria, por isso a conta é a proteção real. + + + MariaDB + + + Amazon RDS para MySQL + + + Amazon Aurora (MySQL) + + + Amazon RDS para PostgreSQL + + + Amazon Aurora (PostgreSQL) + + + Amazon Redshift + + + CockroachDB + + + YugabyteDB (YSQL) + + + Supabase (conexão direta) + + + Supabase (pooler de conexões) + + + Neon (conexão direta) + + + Neon (pooler de conexões) + + + PlanetScale + + + TiDB + + + SingleStore + + + Azure SQL Database + + + Azure Synapse Analytics + + + Microsoft Fabric + + + Google Cloud SQL para MySQL + + + Google Cloud SQL para PostgreSQL + + + Google Cloud SQL para SQL Server + + + Microsoft Access (.mdb, .accdb) + + + Microsoft Excel (.xls, .xlsx) + + + Arquivos de texto e CSV (uma pasta) + + + O complemento da pasta "{0}" não tem um arquivo de descrição válido (pack.json), por isso não foi carregado. Peça uma cópia corrigida a quem forneceu o complemento. + + + O complemento "{0}" precisa de uma versão mais recente do Rotula (interface de dados {1}). Atualize o Rotula e reinicie-o. + + + O complemento "{0}" foi feito para uma versão anterior do Rotula e não pode ser usado. Peça um complemento atualizado. + + + Não foi possível abrir o complemento "{0}". Pode estar faltando um arquivo, algum arquivo pode estar danificado ou o complemento foi feito para outro tipo de computador. Reinstale o complemento. + + + O complemento "{0}" parou com um erro ao iniciar, por isso foi desativado. O Rotula em si não foi afetado. Reinstale ou atualize o complemento. + + + O complemento "{0}" não corresponde ao seu arquivo de descrição, por isso não foi carregado. Reinstale o complemento. + + + Um complemento oferece uma fonte de dados chamada "{0}", que não é um nome válido, por isso o complemento não foi carregado. + + + Um complemento oferece uma fonte de dados chamada "{0}", que já existe, por isso o complemento não foi carregado. Um complemento nunca pode substituir uma fonte de dados. + + + Um complemento chamado "{0}" já está carregado; a segunda cópia foi ignorada. + + + O complemento "{0}" da sua própria pasta não foi carregado. Um complemento é um programa: aprove-o somente se confiar na origem dele. + + + O complemento "{0}" mudou desde que você o aprovou, por isso não foi carregado. Aprove-o novamente somente se você esperava a mudança. + + + A pasta do complemento "{0}" contém atalhos (links) ou é grande demais, por isso foi ignorada. + + + Aprovar este complemento? + + + "{0}" é um complemento da sua própria pasta ({1}). Um complemento é um programa: se você o aprovar, ele poderá fazer tudo o que você pode fazer neste computador, inclusive ler seus arquivos. Aprove-o somente se souber quem o criou e se o obteve de uma fonte confiável. Você pode remover a aprovação depois. + + + Não foi possível usar o complemento "{0}". + + + Alguns complementos de fontes de dados não puderam ser carregados. + + + Um complemento está aguardando a sua aprovação. + + + Detalhes + + + Fechar este aviso + + + Complementos de fontes de dados + + + Estes complementos foram encontrados, mas não foram carregados. O Rotula em si funciona normalmente. + + + Detalhe técnico + + + Revisar... + + + Revisar o complemento {0} + + + O complemento "{0}" foi aprovado e já está disponível. + + + Editor + + + Versão + + + Impressão digital (SHA-256) + + + Pasta + + + Aprovar e carregar + + + Ignorar + + + Ignorar mantém o complemento desativado. O Rotula perguntará novamente na próxima vez que for iniciado. + + + não informado + + + Servidores de banco de dados + + + Serviços na nuvem + + + Arquivos + + + Bancos de documentos + + + Serviços web + + + Outros (drivers ODBC) + + + Mais opções + + + Esta conexão precisa do complemento "{0}", que não está instalado. As configurações dela são mantidas como estão. Instale o complemento e reinicie o Rotula para usar ou editar esta conexão. + + + O Rotula não conseguiu confirmar as configurações deste serviço na documentação do fornecedor. Verifique o endereço e a porta antes de confiar neles. + + + Arquivo + + + Pasta + + + Escolher uma pasta... + + + Este endereço parece ser de {0}. + + + Usar as configurações dele + + + Você alterou onde ou como esta conexão faz login, por isso a senha salva não foi usada. Digite a senha no campo Senha e teste novamente. + + + Escolher... + + + {0} é obrigatório. + + + {0} deve ser um número inteiro de {1} a {2}. + + + Pesquisar suprimentos de etiquetas + + + Pesquise por código da peça, tamanho ou nome + + + Experimente um código de peça como {0} ou um tamanho como 4x6 ou 100x150mm. + + + Marca + + + Todas as marcas + + + Medidas em + + + Polegadas ou mm + + + Polegadas + + + Milímetros + + + Tipo + + + Todos os tipos + + + Rolo + + + Folha + + + Redonda + + + Somente verificados + + + Somente favoritos + + + Tamanhos comuns + + + Favoritos + + + Recentes + + + Resultados + + + Tamanhos próximos + + + Tamanho personalizado... + + + Digite a sua própria largura e altura + + + Tamanho de etiqueta comum + + + {0} suprimentos de etiquetas. Digite para pesquisar. + + + A biblioteca de suprimentos não está disponível. Você ainda pode usar os tamanhos comuns ou um tamanho personalizado. + + + {0} suprimentos encontrados + + + Mostrando {0} de {1} suprimentos. Digite mais para refinar. + + + {0} suprimentos encontrados e {1} tamanhos próximos abaixo + + + Nenhum suprimento corresponde. Tente menos palavras, ou apenas o código da peça ou o tamanho. + + + Carregando a biblioteca de suprimentos... + + + Tamanho verificado + + + Tamanho não verificado + + + O tamanho foi confirmado por duas fontes independentes. + + + O tamanho vem de uma única fonte ou as fontes não coincidem. Meça as etiquetas reais antes de imprimir uma tiragem grande. + + + Meu suprimento + + + Adicionar aos favoritos + + + Remover dos favoritos + + + favorito + + + Criar etiqueta + + + Obtida da sua impressora ({0} dpi). Altere-a se for imprimir em outra. + + + Antes de começar + + + O que se sabe sobre este suprimento + + + Este é um rolo contínuo: digite a altura da etiqueta que você deseja. + + + Altura da etiqueta + + + Este suprimento não tem um tamanho utilizável, por isso não pode ser aplicado. + + + O rolo tem {0} m de comprimento. + + + {0} x {1} mm + + + {0} x {1} pol + + + {0} mm de largura + + + {0} pol de largura + + + {0} mm + + + {0} m + + + {0} mm ({1} pol) + + + {0} na largura, {1} no comprimento + + + Desconhecido. Calibre a impressora com este suprimento carregado. + + + Marca preta no verso + + + Não informado + + + Térmica direta + + + Transferência térmica + + + Térmica direta ou transferência térmica + + + Não informado + + + Rolo de etiquetas cortadas + + + Rolo contínuo + + + Rolo sem liner + + + Formulário sanfonado + + + Folhas avulsas + + + Marca + + + Código da peça + + + Família de produtos + + + Tamanho + + + Tipo de impressão + + + Fornecido como + + + Superfície e adesivo + + + Tubete + + + Etiquetas por rolo + + + Comprimento do rolo + + + Etiquetas na largura + + + Raio dos cantos + + + Espaçamento entre etiquetas + + + Espaçamento entre colunas + + + Espaçamento entre linhas + + + Fontes + + + Verificação + + + Guardar este tamanho como meu próprio suprimento + + + Nome + + + Código da peça (opcional) + + + Salvar como meu suprimento + + + "{0}" salvo. Você pode encontrá-lo pesquisando pelo nome ou pelo código da peça. + + + Dê ao suprimento um nome ou um código de peça. + + + Não foi possível salvar o suprimento: {0} + + + Esse tamanho não pode ser guardado como suprimento: ele está fora dos tamanhos que uma impressora de etiquetas pode usar. + + + Importar uma lista de suprimentos... + + + Escolha uma lista de suprimentos (CSV ou Excel) + + + Importar uma lista de suprimentos + + + Confira o que o Rotula entendeu antes de guardar. Apenas códigos de peça, descrições e tamanhos são lidos; os preços nunca são lidos. + + + {0} linhas lidas, {1} suprimentos encontrados. + + + Não foi possível importar nada desta lista. + + + Linha + + + Código da peça + + + Descrição + + + Tamanho (mm) + + + Tamanho (pol) + + + Certeza + + + Alta + + + Média: confira + + + O tamanho tem uma coluna própria com unidade, ou duas informações sobre ele coincidem. + + + O tamanho foi encontrado em texto livre com unidade. Confira-o com a etiqueta real. + + + Observações sobre a lista + + + Salvar {0} suprimentos + + + {0} suprimentos salvos em {1}. Eles agora fazem parte da pesquisa. + + + Os nomes de marcas e produtos pertencem aos seus proprietários; o Rotula não tem vínculo com eles. + + + Sobre o catálogo de suprimentos + + + Sobre o catálogo de suprimentos + + + A biblioteca de suprimentos lista suprimentos de etiquetas pelo código da peça do fabricante, com o tamanho da etiqueta em milímetros e polegadas, o tipo de impressão e, quando uma fonte informa, o tamanho do tubete, as etiquetas por rolo, as etiquetas na largura e o raio dos cantos. + +Ela contém {0} suprimentos, {1} deles com tamanho verificado (duas fontes independentes coincidem). Códigos de peça e tamanhos são dados de produto que os fabricantes publicam; o Rotula não guarda preços, imagens nem textos de marketing. + +O espaçamento entre etiquetas quase nunca é publicado, por isso o Rotula deixa isso para a impressora: calibre a impressora com o suprimento carregado. Nos suprimentos com várias etiquetas na largura ou no comprimento, e nas folhas avulsas, só o tamanho de uma etiqueta é aplicado. + +Seus próprios suprimentos e as listas de suprimentos que você importar ficam em {2}. Exclua um arquivo dessa pasta para remover os suprimentos dele. + +Os nomes de marcas e produtos pertencem aos seus proprietários; o Rotula não tem vínculo com eles. Encontrou um tamanho errado? Correções são bem-vindas: consulte docs/stock-catalog.md no projeto. + + + Suprimento de etiquetas + + + Suprimento de etiquetas: {0} + + + Suprimento de etiquetas: {0} (não está mais na biblioteca) + + + código da peça + + + tamanho + + + Encontrar um suprimento + + + Tamanho da etiqueta + + + Filtros + + + 1 suprimento encontrado + + + Família de produtos + + + Todas as famílias + + + Igual às suas últimas configurações de impressão ({0} dpi). Altere-a se for imprimir em outra impressora. + + + Os endereços deste serviço têm o formato {0}. + + + Inserir campo + + + Insere um campo de dados na posição do cursor. Você também pode digitar {{ para ver a lista. + + + Carregue os dados primeiro: Impressora > Mesclar dados + + + Abrir Mesclar dados + + + Pesquisar campos + + + Campos disponíveis + + + 1 campo disponível + + + {0} campos disponíveis + + + Nenhum campo corresponde + + + Setas para cima/baixo para escolher, Enter para inserir, Esc para fechar + + + Coluna + + + Contador + + + Variável + + + (vazio) + + + Valor no registro mostrado: {0} + + + Nenhuma coluna chamada {0} nos dados carregados + + + Mostra dados de: {0} + + + Campos dos seus dados + + + Adicione uma coluna à etiqueta como texto, código de barras ou código QR, ou coloque-a no elemento selecionado na etiqueta. + + + Ainda não há campos. Abra primeiro um arquivo de dados ou escolha uma tabela do banco de dados. + + + Adicionar todos os campos à etiqueta + + + Adiciona um texto por coluna, empilhados de cima para baixo + + + Adicionar à etiqueta + + + Adicionar este campo à etiqueta como texto + + + Usar aqui + + + Colocar este campo no elemento selecionado + + + Selecione um texto ou um código de barras na etiqueta para colocar um campo nele. + + + Elemento selecionado: {0} + + + {0} adicionado à etiqueta + + + {0} campos adicionados à etiqueta + + + Nem todos os campos cabem + + + O novo campo não cabe dentro da etiqueta. Aumente a altura da etiqueta ou mova o campo. + + + {0} dos {1} novos campos não cabem dentro da etiqueta. Aumente a altura da etiqueta, use menos campos ou mova-os. + + + {0} colocado em {1} + + + Variáveis da etiqueta + + + inserção de campo + + + adição de campos + + + Valor fixo + + + Campo + + + Escolha se o conteúdo é um valor fixo ou vem de um campo de dados + + + Mostra o campo {0} dos dados. Use Campo para escolher outro. + + + Usar um valor fixo + + + Adicionar como texto + + + Adicionar como código de barras + + + Adicionar como código QR + + + uso de valor fixo + + + Usa campos de dados + + + Mais formas de adicionar este campo + + + + O nome de variável "{0}" não é válido. Use apenas letras, dígitos e sublinhados, e mantenha-o curto. + + + Há duas variáveis chamadas "{0}". Os nomes não diferenciam maiúsculas de minúsculas, por isso dê um nome diferente a cada variável. + + + A variável "{0}" tem o mesmo nome de uma coluna de dados, por isso o valor da própria variável é usado no lugar da coluna. Renomeie uma delas se quiser os dados da coluna. + + + + O formato de data "{1}" da variável "{0}" não pode ser usado. Escolha um formato da lista ou verifique o padrão. + + + O idioma "{1}" escolhido para a variável "{0}" não é conhecido neste computador, por isso o idioma padrão é usado para os nomes de meses e dias. Escolha outro idioma. + + + O deslocamento de data da variável "{0}" é grande demais, ou há deslocamentos demais. Use valores de no máximo {1} e menos deslocamentos. + + + A data calculada para a variável "{0}" está fora das datas que o Rotula consegue tratar. Reduza os deslocamentos de data. + + + A variável "{0}" está definida com uma data fixa, mas nenhuma data foi informada. Informe a data. + + + A variável "{0}" conta dias úteis, o que uma impressora não consegue fazer, por isso o relógio do computador é usado. + + + A variável "{0}" arredonda para o início ou o fim de um período, o que exige o relógio do computador, por isso ele é usado no lugar do relógio da impressora. + + + A variável "{0}" usa o horário UTC, que o relógio da impressora não consegue fornecer, por isso o relógio do computador é usado. + + + O formato de data da variável "{0}" usa "{1}", que a impressora não consegue imprimir, por isso o relógio do computador é usado. Use um formato mais simples para usar o relógio da impressora. + + + A variável "{0}" imprime nomes de meses ou dias no idioma "{1}", mas a impressora os escreve em inglês, por isso o relógio do computador é usado. Escolha inglês ou um formato numérico para usar o relógio da impressora. + + + Uma etiqueta só pode deslocar o relógio da impressora de duas maneiras diferentes, e ambas já estão em uso, por isso a variável "{0}" usa o relógio do computador. + + + A variável "{0}" usa uma data fixa, que não é um relógio, por isso não há relógio da impressora para ler e o computador é usado. + + + O relógio da impressora não pode ser usado dentro de um código de barras, por isso a variável "{0}" usa o relógio do computador. + + + Este campo já usa os seus próprios marcadores de relógio e nenhum está livre para a variável "{0}", por isso o relógio do computador é usado. + + + Nenhum caractere marcador de relógio está livre neste campo para a variável "{0}", por isso o relógio do computador é usado. + + + + O contador "{0}" tem uma configuração que não é válida: {1}. Corrija-a nas configurações da variável. + + + O contador "{0}" passou do seu maior número e para em vez de recomeçar. Permita mais dígitos ou reinicie o contador. + + + O início, o incremento, o tipo ou o preenchimento do contador "{0}" foi alterado depois que números já tinham sido usados (último número: {1}). A impressão fica bloqueada até você aceitar a alteração ou reiniciar o contador. + + + Uma impressão anterior reservou os números {1} a {2} do contador "{0}" e nunca terminou, por isso esses números foram pulados. Se nada foi impresso com eles, você pode devolvê-los nas configurações do contador. + + + O arquivo de contadores "{0}" está danificado. Nada foi alterado. Recupere os contadores (o arquivo danificado é guardado à parte) ou restaure uma cópia de segurança; depois tente novamente. + + + O arquivo de contadores "{0}" foi gravado por uma versão mais recente do Rotula (versão de arquivo {1}). Atualize o Rotula para usá-lo. Nada foi alterado. + + + O arquivo de contadores "{0}" está em uso por outro programa ou não pode ser gravado. Feche o outro programa, verifique se você pode gravar nessa pasta e tente novamente. + + + O contador "{0}" lembra o seu número entre impressões, por isso a etiqueta precisa ser salva primeiro. Salve a etiqueta e tente novamente. + + + O contador "{0}" deveria lembrar o seu número, mas os contadores não podem ser armazenados agora, por isso ele só pode contar a partir do primeiro número. Para não repetir números, a impressão precisa que o arquivo de contadores esteja disponível. + + + + Informe um valor para "{0}". + + + A resposta para "{0}" é curta demais. Informe pelo menos {1} caracteres. + + + A resposta para "{0}" é longa demais. Informe no máximo {1} caracteres. + + + A resposta para "{0}" não está no formato esperado. Verifique-a e tente novamente. + + + A regra de formato definida para a pergunta "{0}" não é válida. Corrija a regra nas configurações da variável. + + + A resposta para "{0}" deve ser um número. + + + A resposta para "{0}" deve ser uma data. + + + Escolha uma das opções oferecidas para "{0}". + + + A pergunta "{0}" não tem opções para escolher. Adicione pelo menos uma opção nas configurações da variável. + + + Nenhuma resposta foi dada para "{0}". Responda a todas as perguntas antes de imprimir. + + + + A fórmula da variável "{0}" está vazia. Escreva uma fórmula ou remova a variável. + + + A fórmula da variável "{0}" é longa demais (máximo de {2} caracteres). Encurte-a. + + + A fórmula da variável "{0}" tem aninhamento profundo demais perto da posição {1} (máximo de {2} níveis). Simplifique-a. + + + A fórmula da variável "{0}" tem partes demais (máximo de {2}). Divida-a em duas variáveis. + + + A fórmula da variável "{0}" tem um caractere não permitido na posição {1}: {2}. Remova-o. + + + Era esperado outro elemento na posição {1} da fórmula de "{0}", mas foi encontrado {2}. Verifique a fórmula. + + + A fórmula da variável "{0}" termina cedo demais (posição {1}). Verifique se falta um valor ou um parêntese de fechamento. + + + Um texto entre aspas na posição {1} da fórmula de "{0}" não está fechado. Adicione as aspas de fechamento. + + + O número "{2}" na posição {1} da fórmula de "{0}" não é válido. Verifique como ele está escrito. + + + A função "{2}" na posição {1} da fórmula de "{0}" não existe. Verifique a grafia. + + + A função {2} na posição {1} da fórmula de "{0}" aceita de {3} a {4} valores. Adicione ou remova valores. + + + O valor número {3} da função {2} na fórmula de "{0}" não é permitido (por exemplo, um comprimento negativo). Altere-o. + + + A fórmula da variável "{0}" usa "{2}", que não é uma variável declarada e não tem dados. Crie a variável ou corrija o nome. + + + Variáveis calculadas dependem umas das outras em círculo ({2}). Altere uma das fórmulas para quebrar o círculo. + + + Um resultado da fórmula de "{0}" teria mais de {2} caracteres e foi cortado. Encurte a fórmula. + + + A função {2} na fórmula de "{0}" não conseguiu ler "{3}" como número. Verifique o valor. + + + A função {2} na fórmula de "{0}" não conseguiu ler "{3}" como data. Escreva as datas como aaaa-MM-dd ou aaaaMMdd. + + + O formato "{3}" usado pela função {2} na fórmula de "{0}" não pode ser usado. Verifique o formato. + + + + Os dados não cabem em um código QR com o nível de correção de erros "{2}": eles têm {0} caracteres e o máximo que cabe é {1}. Encurte os dados ou escolha um nível de correção de erros mais baixo. + + + O modo de entrada de dados QR não é um modo que o Rotula conheça. Escolha Automático ou um dos modos manuais. + + + O modo alfanumérico manual só aceita dígitos, letras maiúsculas de A a Z, espaços e os símbolos $ % * + - . / : . Altere os dados ou escolha outro modo de entrada. + + + Os dados são longos demais para o modo de bytes manual (máximo de {0} bytes). Encurte os dados. + + + O modo numérico manual só aceita os dígitos de 0 a 9. Altere os dados ou escolha outro modo de entrada. + + + O padrão de máscara QR deve ser um número de {0} a {1}, ou ficar automático. + + + baixa (7%) + + + média (15%) + + + quartil (25%) + + + alta (30%) + + + Este Code 128 ({0}) contém dados GS1 que o Rotula não consegue regravar exatamente como estão. Ele foi mantido como texto ZPL e continuará sendo impresso, mas não pode ser editado. + + + + Não há dados GS1. Informe pelo menos um identificador com o seu valor, por exemplo (01)09506000134352. + + + Os dados GS1 não estão escritos corretamente perto do caractere {0}. Cada elemento deve ter o formato (01)09506000134352. + + + O identificador ({0}) tem comprimento fixo, por isso não deve ser seguido de um caractere separador. Remova o separador. + + + Os dados terminam antes de o valor de ({0}) estar completo. Verifique se o valor tem todos os seus caracteres. + + + Os dados terminam com um separador sem nada depois dele. Remova-o. + + + O identificador que começa com "{0}" não é um identificador GS1 conhecido. Verifique o número. + + + O identificador ({0}) não existe no sistema GS1. Verifique o número entre parênteses. + + + O valor de ({0}) é longo demais (máximo de {1} caracteres). Encurte-o. + + + O valor de ({0}) é curto demais (mínimo de {1} caracteres). Complete-o. + + + O identificador ({0}) aparece mais de uma vez com valores diferentes. Mantenha apenas um. + + + O identificador ({0}) aparece mais de uma vez com o mesmo valor. Remova a repetição. + + + Os identificadores ({0}) e ({1}) não podem ser usados juntos. Remova um deles. + + + O identificador ({0}) normalmente é usado junto com outros identificadores: {1} (a vírgula separa alternativas e o sinal de mais significa todos eles). Adicione-os ou verifique os seus dados. + + + Um Data Matrix GS1 não pode ser também um GS1 Digital Link. Desative uma das duas opções. + + + A opção de dígito verificador não é necessária: o GS1-128 adiciona o seu próprio dígito verificador. Desative a opção. + + + Um valor contém um parêntese, que a impressora descarta no modo GS1-128. Remova o parêntese do valor. + + + Os dados GS1-128 têm {0} caracteres; o máximo é {1} (identificadores e separadores contam). Encurte os dados. + + + Um código ITF-14 precisa de exatamente 14 dígitos. + + + O dígito verificador do código ITF-14 está errado. Ele deveria ser {0}. + + + A parte linear "{0}" do código composto não é um GTIN válido (8, 12, 13 ou 14 dígitos com um dígito verificador correto). Corrija-a. + + + O identificador ({0}) aparece tanto na parte linear quanto na parte 2D do código composto. Mantenha-o em apenas uma delas. + + + + O texto não é um endereço da web válido. Um GS1 Digital Link deve começar com https:// (ou http://). + + + O link não tem chave GS1. Um Digital Link deve ter uma chave, como o GTIN (01), seguida do seu valor. + + + O link tem mais de uma chave GS1. Um Digital Link precisa de exatamente uma chave, como o GTIN. Remova as outras. + + + O identificador ({0}) não pode ser usado em um GS1 Digital Link. Remova-o. + + + O caminho do link está incompleto: ele deve ser formado por pares, um identificador seguido do seu valor. Verifique o link. + + + Os qualificadores ({1}) não são uma combinação permitida para a chave ({0}). Remova um deles ou verifique as regras GS1. + + + Os qualificadores ({1}) da chave ({0}) não estão em uma ordem permitida. Coloque-os na ordem que o GS1 define para essa chave. + + + + O valor de ({0}) deve conter somente hifens (-). + + + O valor de ({0}) deve conter pelo menos um caractere que não seja dígito. + + + O valor de ({0}) deve ser formado somente por zeros. + + + O valor de ({0}) deve ser 0, 1 ou 9. + + + O valor de ({0}) deve ser 0 ou 1. + + + O valor de ({0}) deve ser 0, 1, 2 ou 9. + + + O valor "{1}" de ({0}) não é um código de país de 3 dígitos válido (ISO 3166). Verifique-o. + + + O valor "{1}" de ({0}) não é um código de país. Use um código ISO 3166 de 3 dígitos ou 999. + + + O valor "{1}" de ({0}) não é um código de país de 2 letras válido (ISO 3166). Verifique-o. + + + O valor "{1}" de ({0}) não é um código numérico de moeda válido (ISO 4217). Verifique-o. + + + O valor "{1}" de ({0}) não é um código de tipo de embalagem válido. Verifique-o. + + + O valor de ({0}) deve ser uma única letra, dígito, hífen ou sublinhado. + + + O valor de ({0}) deve ter 10 dígitos e representar uma latitude entre -90 e 90 graus. + + + O valor de ({0}) deve ter 10 dígitos e representar uma longitude entre -180 e 180 graus. + + + O caractere "{1}" no valor de ({0}) não é um dígito. Este identificador aceita somente dígitos. + + + O caractere "{1}" no valor de ({0}) não pode ser usado em dados GS1. Use somente as letras, dígitos e símbolos que o GS1 permite. + + + O caractere "{1}" no valor de ({0}) não é permitido. Use somente letras maiúsculas, dígitos, # - e /. + + + O caractere "{1}" no valor de ({0}) não é permitido em texto Base64. Use letras, dígitos, - e _. + + + O preenchimento "=" no final do valor de ({0}) não está correto. + + + O dígito verificador de ({0}) está errado ou faltando (esperado: {1}). Corrija-o. + + + Os dois caracteres de verificação no final do valor de ({0}) estão errados. Corrija-os. + + + O valor de ({0}) deve conter um prefixo de empresa GS1 de pelo menos 4 dígitos na posição esperada. + + + A data de ({0}) deve ser escrita como {1}. + + + O mês "{1}" na data de ({0}) deve ser de 01 a 12. + + + O dia "{1}" na data de ({0}) não é válido para esse mês. + + + O horário de ({0}) deve ser escrito como {1}. + + + A hora "{1}" em ({0}) deve ser de 00 a 23. + + + Os minutos "{1}" em ({0}) devem ser de 00 a 59. + + + Os segundos "{1}" em ({0}) devem ser de 00 a 59. + + + O valor de ({0}) não pode ser zero. + + + O valor de ({0}) não pode começar com zero. + + + O valor de ({0}) deve ser formado por dois números do mesmo comprimento: a peça e o total (por exemplo 0204). + + + Nem o número da peça nem o total de ({0}) podem ser zero. + + + O número da peça de ({0}) é maior que o total. Verifique os dois números. + + + O valor de ({0}) deve ser formado por dois números separados por uma barra (por exemplo 2/5). + + + O primeiro número de ({0}) é maior que o segundo. Verifique os dois números. + + + O valor de ({0}) tem um código % incompleto ou inválido. + + + O tipo de mídia "{1}" de ({0}) não é válido. Use de 01 a 10 ou de 80 a 99. + + + O valor de ({0}) não é um IBAN válido. Verifique-o. + + + + Somente dígitos são permitidos. + + + O dígito verificador está errado. Ele deveria ser {0}. + + + Um GTIN tem 8, 12, 13 ou 14 dígitos. + + + O dígito indicador deve ser de 0 a 9. + + + Um prefixo de empresa GS1 tem de 4 a 12 dígitos. + + + A referência não pode ficar vazia e deve ser formada por dígitos. + + + O prefixo de empresa é longo demais: ele não deixa espaço para a referência. + + + A referência é longa demais. São permitidos no máximo {0} caracteres. + + + O dígito de extensão deve ser de 0 a 9. + + + O caractere "{0}" não é permitido em dados GS1. + + + O identificador ({0}) não é uma medida com posição de ponto decimal. + + + A medida não pode ser negativa e pode ter no máximo {0} dígitos. + + + + O código QR está {0}% cheio (versão {1}). Ele pode ser difícil de ler. Encurte o conteúdo ou escolha um nível de correção de erros mais baixo. + + + O conteúdo não cabe em um código QR com o nível de correção de erros "{2}": ele precisa de {0} e o limite é {1}. + + + Encurte o conteúdo para que ele caiba. + + + Escolher o nível de correção de erros "{0}" faria o conteúdo caber. + + + O endereço de e-mail não é válido. Verifique-o (por exemplo nome@exemplo.com). + + + Informe pelo menos um endereço de e-mail. + + + O evento termina antes de começar. Verifique as datas de início e de término. + + + A data não tem fuso horário, por isso é lida como horário UTC e pode aparecer deslocada em um calendário. Adicione o fuso, por exemplo -03:00. + + + Informe um título para o evento. + + + A altitude não é um número válido. + + + A latitude deve ser um número entre -90 e 90. + + + A longitude deve ser um número entre -180 e 180. + + + Informe um nome ou um sobrenome para o contato. + + + Informe um nome, sobrenome ou organização para o contato. + + + O número de telefone não é válido. Use dígitos, opcionalmente com + e o código do país (por exemplo +5511912345678). + + + A mensagem tem mais de {0} caracteres, por isso pode ser enviada como várias mensagens. + + + Informe o endereço da web. + + + O endereço da web não é válido. Escreva-o por completo, sem espaços (por exemplo https://exemplo.com). + + + O endereço começa com "{0}", que muitas câmeras de celular não abrem. Use https:// se puder. + + + Esta rede não tem segurança, por isso a senha é ignorada. Apague a senha ou escolha um tipo de segurança. + + + O comprimento da senha não é válido para este tipo de segurança. Ela deve ter {0} caracteres. + + + Informe a senha da rede. + + + Informe o nome da rede Wi-Fi. + + + O nome da rede é longo demais (máximo de {0} bytes; caracteres acentuados usam mais de um). + + + Este campo é obrigatório. + + + Este texto é longo demais (máximo de {1} caracteres). Encurte-o. + + + Escolha um dos valores disponíveis. + + + Informe um número válido (use ponto para as casas decimais). + + + Informe uma data válida, ou uma data e hora com o fuso (por exemplo 2026-03-14T09:00:00-03:00). + + + + Os dados são GS1 (GS1 DataMatrix) + + + Os dados são um GS1 Digital Link + + + Padrão de máscara + + + Modo de entrada de dados + + + Conteúdo estruturado + + + Baixa (7%) + + + Média (15%) + + + Quartil (25%) + + + Alta (30%) + + + Modelo 1 (original) + + + Modelo 2 (recomendado) + + + Automático (a impressora escolhe) + + + Padrão 0 + + + Padrão 1 + + + Padrão 2 + + + Padrão 3 + + + Padrão 4 + + + Padrão 5 + + + Padrão 6 + + + Padrão 7 + + + Automático + + + Manual: somente dígitos + + + Manual: maiúsculas e dígitos + + + Manual: qualquer texto (bytes) + + + Manual: kanji + + + + Endereço da web + + + Texto + + + Wi-Fi + + + Contato (vCard) + + + Contato (MeCard) + + + GS1 Digital Link + + + E-mail + + + Ligação telefônica + + + Mensagem SMS + + + Localização + + + Evento de calendário + + + + Endereço da web + + + Remover parâmetros de rastreamento + + + Texto + + + Nome da rede + + + Senha + + + Segurança + + + Rede oculta + + + Nome + + + Sobrenome + + + Organização + + + Cargo + + + Telefone + + + Celular + + + E-mail + + + Rua + + + Cidade + + + Estado ou região + + + CEP + + + País + + + Observação + + + Prefixo do endereço da web + + + GTIN + + + Lote + + + Número de série + + + Data de validade (AAMMDD) + + + Consumir preferencialmente antes de (AAMMDD) + + + Data de fabricação (AAMMDD) + + + Para + + + Assunto + + + Corpo da mensagem + + + Número de telefone + + + Mensagem + + + Latitude + + + Longitude + + + Nome do local + + + Início + + + Término + + + Local + + + Descrição + + + Dia inteiro + + + + WPA / WPA2 + + + WPA3 + + + WEP (antigo) + + + Nenhuma (rede aberta) + + + + O servidor fechou a conexão durante o login. O nome de usuário ou a senha provavelmente estão errados. Se você usa uma conexão criptografada, o certificado do servidor também pode ser o problema. + + + O banco de dados não consegue saber o tipo deste parâmetro. + + + Indique o tipo do parâmetro na consulta, por exemplo @x::text. + + + O parâmetro tem um tipo diferente do da coluna com a qual é comparado. + + + Passe um valor do tipo da coluna ou converta o parâmetro, por exemplo @x::numeric ou @x::date. + + + Aceita um certificado que não pode ser verificado. No MariaDB com uma conta com senha, o certificado é verificado pela impressão digital, por isso um servidor com certificado autoassinado normalmente se conecta sem esta opção. + + + Nome da tabela + + + + Arquivo DuckDB + + + Firebird + + + Oracle Database + + + ClickHouse + + + Arquivo de banco de dados DuckDB + + + O arquivo .duckdb neste computador. + + + Limite de memória (MB) + + + Limite opcional de memória que uma consulta pode usar. + + + Caminho ou alias do banco de dados no servidor + + + O caminho como o servidor o vê (C:\dados\vendas.fdb) ou um alias do databases.conf do servidor. + + + Criptografa toda a conexão. Com nome de usuário e senha, é obrigatório: se o servidor não puder criptografar, a conexão falha. + + + Função SQL (opcional) + + + Use uma função que só possa ler (SELECT). + + + Nome do serviço + + + O serviço oferecido pelo listener (por exemplo ORCLPDB1). + + + Usa TLS. Desative somente para um listener sem TLS. + + + Não verificar o nome do servidor no certificado + + + Mantém o TLS ativo, mas pula a verificação do nome do host. Reduz a segurança. + + + O nome acima é um + + + Nome do serviço + + + SID (estilo antigo) + + + Nome do tnsnames.ora + + + Pasta com o tnsnames.ora + + + Somente para nomes do tnsnames.ora. + + + Pasta da wallet (certificados TLS) + + + Uma wallet do Oracle com os certificados confiáveis, quando o repositório de certificados do Windows não é suficiente. + + + Deixar o servidor interromper consultas longas (requer uma conta que possa alterar configurações) + + + Desativado por padrão: contas com perfil somente leitura são recusadas quando uma configuração muda. + + + Assistente de conteúdo... + + + Assistente GS1... + + + Criado com o assistente de conteúdo: {0}. Editar o texto manualmente remove esta definição. + + + Não é possível ler a definição de conteúdo salva. Abra o assistente de conteúdo para criá-la novamente. + + + Você editou o texto manualmente, por isso a definição do assistente de conteúdo foi removida. Abra o assistente de conteúdo para recomeçar a partir de um formulário. + + + assistente de conteúdo QR + + + assistente GS1 + + + Ainda não há nada para medir. O medidor aparece quando o código tem conteúdo. + + + Cabe: {0}% da capacidade no nível {1}, versão {2} + + + Cabe: {0}% da capacidade no nível {1} (modelo 1) + + + Quase cheio: {0}% da capacidade no nível {1}, versão {2}. Reduza o nível ou encurte o texto para facilitar a leitura. + + + Quase cheio: {0}% da capacidade no nível {1} (modelo 1). Reduza o nível ou encurte o texto para facilitar a leitura. + + + Não cabe no nível {0}: precisa de {1} e o limite é {2}. Reduza o nível ou encurte o texto. + + + No nível {0}, caberia. + + + Nem o nível mais baixo é suficiente: encurte o texto. + + + Nível sugerido para este conteúdo: {0}. + + + Medido no mais longo dos {0} registros carregados. + + + Tamanho impresso: {0} mm ({1} pol) de lado a {2} dpi (versão {3}). + + + Cada módulo tem {0} mm de largura a {1} dpi. + + + Pequeno demais para ser lido: cada módulo tem {0} mm de largura. Use pelo menos {1} pontos (cerca de {2} mm) por módulo a {3} dpi. + + + Assistente de conteúdo QR + + + Aplicar ao código + + + O que o código deve fazer ao ser lido? + + + Visualização + + + Texto no código + + + Capacidade + + + A corrigir + + + Usar um valor fixo + + + {0} caracteres + + + A senha é impressa no código como texto simples: qualquer pessoa que o ler poderá vê-la. Ela é mostrada aqui de propósito, para que você possa conferi-la. + + + Exemplo: {0} + + + Você pode escrever várias linhas. Exemplo: {0} + + + Um número de telefone com o código do país. Exemplo: {0} + + + Um endereço de e-mail. Exemplo: {0} + + + Um endereço da web completo que comece com https://. Exemplo: {0} + + + Um número; use ponto para as casas decimais. Exemplo: {0} + + + Somente dígitos. Exemplo: {0} + + + Escolha a data, escreva o horário em horas e minutos (09:30) ou vincule um campo. Exemplo: {0} + + + Preencha o formulário e o código aparecerá aqui. + + + O código aparece quando os problemas à direita forem corrigidos. + + + Os campos vinculados mostram o valor do registro exibido na área de design, ou o exemplo deles. + + + Preencha os campos marcados com * para continuar. + + + Assistente GS1-128 + + + Assistente GS1 DataMatrix + + + Aplicar ao código de barras + + + Valor + + + Valor + + + Identificador desconhecido + + + Formato {0}, até {1} caracteres. + + + Expedição (SSCC) + + + Um palete ou volume: o seu código seriado de unidade logística. + + + Produto (GTIN, lote, validade) + + + O código do produto com o lote e a data de validade. + + + Serialização (GTIN, série) + + + O código do produto com um número de série para cada unidade. + + + Identificadores comuns. Digite para pesquisar entre todos. + + + {0} identificadores encontrados + + + {0} de {1} caracteres + + + Cerca de {0} de {1} caracteres (os campos vinculados são estimados) + + + {0} caracteres + + + A mensagem precisa de {0} caracteres e o GS1-128 comporta no máximo {1}. Remova um identificador ou use valores mais curtos. + + + Este valor vem de um campo. Ele é verificado com os dados na hora de imprimir. + + + Escolha um uso predefinido ou adicione identificadores da lista para começar. + + + Preencha o valor de cada identificador para continuar. + + + Mover para cima + + + Mover para baixo + + + Remover este identificador + + + Começar a partir de um uso + + + Adicionar um identificador + + + Pesquisar identificadores + + + Digite o número de um identificador (10) ou palavras do nome dele em inglês (expiry). + + + Enter ou clique duplo adiciona o identificador. Seta para baixo entra na lista. + + + Identificadores correspondentes + + + A mensagem + + + Ainda não há identificadores. Escolha um uso à esquerda ou pesquise um identificador e adicione-o. + + + Visualização + + + O símbolo aparece quando todos os valores forem válidos. + + + Forma legível + + + Caracteres codificados com FNC1 + + + [FNC1] marca o caractere especial que inicia o símbolo e separa os valores de comprimento variável. + + + Comprimento + + + Sim + + + Hora do dia em horas e minutos, por exemplo 09:30 + + + Antes de imprimir + + + Esta etiqueta faz algumas perguntas. Suas respostas são usadas em todas as etiquetas desta impressão. + + + Continuar + + + Digite a data como aaaa-MM-dd, por exemplo 2026-03-07. + + + Digite um número, por exemplo 12 ou 3.5. + + + Mostrar ou ocultar a lista de objetos + + + O texto não cabe na caixa: parte dele fica cortada. Aumente a caixa, permita mais linhas, use um tamanho menor ou encurte o texto. + + + O texto pode não caber na caixa. A impressora não informa a largura das letras, então isto é uma estimativa: imprima uma etiqueta de teste para conferir, ou aumente a caixa ou permita mais linhas. + + + O texto usa a fonte do Windows "{0}", que precisa ser desenhada como imagem, mas este programa não consegue desenhá-la aqui, então o texto não foi impresso. Escolha uma fonte da impressora. + + + Um texto em fonte do Windows é impresso como imagem, então a impressora não consegue numerá-lo, tirá-lo de um campo armazenado nem incorporar caracteres nele. Remova essas opções do campo ou escolha uma fonte da impressora. + + + A data "{0}" usa o relógio da impressora, que não pode ser usado dentro de uma imagem de texto. Foi usado o relógio do computador. + + + A direção e o espaçamento entre caracteres de um texto são ignorados quando ele usa uma fonte do Windows. + + + O texto é grande demais para ser impresso como imagem. Use um tamanho menor ou uma caixa mais estreita. + + + Este computador não tem uma fonte chamada "{0}", então "{1}" é usada no lugar. Instale a fonte ou escolha outra; caso contrário, o texto impresso ficará diferente. + + + A fonte "{0}" não pode ser usada: ela precisa de um nome e de um tamanho de 1 a 1000 pontos. + + + Negrito, itálico e sublinhado não existem nas fontes próprias da impressora e não são impressos. Escolha uma fonte do Windows para usá-los. + + + "{0}" não é um nome de arquivo de fonte aceito pela impressora. Use um nome como E:ARIAL.TTF. + + + Esta etiqueta tem um contador, então cada cópia precisa do próprio número, e isso é limitado a {0} cópias por impressão. Imprima menos cópias por vez ou use a janela Mesclar dados. + + + Formatação + + + Quebra automática de texto + + + Tamanho em pontos da impressora (avançado) + + + Formatação do texto + + + Fonte + + + Tamanho (pt) + + + Tamanho em pontos tipográficos (1 ponto é 1/72 de polegada), como em um editor de texto. Escolha um tamanho ou digite o seu. + + + Escolha uma fonte da impressora (impressa como texto real) ou uma fonte do Windows (impressa como imagem). Digite para pesquisar. + + + Fontes da impressora + + + Fontes do Windows + + + Fonte da impressora {0} (escalável) + + + Fonte da impressora {0} (forma fixa) + + + Fonte da impressora {0} + + + Fonte armazenada na impressora ({0}) + + + {0} (não instalada) + + + Negrito + + + Itálico + + + Sublinhado + + + Negrito (Ctrl+B) + + + Itálico (Ctrl+I) + + + Sublinhado (Ctrl+U) + + + Negrito, itálico e sublinhado não existem nas fontes próprias da impressora. Escolha uma fonte do Windows na lista Fonte para usá-los. + + + O alinhamento funciona dentro de uma caixa de texto. Ative a quebra automática de texto para usá-lo. + + + Alinhar à esquerda + + + Centralizar + + + Alinhar à direita + + + Justificar + + + Este texto é impresso como imagem. Ele fica nítido na resolução da impressora, mas a impressora não pode editá-lo nem tirá-lo dos próprios campos. As fontes da impressora são impressas como texto real. + + + Entendi + + + Fonte do Windows: impressa como imagem + + + Aumentar a caixa + + + Diminuir o texto + + + Clique em um dos botões abaixo para corrigir em um passo. + + + Este computador não tem uma fonte chamada "{0}", então "{1}" é usada no lugar. Instale a fonte ou escolha outra; caso contrário, o texto impresso ficará diferente. + + + Negrito, itálico e sublinhado precisam de uma fonte do Windows. Escolha uma na lista Fonte. + + + Largura da caixa: {0} pontos ({1} mm) + + + Agora o texto cabe na caixa. + + + O texto ainda não cabe; tente uma caixa maior ou um tamanho menor. + + + troca de fonte + + + estilo do texto + + + tamanho da fonte + + + alinhamento + + + quebra automática de texto + + + ajuste do texto à caixa + + + Não é possível desenhar a visualização agora. Reduza o zoom ou diminua a etiqueta e tente de novo; sua etiqueta não é afetada. + + + Ajuste do texto + + + Quando o texto for longo demais + + + Deixar como foi criado + + + Reduzir para caber + + + Aumentar a altura + + + A caixa fica como foi criada. O texto que não couber é cortado na impressão. + + + Cada etiqueta usa o maior tamanho, entre o menor e o maior abaixo, em que todo o texto cabe na caixa. Com dados, cada registro tem o próprio tamanho. + + + O tamanho continua o mesmo e a caixa ganha quantas linhas o texto precisar, até o limite abaixo. O que estiver embaixo dela não se move. + + + Menor tamanho (pt) + + + Maior tamanho (pt) + + + Máximo de linhas + + + Não avisar quando o texto não couber + + + Use quando você quiser que o texto seja cortado. + + + O menor tamanho não pode ser maior que o maior tamanho. + + + mudança do ajuste do texto + + + mudança do limite do ajuste do texto + + + Impressão + + + Não imprimir este objeto + + + O objeto continua na etiqueta no editor, por exemplo como nota ou guia, mas nunca é impresso nem exportado. + + + Imprimir somente quando + + + Omite este objeto nas etiquetas em que a regra abaixo não for verdadeira, por exemplo para imprimir um aviso somente em alguns registros. + + + Campo a verificar + + + A coluna de dados ou variável cujo valor decide se este objeto é impresso. + + + Escolher campo + + + Comparação + + + Valor a comparar + + + Maiúsculas e espaços em volta do texto não importam: Sim, sim e " SIM " são iguais. + + + Regra de impressão + + + Bloquear posição + + + Um objeto bloqueado não pode ser movido, redimensionado nem excluído com o mouse ou o teclado até você desbloqueá-lo. Você ainda pode selecioná-lo e alterar as propriedades dele aqui. Bloquear não muda o que é impresso. + + + é igual a + + + é diferente de + + + está vazio + + + não está vazio + + + contém + + + Imprime somente quando {0} é igual a "{1}". + + + Imprime somente quando {0} é diferente de "{1}". + + + Imprime somente quando {0} está vazio. + + + Imprime somente quando {0} não está vazio. + + + Imprime somente quando {0} contém "{1}". + + + Escolha o campo a verificar. Até lá, este objeto sempre é impresso. + + + Este objeto nunca é impresso, seja qual for a regra. + + + Escolha o campo a verificar. + + + Não há nenhum campo ou variável chamado "{0}" nos dados carregados nem na etiqueta. Ele é tratado como vazio, então confira o nome. + + + A regra de impressão deste objeto verifica "{0}", mas nenhuma coluna de dados ou variável tem esse nome. Ele é tratado como vazio, então confira o nome. + + + Os objetos bloqueados não foram alterados. Desmarque "Bloquear posição" nas propriedades para alterá-los. + + + Não imprime + + + Marcado como "Não imprimir": este objeto nunca é impresso nem exportado. + + + Oculto + + + A regra de impressão deste objeto não é verdadeira para o registro mostrado, por isso ele fica fora desta etiqueta. + + + Bloqueado + + + Bloqueado: este objeto não pode ser movido, redimensionado nem excluído até você desbloqueá-lo. + + + Nada foi enviado: a impressora "{0}" informa que a cabeça de impressão está aberta. Feche a cabeça até ouvir o clique e imprima novamente. + + + Nada foi enviado: a impressora "{0}" informa que está sem etiquetas. Coloque etiquetas, feche a cabeça, pressione Feed uma vez e imprima novamente. + + + Nada foi enviado: a impressora "{0}" informa que o ribbon acabou. Coloque um ribbon novo, feche a cabeça e imprima novamente. + + + Nada foi enviado: a impressora "{0}" informa que a cabeça de impressão está quente demais. Aguarde alguns minutos até ela esfriar e imprima novamente. + + + Nada foi enviado: a impressora "{0}" está no modo de diagnóstico de comunicação e imprimiria os dados como texto em vez de etiquetas. Desative esse modo na impressora (ou reinicie-a) e imprima novamente. + + + Nada foi enviado: a impressora "{0}" informa que a memória para trabalhos recebidos está cheia. Aguarde até que ela imprima o que já recebeu e imprima novamente. + + + Nada foi enviado: a impressora "{0}" está em pausa. Pressione Pause na impressora para continuar e imprima novamente. + + + A impressora também informa: {0}. + + + Não foi possível ler o status da impressora antes de imprimir ({0}), por isso as etiquetas foram enviadas sem essa verificação. + + + A impressora informa: {0}. As etiquetas foram enviadas; confira a qualidade de impressão. + + + Impresso e verificado: a impressora informa que concluiu o trabalho sem problemas. + + + As etiquetas foram enviadas, mas depois a impressora informou um problema: {0}. Algumas etiquetas podem não ter sido impressas. Resolva o problema na impressora e confira as etiquetas antes de imprimir novamente. + + + As etiquetas foram enviadas, mas o Rotula não conseguiu confirmar que foram impressas ({0}). Verifique a impressora. + + + esta impressora ou o driver dela no Windows não responde a consultas de status, o que é comum com drivers USB + + + o Windows informa que a impressora está offline + + + não foi possível contatar a impressora + + + a impressora não respondeu a tempo + + + a resposta da impressora não era uma resposta de status ZPL + + + ela ainda estava imprimindo quando o Rotula parou de esperar + + + a verificação foi interrompida + + + o motivo é desconhecido + + + cabeça de impressão aberta + + + sem etiquetas + + + sem ribbon + + + cabeça de impressão quente demais + + + modo de diagnóstico de comunicação ativado + + + memória para trabalhos recebidos cheia + + + em pausa + + + cabeça de impressão fria demais (as etiquetas podem sair mais claras) + + + configurações perdidas (a impressora está usando os valores de fábrica) + + + um trabalho ainda está chegando + + + uma etiqueta impressa está esperando ser retirada + + + desconhecido + + + rebobinar + + + destacar (peel-off) + + + destacar manualmente (tear-off) + + + cortador + + + aplicador + + + corte retardado + + + destacar sem liner + + + rebobinar sem liner + + + corte parcial + + + RFID + + + quiosque + + + contínuo + + + Verificar status da impressora + + + Pergunta à impressora o status dela (papel, cabeça, ribbon, pausa) com um pequeno comando ZPL. Nada é impresso em uma impressora compatível com ZPL. + + + Consultando o status da impressora... + + + A impressora está pronta. + + + A impressora não pode imprimir agora: {0}. + + + A impressora informa: {0}. + + + Não foi possível ler o status da impressora: {0}. Você ainda pode imprimir. + + + Só parte do status foi recebida, por isso alguns problemas podem não aparecer na lista. + + + Etiquetas ainda a imprimir: {0}. Trabalhos aguardando na impressora: {1}. + + + Modo de impressão: {0}. {1}. Comprimento da etiqueta: {2} pontos. + + + Transferência térmica (com ribbon) + + + Térmica direta (sem ribbon) + + + Verificar a impressora antes e depois de imprimir + + + Antes de cada trabalho, o Rotula consulta o status da impressora e não envia nada enquanto ela estiver sem etiquetas, aberta, em pausa ou sem ribbon. Depois do trabalho, espera até a impressora informar que terminou. Para impressoras compatíveis com ZPL; muitos drivers USB não conseguem responder e, nesse caso, a impressão continua sem a verificação. + + + Abrir posto de impressão... + + + Criar PIN do posto de impressão... + + + Posto de impressão + + + Escolha a pasta do posto de impressão (com o station.json dele ou com as etiquetas a oferecer) + + + Não é possível abrir o posto de impressão. + + + Sair + + + Todas as etiquetas + + + Pesquise ou leia uma etiqueta + + + Digite um nome ou leia o código da etiqueta e pressione Enter. Ou toque em uma etiqueta abaixo. + + + Nenhuma etiqueta corresponde à pesquisa. + + + Nenhuma etiqueta tem o código "{0}". Leia-o novamente ou escolha uma etiqueta da lista. + + + Algumas etiquetas não estão disponíveis: + + + A etiqueta "{0}" não pode ser usada: {1} + + + Impressora: {0} + + + Nenhuma impressora está configurada para esta etiqueta. Peça a quem configurou o posto para adicionar uma no station.json. + + + Quantidade + + + De 1 a {0} etiquetas + + + Informe uma quantidade de 1 a {0} e pressione Imprimir novamente. + + + Imprimir + + + Imprimir todos os registros + + + {0} registros dos dados da etiqueta estão prontos para imprimir. + + + Não foi possível ler os dados da etiqueta: {0} + + + os dados não têm registros. + + + Esta etiqueta não faz perguntas. Defina a quantidade e pressione Imprimir. + + + Algumas respostas não são aceitas. Corrija os campos marcados em vermelho e imprima novamente. + + + Visualização + + + Verificando a impressora... + + + Impressora pronta + + + Status da impressora desconhecido + + + A impressora precisa de atenção + + + Imprimindo... + + + O Rotula verifica a impressora antes de enviar e espera até ela informar que terminou. + + + Não impresso: a impressora precisa de atenção + + + Enviado para a impressora + + + Impresso e verificado + + + Enviado, mas não confirmado + + + Enviado, mas a impressora informa um problema + + + Não impresso + + + Digite o PIN para sair do posto de impressão + + + PIN + + + Sair do posto + + + Esse PIN não está certo. Tente novamente. + + + Muitos PINs errados. Aguarde {0} segundos e tente novamente. + + + Criar um PIN para o posto de impressão + + + O PIN é necessário para sair do posto de impressão. O Rotula guarda apenas um código protegido criado a partir dele, nunca o próprio PIN. Copie a linha para o station.json do posto. + + + Novo PIN + + + Digite o PIN novamente + + + Criar + + + Linha para o station.json + + + Copiar + + + Copiado. Cole-o no station.json, junto com as outras configurações. + + + Um PIN precisa ter de {0} a {1} caracteres. + + + Os dois PINs não são iguais. Digite-os novamente. + + + "{0}" não foi encontrado. Confira o caminho informado ao posto de impressão. + + + Não foi possível ler "{0}". Verifique se o arquivo não está bloqueado e se este usuário tem permissão para lê-lo. + + + "{0}" tem um erro perto da linha {1} ({2}): uma configuração está escrita errado, tem um valor do tipo errado, ou falta uma vírgula ou aspas. + + + As configurações do posto foram gravadas para uma versão mais recente do Rotula (formato {0}). Atualize o Rotula neste computador. + + + Não há nenhuma etiqueta para oferecer. Coloque arquivos .rotula na pasta do posto ou liste-os em "labels" no station.json. + + + A etiqueta "{0}" listada no station.json não foi encontrada e foi deixada de fora. + + + "{0}" não é uma etiqueta do Rotula (arquivo .rotula). Corrija no station.json. + + + A entrada de etiqueta {0} do station.json não tem "file". Adicione o nome do arquivo da etiqueta. + + + Duas etiquetas usam o código "{0}". Dê a cada etiqueta o próprio código, para que uma leitura abra a etiqueta certa. + + + A impressora "{0}" não está definida em "printers" no station.json. + + + A impressora "{0}" precisa de "windowsPrinter" (uma impressora instalada) ou de "host" (um endereço de rede), não dos dois. + + + A porta da impressora "{0}" deve ser de 1 a 65535 (normalmente 9100). + + + A resolução da impressora "{0}" deve ser 203, 300 ou 600. + + + A impressora "{0}" tem uma intensidade fora de 0 a 30 ou uma velocidade fora de 1 a 14. + + + As configurações de cópias do posto não são válidas: "maxCopies" deve ser de 1 a 99999999 e "defaultCopies" de 1 a "maxCopies". + + + As configurações de cópias da etiqueta "{0}" não são válidas: "maxCopies" deve ser de 1 a 99999999 e "defaultCopies" de 1 a "maxCopies". + + + "exitPinHash" não é um código de PIN criado pelo Rotula. Crie-o em Arquivo > Criar PIN do posto de impressão (um PIN nunca é guardado como texto simples). + + + + Gerenciado pela sua organização + + + Verificar atualizações uma vez por dia + + + Quando ativado, o Rotula pergunta uma vez por dia à página do projeto no GitHub se existe uma versão mais recente. Nada sobre você ou este computador é enviado, e nada é baixado ou instalado. + + + O Rotula {0} está disponível. Você pode baixá-lo na página da versão. + + + Atualização disponível + + + Ver a versão + + + Fechar este aviso + + + Não foi possível abrir o navegador. A página da versão é {0} + + + Mostrar a etiqueta como é impressa ou como foi criada + + + Como é impressa + + + Mostra a etiqueta do jeito que ela sai da impressora, girada ou espelhada de acordo com as configurações de impressão da etiqueta. + + + Como foi criada + + + Mostra a etiqueta na posição normal, do jeito que você a edita na área de design. + + + Exibida como é impressa: a orientação de impressão desta etiqueta está invertida, por isso a impressora a imprime de cabeça para baixo. + + + Exibida como é impressa: esta etiqueta está configurada para imprimir espelhada, por isso a esquerda e a direita ficam trocadas. + + + Exibida como é impressa: esta etiqueta está configurada para imprimir de cabeça para baixo e espelhada. + + + Exibida como foi criada, do jeito que você a edita. Escolha "Como é impressa" para ver como ela sai da impressora. + + + Imprime de cabeça para baixo + + + Imprime espelhada + + + Imprime de cabeça para baixo e espelhada + + + Orientação de impressão + + + A área de design mostra a etiqueta na posição normal para que você possa editá-la. Abra a visualização para vê-la como é impressa. Altere isso nas propriedades da etiqueta. + + + Folhas de etiquetas (PDF)... + + + Folhas de etiquetas (PDF)... + + + Coloca as etiquetas em folhas para uma impressora de escritório (laser ou jato de tinta) e as salva como PDF. + + + Folhas de etiquetas + + + Imprima etiquetas em folhas com uma impressora de escritório (laser ou jato de tinta). O Rotula salva as folhas como PDF no tamanho real; imprima-o em tamanho real (100%). + + + Há dados carregados: os registros marcados vão para as folhas, com suas cópias e números de série, como na impressão. + + + Folha + + + Folhas de fornecedores diferentes com o mesmo tamanho de etiqueta podem variar um milímetro. Imprima uma página em papel comum, coloque-a contra uma folha de etiquetas e ajuste as margens se for preciso. + + + Layout personalizado + + + {0}, {1} por folha ({2} x {3}), {4} + + + {0} x {1} pol + + + {0} x {1} mm + + + Papel + + + Medidas em polegadas + + + Todas as medidas em milímetros + + + Todas as medidas em polegadas + + + Largura do papel + + + Altura do papel + + + Largura da etiqueta + + + Altura da etiqueta + + + Etiquetas na horizontal + + + Etiquetas na vertical + + + Margem superior + + + Margem esquerda + + + Na horizontal: de borda a borda + + + Na vertical: de borda a borda + + + A distância da borda esquerda (ou superior) de uma etiqueta até a mesma borda da seguinte: o tamanho da etiqueta mais o espaço entre etiquetas. + + + Começar na posição + + + Em uma folha já usada em parte, a primeira posição livre. As posições são contadas pelas linhas a partir do canto superior esquerdo. Você também pode clicar em uma posição na visualização. + + + Ordem de preenchimento + + + Guias + + + Imprima o PDF em tamanho real (100%), sem "Ajustar à página", ou as etiquetas não vão coincidir com a folha. + + + Visualização da folha + + + Clique em uma posição para começar nela. As posições cinza já estão usadas; os números mostram onde vão as primeiras etiquetas. + + + Posição {0}: já usada + + + Posição {0}: etiqueta {1} + + + Posição {0}: vazia + + + Salvar como PDF... + + + Etiquetas: {0}. Folhas: {1} ({2} por folha). + + + {0} por folha. As cópias vêm de uma coluna dos dados, por isso o número de folhas só é conhecido quando o PDF é criado. + + + {0}: digite um número. + + + Cópias: digite um número inteiro de 1 a {0}. + + + Criando as folhas... + + + {2} salvo (etiquetas: {0}, folhas: {1}). Imprima-o em tamanho real (100%). + + + São mais de {0} etiquetas, demais para um PDF. Marque menos registros ou diminua as cópias, e salve o restante em outro arquivo. + + + Carta (8,5 x 11 pol) + + + A4 (210 x 297 mm) + + + Ofício (8,5 x 14 pol) + + + Tamanho personalizado + + + Pelas linhas, depois para baixo + + + Pelas colunas, depois para a direita + + + Nenhuma + + + Marcas de corte nas margens + + + Contorno de cada etiqueta (teste em papel comum) + + + O papel deve ter entre {0} e {1} mm de cada lado. + + + As etiquetas devem ter pelo menos {0} mm de largura e de altura. + + + Uma folha pode ter de 1 a {0} etiquetas na horizontal e de 1 a {1} na vertical. + + + As margens não podem ser negativas. + + + As etiquetas se sobrepõem na horizontal: de uma borda de etiqueta à seguinte há {0} mm, mas cada etiqueta tem {1} mm de largura. A distância deve ser pelo menos a largura da etiqueta. + + + As etiquetas se sobrepõem na vertical: de uma borda de etiqueta à seguinte há {0} mm, mas cada etiqueta tem {1} mm de altura. A distância deve ser pelo menos a altura da etiqueta. + + + As etiquetas não cabem na largura do papel: a última coluna termina em {0} mm e o papel tem {1} mm de largura. Reduza a margem esquerda, a distância ou o número de etiquetas na horizontal. + + + As etiquetas não cabem na altura do papel: a última linha termina em {0} mm e o papel tem {1} mm de altura. Reduza a margem superior, a distância ou o número de etiquetas na vertical. + + + Algumas etiquetas ficam a menos de {0} mm da borda do papel. A maioria das impressoras de escritório não imprime ali, por isso mantenha textos e códigos de barras longe dessas bordas da etiqueta. + + + A posição inicial deve ser um número de 1 a {0}. + + + O design da etiqueta tem {0} x {1} mm, maior que as etiquetas de {2} x {3} mm desta folha. Ele é impresso no próprio tamanho, centralizado, e cortado nas bordas da etiqueta. Escolha uma folha com etiquetas maiores ou redimensione o design. + + + O design da etiqueta tem {0} x {1} mm, menor que as etiquetas de {2} x {3} mm desta folha. Ele é impresso no próprio tamanho, centralizado em cada etiqueta. + + + {0} precisa de NOME=VALOR, mas recebeu "{1}". + + + "{1}" não é um valor válido para {0}. + + + rotula {0}: imprime etiquetas do Rotula sem o designer, para scripts, agendadores de tarefas e sistemas de armazém. + +Comandos: + rotula print ETIQUETA.rotula --printer NOME | --host ENDEREÇO [opções] + Imprime uma etiqueta. Opções: + --port N porta TCP de --host (padrão 9100) + --copies N cópias da etiqueta, ou de cada registro + --data ARQUIVO registros de um arquivo .csv, .txt, .xlsx ou .json (uma etiqueta por registro) + --sheet NOME a planilha do Excel a ler + --set NOME=VALOR o valor de um campo (repita para mais campos) + --answer NOME=VALOR a resposta a uma pergunta que a etiqueta faz na impressão + --dpi 203|300|600 resolução da cabeça de impressão (padrão: a informada pelo driver do Windows) + --darkness 0-30 escurecimento enviado antes das etiquetas + --speed 1-14 velocidade enviada antes das etiquetas + --no-status não pergunta o status à impressora antes de cada trabalho + --no-verify não espera a impressora informar que terminou + --dry-run mostra o ZPL sem imprimir (--output ARQUIVO grava em um arquivo) + --reference TEXTO sua própria referência, repetida no resultado + --counter-file ARQUIVO onde os números de série são lembrados (padrão: o mesmo arquivo do aplicativo) + rotula status --printer NOME | --host ENDEREÇO [--port N] + Pergunta à impressora se ela pode imprimir agora. + rotula printers + Lista as impressoras do Windows. + rotula serve [--settings ARQUIVO] [--urls URL] + Executa a API REST. "rotula serve --init [--label-folder PASTA]" cria um arquivo de configuração inicial com uma chave nova. + rotula new-key + Cria uma chave de API nova e o hash que vai no arquivo de configuração. + +Todos os comandos aceitam --json (saída para programas) e --lang en|es|pt-BR|fr-CA. +Códigos de saída: 0 concluído, 1 a impressão falhou, 2 linha de comando incorreta, 3 erros na etiqueta, 4 impressora não está pronta, 5 bloqueado por política, 6 cancelado. + + + O arquivo de configuração {0} foi criado. Adicione suas impressoras em allowedPrinters e execute "rotula serve". + + + O arquivo de configuração {0} já existe; ele não foi alterado. + + + A opção {0} precisa de um valor. + + + Chave de API nova (mostrada só agora; entregue-a ao sistema que chama o servidor): {0} +Hash a adicionar em apiKeyHashes no arquivo de configuração: {1} + + + Indique exatamente um arquivo de etiqueta, por exemplo: rotula print envio.rotula --printer "Doca 1" + + + Registro {0}: + + + Execute "rotula help" para ver os comandos e as opções. + + + Comando desconhecido "{0}". + + + Opção desconhecida {0}. + + + Aviso: + + + Faltam respostas às perguntas da etiqueta ou alguma não é válida. A etiqueta pergunta: {0}. As perguntas marcadas com * são obrigatórias; as demais usam o valor padrão. + + + É necessária uma chave de API válida. Envie-a no cabeçalho X-Api-Key. + + + Não foi possível ler a solicitação: {0} + + + As cópias devem ser um número inteiro de 1 a {0}. + + + Não foi possível ler o arquivo de dados {0}: {1} + + + O arquivo de dados {0} não existe.{1} + + + Os dados de {0} devem ser uma lista de registros (objetos com nomes de campo e valores) ou um único registro. {1} + + + A resolução deve ser 203, 300 ou 600 DPI. + + + Este servidor não aceita etiquetas enviadas como conteúdo (allowInlineLabels está desativado). Indique um arquivo de etiqueta de uma das suas pastas de etiquetas. + + + Envie o nome de um arquivo de etiqueta (label) ou o conteúdo da etiqueta (labelContent), não os dois. + + + A etiqueta {0} não foi encontrada. + + + A etiqueta {0} não é um arquivo .rotula dentro de uma das pastas de etiquetas deste servidor. Indique-a em relação a uma pasta de etiquetas, sem letra de unidade nem "..". + + + Indique um arquivo de etiqueta (label) ou envie o conteúdo da etiqueta (labelContent). + + + A etiqueta {0} é maior que o limite de {1} KB. + + + Os dados não têm registros, então nada foi impresso. + + + Nenhum registro gerou uma etiqueta, então nada foi enviado. Veja os problemas listados no resultado. + + + Não foi possível gravar o arquivo {0}: {1} + + + Indique uma impressora do Windows ou o endereço de uma impressora de rede, não as duas. + + + A impressora {0} não está na lista de impressoras permitidas deste servidor (allowedPrinters). Nada foi enviado. + + + Indique uma impressora: uma impressora do Windows (printer) ou o endereço de uma impressora de rede (host). + + + A impressão com o mecanismo de impressão está desativada pela política da sua organização para este computador. Fale com o seu departamento de TI. + + + Uma solicitação pode ter no máximo {0} registros. + + + Envie os valores de uma etiqueta ou uma lista de registros, não os dois. + + + [padrão] + + + Não há impressoras instaladas no Windows. + + + [offline] + + + Nada foi impresso (simulação). Este é o ZPL que seria enviado para {0}. + + + {0} etiqueta(s) enviada(s) para {1}. + + + {0} registro(s), {1} etiqueta(s), enviado(s) para {2} em {3} trabalho(s). + + + O servidor não conseguiu escutar em {0}: {1} + + + Mecanismo de impressão do Rotula escutando em {0}. Configuração: {1}. Pressione Ctrl+C para parar. + + + "{0}" em apiKeyHashes não é um hash de chave (64 caracteres hexadecimais). Crie um com "rotula new-key". + + + A pasta de etiquetas {0} não existe. + + + Não é possível usar o arquivo de configuração {0}: {1} + + + O arquivo de configuração {0} não existe. Crie um com "rotula serve --init". + + + Não há pastas de etiquetas (labelFolders) e as etiquetas enviadas como conteúdo estão desativadas, então nenhuma etiqueta pode ser impressa. + + + O arquivo de configuração {0} não tem nenhuma chave de API (apiKeyHashes). Crie uma com "rotula new-key". + + + Ainda não há impressoras permitidas (allowedPrinters), então toda solicitação de impressão será recusada. + + + O servidor aceita chamadas de outros computadores por http sem criptografia: as chaves de API e os dados das etiquetas trafegam pela rede sem criptografia. Use https. + + diff --git a/src/Rotula.App/Rotula.App.csproj b/src/Rotula.App/Rotula.App.csproj index 80ef5de..5449ed4 100644 --- a/src/Rotula.App/Rotula.App.csproj +++ b/src/Rotula.App/Rotula.App.csproj @@ -40,8 +40,8 @@ Assets/rotula.ico true - - en;es + + en;es;pt-BR;fr-CA diff --git a/src/Rotula.App/Services/AppSettings.cs b/src/Rotula.App/Services/AppSettings.cs index df4971e..b619e79 100644 --- a/src/Rotula.App/Services/AppSettings.cs +++ b/src/Rotula.App/Services/AppSettings.cs @@ -1,11 +1,12 @@ using System.Text.Json; +using Rotula.Files.Policy; namespace Rotula.App.Services; /// User preferences stored in %AppData%\Rotula\settings.json. public sealed class AppSettings { - /// "auto" (follow Windows), "en" or "es". + /// "auto" (follow Windows), "en", "es", "pt-BR" or "fr-CA". public string Language { get; set; } = "auto"; /// Ruler unit: "Millimeters", "Inches" or "Dots". @@ -46,6 +47,24 @@ public sealed class AppSettings /// The printer and printer profile used last time, so the Print dialog opens ready to go. public PrintSettings Print { get; set; } = new(); + + /// + /// The user switched on the daily update check (Help menu). Off by default: without it Rotula makes no network request + /// of its own. An organization policy overrides it (see ). + /// + public bool UpdateCheckEnabled { get; set; } + + /// When the release page was last asked (UTC); null before the first check. + public DateTimeOffset? LastUpdateCheckUtc { get; set; } + + /// The newer version whose notice the user closed, so that notice is not shown again for the same version. + public string? UpdateDismissedVersion { get; set; } + + /// The language in use: the organization's choice when a policy sets one, otherwise the user's own. + public string LanguageIn(RotulaPolicy policy) => policy.Language ?? Language; + + /// Whether the update check runs: a policy that switches it on or off wins over the user's choice. + public bool UpdateCheckIn(RotulaPolicy policy) => policy.UpdateCheck ?? UpdateCheckEnabled; } /// The last printer and printer profile, stored in settings.json. Enum values are stored by name. @@ -63,7 +82,7 @@ public sealed class PrintSettings /// Host name or IP address of a network printer. public string Host { get; set; } = string.Empty; - /// TCP port of a network printer (9100 for Zebra). + /// TCP port of a network printer (9100 on most label printers). public int Port { get; set; } = 9100; /// Printhead resolution: "Dpi203", "Dpi300" or "Dpi600". @@ -83,6 +102,15 @@ public sealed class PrintSettings /// Print speed 1-14 (^PR) or null to leave the printer as it is. public int? SpeedIps { get; set; } + + /// Ask a network printer for its status (~HS) before and after printing. On by default: a TCP printer answers on the same port. + public bool StatusCheckNetwork { get; set; } = true; + + /// + /// Ask a Windows printer for its status before and after printing. Off by default: many USB drivers cannot answer, and a queue that is + /// not a ZPL compatible printer would print the query as text. + /// + public bool StatusCheckWindows { get; set; } } /// Loads and saves . @@ -103,8 +131,9 @@ public sealed class JsonSettingsStore : ISettingsStore { private const string Header = "// Rotula settings. Edit with the program closed, or change them from the menus.\n" + - "// language: \"auto\" follows Windows, or use \"en\" / \"es\".\n" + - "// unit: \"Millimeters\", \"Inches\" or \"Dots\".\n"; + "// language: \"auto\" follows Windows, or use \"en\" / \"es\" / \"pt-BR\" / \"fr-CA\".\n" + + "// unit: \"Millimeters\", \"Inches\" or \"Dots\".\n" + + "// updateCheckEnabled: true asks GitHub once a day whether a newer version exists (off by default; nothing is sent about you).\n"; private static readonly JsonSerializerOptions Options = new() { diff --git a/src/Rotula.App/Services/ServiceInterfaces.cs b/src/Rotula.App/Services/ServiceInterfaces.cs index 5492c79..53a0dfe 100644 --- a/src/Rotula.App/Services/ServiceInterfaces.cs +++ b/src/Rotula.App/Services/ServiceInterfaces.cs @@ -164,4 +164,20 @@ public interface IToolDialogService /// returns false, which means "cancelled": a label that asks questions is never printed with guessed answers. /// bool ShowPromptDialog(Rotula.App.ViewModels.PromptDialogViewModel viewModel) => false; + + /// Shows the print station full screen and returns when the operator has left it. The default shows nothing. + void ShowPrintStation(Rotula.App.ViewModels.StationViewModel viewModel) { } + + /// Shows the tool that makes the exit PIN line for station.json. The default shows nothing. + void ShowStationPinTool(Rotula.App.ViewModels.StationPinViewModel viewModel) { } + + /// Shows the "Label sheets" dialog (office printer sheets as a PDF) modally. The default offers no dialog. + void ShowSheetDialog(Rotula.App.ViewModels.SheetLayoutViewModel viewModel) { } +} + +/// Opens a web address in the user's browser (the update notice's "View release" link). +public interface IUrlLauncher +{ + /// Opens ; returns false when no browser could be started. + bool Open(string url); } diff --git a/src/Rotula.App/Services/StationLaunch.cs b/src/Rotula.App/Services/StationLaunch.cs new file mode 100644 index 0000000..7db43aa --- /dev/null +++ b/src/Rotula.App/Services/StationLaunch.cs @@ -0,0 +1,52 @@ +namespace Rotula.App.Services; + +/// What the program was started for. +public enum LaunchMode +{ + /// The designer (optionally opening a label given as the only argument). + Designer, + /// The print station (--station [folder-or-file]). + Station, + /// The tool that makes a station exit PIN (--station-pin). + StationPin, +} + +/// The command line, read once at start-up. +/// What to open. +/// The label (designer) or the station folder, configuration or label (station); null when none was given. +public sealed record StationLaunch(LaunchMode Mode, string? Path) +{ + /// The option that starts the print station. + public const string StationOption = "--station"; + + /// The option that starts the PIN tool. + public const string PinOption = "--station-pin"; + + /// + /// Reads the arguments. --station may be followed by a folder, a station.json or a .rotula file (without one, the shared policy file is used); + /// --station-pin opens the PIN tool; anything else is the designer with the first argument as the label to open. Options ignore case and may + /// also be written /station, as Windows shortcuts often do, or --station=C:\Labels. + /// + public static StationLaunch Parse(IReadOnlyList args) + { + for (int i = 0; i < args.Count; i++) + { + string a = args[i].Trim(); + if (IsOption(a, PinOption)) return new StationLaunch(LaunchMode.StationPin, null); + if (IsOption(a, StationOption)) + { + string? path = i + 1 < args.Count && !args[i + 1].StartsWith("--", StringComparison.Ordinal) ? args[i + 1] : null; + return new StationLaunch(LaunchMode.Station, string.IsNullOrWhiteSpace(path) ? null : path.Trim()); + } + if (a.StartsWith(StationOption + "=", StringComparison.OrdinalIgnoreCase) || a.StartsWith("/station=", StringComparison.OrdinalIgnoreCase)) + { + string path = a[(a.IndexOf('=') + 1)..].Trim().Trim('"'); + return new StationLaunch(LaunchMode.Station, path.Length == 0 ? null : path); + } + } + return new StationLaunch(LaunchMode.Designer, args.Count > 0 ? args[0] : null); + } + + private static bool IsOption(string arg, string option) => + string.Equals(arg, option, StringComparison.OrdinalIgnoreCase) || string.Equals(arg, "/" + option[2..], StringComparison.OrdinalIgnoreCase); +} diff --git a/src/Rotula.App/Services/UpdateCheck.cs b/src/Rotula.App/Services/UpdateCheck.cs new file mode 100644 index 0000000..d1f0771 --- /dev/null +++ b/src/Rotula.App/Services/UpdateCheck.cs @@ -0,0 +1,291 @@ +using System.Globalization; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; + +namespace Rotula.App.Services; + +// The opt-in update check. Rules (docs/decisions.md, "Update check"): off unless the user or an administrator switches it on; +// at most one anonymous GET of the public "latest release" page of the GitHub API a day; nothing about the user or the computer +// is sent; nothing is ever downloaded or installed. Everything except the HTTP call itself is plain code with tests. + +/// A semantic version (major.minor.patch with an optional pre-release part), compared by the SemVer 2.0 rules. +public sealed class SemanticVersion : IComparable, IEquatable +{ + private readonly string[] _pre; + + private SemanticVersion(int major, int minor, int patch, string[] pre) + { + Major = major; + Minor = minor; + Patch = patch; + _pre = pre; + } + + /// Major version. + public int Major { get; } + + /// Minor version. + public int Minor { get; } + + /// Patch version. + public int Patch { get; } + + /// The pre-release part after "-" (for example "rc1"), or empty for a normal release. + public string PreRelease => string.Join('.', _pre); + + /// True for a pre-release such as 0.4.0-rc1. + public bool IsPreRelease => _pre.Length > 0; + + /// + /// Reads "1.2.3", "v1.2.3", "1.2.3-rc.1" or "1.2.3+build" (build metadata is ignored, as SemVer says). Returns false for + /// anything else, so a strange tag on the release page can never be taken for a newer version. + /// + public static bool TryParse(string? text, out SemanticVersion version) + { + version = null!; + if (string.IsNullOrWhiteSpace(text)) return false; + string s = text.Trim(); + if (s.Length > 64) return false; + if (s[0] is 'v' or 'V') s = s[1..]; + int plus = s.IndexOf('+'); + if (plus >= 0) s = s[..plus]; + string[] pre = Array.Empty(); + int dash = s.IndexOf('-'); + if (dash >= 0) + { + pre = s[(dash + 1)..].Split('.'); + s = s[..dash]; + if (pre.Any(p => p.Length == 0 || !p.All(c => char.IsAsciiLetterOrDigit(c) || c == '-'))) return false; + } + var parts = s.Split('.'); + if (parts.Length != 3) return false; + var numbers = new int[3]; + for (int i = 0; i < 3; i++) + { + if (parts[i].Length == 0 || !parts[i].All(char.IsAsciiDigit)) return false; + if (!int.TryParse(parts[i], NumberStyles.None, CultureInfo.InvariantCulture, out numbers[i])) return false; + } + version = new SemanticVersion(numbers[0], numbers[1], numbers[2], pre); + return true; + } + + /// + public int CompareTo(SemanticVersion? other) + { + if (other is null) return 1; + int c = Major.CompareTo(other.Major); + if (c == 0) c = Minor.CompareTo(other.Minor); + if (c == 0) c = Patch.CompareTo(other.Patch); + if (c != 0) return c; + // A normal release is newer than any pre-release of the same number. + if (_pre.Length == 0 && other._pre.Length == 0) return 0; + if (_pre.Length == 0) return 1; + if (other._pre.Length == 0) return -1; + for (int i = 0; i < Math.Min(_pre.Length, other._pre.Length); i++) + { + c = ComparePart(_pre[i], other._pre[i]); + if (c != 0) return c; + } + return _pre.Length.CompareTo(other._pre.Length); + } + + private static int ComparePart(string a, string b) + { + bool an = a.All(char.IsAsciiDigit), bn = b.All(char.IsAsciiDigit); + if (an && bn) + { + // Compare digit strings without parsing, so a very long number cannot overflow. + string ta = a.TrimStart('0'), tb = b.TrimStart('0'); + return ta.Length != tb.Length ? ta.Length.CompareTo(tb.Length) : string.CompareOrdinal(ta, tb); + } + if (an) return -1; // numeric identifiers sort before alphanumeric ones + if (bn) return 1; + return string.CompareOrdinal(a, b); + } + + /// + public bool Equals(SemanticVersion? other) => CompareTo(other) == 0; + + /// + public override bool Equals(object? obj) => obj is SemanticVersion v && Equals(v); + + /// + public override int GetHashCode() => HashCode.Combine(Major, Minor, Patch, PreRelease); + + /// + public override string ToString() => IsPreRelease ? $"{Major}.{Minor}.{Patch}-{PreRelease}" : $"{Major}.{Minor}.{Patch}"; +} + +/// When the next update check is due. +public static class UpdateSchedule +{ + /// At most one check per this interval. + public static readonly TimeSpan Interval = TimeSpan.FromDays(1); + + /// + /// True when no check was made yet, or the last one is at least ago. A last check in the future + /// (the clock was set back) also counts as due, otherwise a wrong date could stop the check for a long time. + /// + public static bool IsDue(DateTimeOffset? lastCheckUtc, DateTimeOffset nowUtc) + { + if (lastCheckUtc is not { } last) return true; + if (last > nowUtc + TimeSpan.FromMinutes(5)) return true; + return nowUtc - last >= Interval; + } +} + +/// The newest published release. +/// Its version, read from the tag. +/// The release page on GitHub (built by Rotula, not taken from the answer). +public sealed record ReleaseInfo(SemanticVersion Version, string PageUrl); + +/// Reads the answer of the GitHub "latest release" API. +public static class GitHubRelease +{ + /// The repository whose releases are checked. + public const string Repository = "P4Software/Rotula"; + + /// The only address the update check ever asks. + public const string LatestApiUrl = "https://api.github.com/repos/" + Repository + "/releases/latest"; + + /// The list of releases, used when a release page cannot be named. + public const string ReleasesPageUrl = "https://github.com/" + Repository + "/releases"; + + /// + /// Reads tag_name of the answer. Drafts and pre-releases are ignored (the API should never return them, but a wrong + /// answer must not offer a test build). The page link is built from the parsed version, so the answer can never send the + /// user to another site. Returns null when the answer is not understood. + /// + public static ReleaseInfo? Parse(string json) + { + try + { + using var doc = JsonDocument.Parse(json, new JsonDocumentOptions { MaxDepth = 16 }); + var root = doc.RootElement; + if (root.ValueKind != JsonValueKind.Object) return null; + if (IsTrue(root, "draft") || IsTrue(root, "prerelease")) return null; + if (!root.TryGetProperty("tag_name", out var tag) || tag.ValueKind != JsonValueKind.String) return null; + if (!SemanticVersion.TryParse(tag.GetString(), out var version) || version.IsPreRelease) return null; + return new ReleaseInfo(version, $"{ReleasesPageUrl}/tag/v{version}"); + } + catch (JsonException) + { + return null; + } + } + + private static bool IsTrue(JsonElement root, string name) => root.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.True; +} + +/// Asks where the newest release is. Production: ; tests use a fake. +public interface IReleaseFeed +{ + /// The newest release, or null when the answer was not understood. Network failures throw . + Task GetLatestAsync(CancellationToken cancellationToken); +} + +/// +/// One anonymous GET of . The request carries only what GitHub requires (a product name +/// as User-Agent, without the version) and an Accept header: no cookies, no identifiers. The Windows proxy settings are used, with +/// the signed-in user's credentials for a proxy that asks for them, so it works behind a company proxy like the browser does. +/// +public sealed class GitHubReleaseFeed : IReleaseFeed, IDisposable +{ + /// Longest answer read (the real one is a few kilobytes). + public const int MaxAnswerBytes = 512 * 1024; + + private readonly HttpClient _http; + + /// Creates the feed. is for tests; null uses the system proxy settings. + public GitHubReleaseFeed(HttpMessageHandler? handler = null) + { + handler ??= new SocketsHttpHandler + { + UseCookies = false, + UseProxy = true, + // Default proxy = the Windows (WinINET / WinHTTP) settings; the credentials are for proxies that need sign-in. + DefaultProxyCredentials = CredentialCache.DefaultCredentials, + AllowAutoRedirect = true, + MaxAutomaticRedirections = 3, + ConnectTimeout = TimeSpan.FromSeconds(10), + }; + _http = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20), MaxResponseContentBufferSize = MaxAnswerBytes }; + } + + /// + public async Task GetLatestAsync(CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(HttpMethod.Get, GitHubRelease.LatestApiUrl); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue("Rotula", null)); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); + using var response = await _http.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) throw new HttpRequestException($"The release page answered {(int)response.StatusCode}.", null, response.StatusCode); + string json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + return GitHubRelease.Parse(json); + } + + /// + public void Dispose() => _http.Dispose(); +} + +/// What one update check found. +public enum UpdateCheckStatus +{ + /// The check is switched off; nothing was sent. + Off, + /// The last check is less than a day old; nothing was sent. + NotDue, + /// This is the newest version. + UpToDate, + /// A newer version exists. + Available, + /// No answer (offline, blocked, a wrong answer). Silent: it is tried again the next day. + Failed, +} + +/// The outcome of . +/// What happened. +/// The newer release when is . +/// True when the release page was asked, so the caller records the time of the check. +public sealed record UpdateCheckResult(UpdateCheckStatus Status, ReleaseInfo? Release = null, bool Asked = false); + +/// Decides whether to ask, asks, and compares the answer with the running version. +public sealed class UpdateChecker +{ + private readonly IReleaseFeed _feed; + private readonly SemanticVersion? _current; + + /// Creates the checker. + /// Where the newest release is read. + /// The running version, for example "0.3.5" (build metadata after "+" is ignored). + public UpdateChecker(IReleaseFeed feed, string currentVersion) + { + _feed = feed ?? throw new ArgumentNullException(nameof(feed)); + _current = SemanticVersion.TryParse(currentVersion, out var v) ? v : null; + } + + /// + /// Checks once. Asks nothing when is false or the last check is less than a day old. Never throws + /// for network trouble: a failure is and nobody is bothered with it. + /// + public async Task CheckAsync(bool enabled, DateTimeOffset? lastCheckUtc, DateTimeOffset nowUtc, CancellationToken cancellationToken = default) + { + if (!enabled) return new UpdateCheckResult(UpdateCheckStatus.Off); + if (!UpdateSchedule.IsDue(lastCheckUtc, nowUtc)) return new UpdateCheckResult(UpdateCheckStatus.NotDue); + ReleaseInfo? latest; + try + { + latest = await _feed.GetLatestAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or IOException or InvalidOperationException) + { + return new UpdateCheckResult(UpdateCheckStatus.Failed, Asked: true); + } + if (latest is null || _current is null) return new UpdateCheckResult(UpdateCheckStatus.Failed, Asked: true); + return latest.Version.CompareTo(_current) > 0 + ? new UpdateCheckResult(UpdateCheckStatus.Available, latest, Asked: true) + : new UpdateCheckResult(UpdateCheckStatus.UpToDate, Asked: true); + } +} diff --git a/src/Rotula.App/Services/WpfServices.cs b/src/Rotula.App/Services/WpfServices.cs index c3da1ef..17b21e5 100644 --- a/src/Rotula.App/Services/WpfServices.cs +++ b/src/Rotula.App/Services/WpfServices.cs @@ -228,6 +228,30 @@ public bool ShowPromptDialog(Rotula.App.ViewModels.PromptDialogViewModel viewMod return viewModel.Accepted; } + /// + public void ShowPrintStation(Rotula.App.ViewModels.StationViewModel viewModel) + { + // Modal over the designer: the operator works only in the station until it is left (with the PIN when one is set). + var window = new StationWindow(viewModel) { Owner = Application.Current?.MainWindow, ShowInTaskbar = false }; + window.ShowDialog(); + } + + /// + public void ShowStationPinTool(Rotula.App.ViewModels.StationPinViewModel viewModel) + { + var window = new StationPinWindow(viewModel) { Owner = Application.Current?.MainWindow }; + window.ShowDialog(); + } + + /// + public void ShowSheetDialog(Rotula.App.ViewModels.SheetLayoutViewModel viewModel) + { + // Opened from the main window or from the Print dialog: the active window owns it, so it stays on top of the one it came from. + var owner = Application.Current?.Windows.OfType().FirstOrDefault(w => w.IsActive && w.IsLoaded) ?? Application.Current?.MainWindow; + var window = new SheetLayoutWindow(viewModel) { Owner = owner }; + window.ShowDialog(); + } + /// public void ShowDataWindow(Rotula.App.ViewModels.DataMergeViewModel viewModel) { @@ -275,3 +299,23 @@ public void ShowAbout(string title, string body) MessageDialogWindow.Show(MessageKind.Info, title, body, new DialogChoice(Loc.Get("Common.Close"), IsPrimary: true, IsDefault: true, IsCancel: true)); } } + +/// Hands a web address to the default browser. Rotula itself downloads nothing. +public sealed class ShellUrlLauncher : IUrlLauncher +{ + /// + public bool Open(string url) + { + // Only https addresses: the update notice builds its own link, but a launcher must never start a program or a file. + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps) return false; + try + { + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true }); + return true; + } + catch (Exception ex) when (ex is System.ComponentModel.Win32Exception or InvalidOperationException) + { + return false; + } + } +} diff --git a/src/Rotula.App/ViewModels/DataMergeViewModel.Sheets.cs b/src/Rotula.App/ViewModels/DataMergeViewModel.Sheets.cs new file mode 100644 index 0000000..5dbf011 --- /dev/null +++ b/src/Rotula.App/ViewModels/DataMergeViewModel.Sheets.cs @@ -0,0 +1,117 @@ +using System.Globalization; +using Rotula.App.Localization; +using Rotula.Core.Merge; +using Rotula.Core.Sheets; +using Rotula.Core.Validation; +using Rotula.Core.Variables; +using Rotula.Core.Variables.Counters; +using Rotula.Rendering; +using Rotula.Rendering.Export; +using Rotula.Rendering.Input; + +namespace Rotula.App.ViewModels; + +/// The ticked records written onto label sheets as a PDF (the "Label sheets" dialog uses this when data is loaded). +public sealed partial class DataMergeViewModel +{ + /// Most labels one sheet export writes; beyond it the PDF would be too large to print or even open. + internal const long MaxSheetLabels = 100_000; + + /// + /// How many labels the ticked records make with copies each, or null when a column of the data sets the copies (then + /// the number is only known once the records are read). + /// + internal long? EstimateSheetLabels(int labelCopies) => CopiesSource switch + { + CopiesSource.Column => null, + CopiesSource.Fixed => int.TryParse(FixedCopiesText.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out int n) && n >= 1 + ? (long)SelectedCount * n + : SelectedCount, + _ => (long)SelectedCount * Math.Max(1, labelCopies), + }; + + /// + /// Writes the ticked records onto sheets as a PDF, through the same record iteration as printing (): the same + /// selection, copies, skipped invalid records and print-time questions, and the serial numbers of the batch. A PDF will be printed, so its serial + /// numbers count as used once the file is complete; a failed or cancelled export gives them back. Every copy of a record carries that record's + /// values, as the copies of a printed record do. + /// + /// The host window's single-operation runner. + /// Receives 0-100 while records are read. + /// Copies per record when the copies setting is the label's own quantity; null keeps the label's own. + /// The sheet (already checked). + /// Start position, order and guides. + /// The PDF file to write. + internal async Task ExportSheetsCoreAsync(Func, Task> run, Action progress, + int? labelCopies, SheetLayout layout, SheetExportOptions options, string path) + { + // The PDF keeps the design's own resolution: it is drawn, not sent to a label printer. + string? error = Snapshot(out var doc, out var ds, convertToPrinter: false, quantity: labelCopies); + if (error is not null) return new BatchOutcome(error, true); + if (!AskBatchPrompts(ds)) return null; + + VariableJob? serials = null; + if (VariableEngine.HasDefinitions(doc)) + { + serials = VariableJob.Begin(doc, ds.CreateBatchContext(), ds.BatchCount); + ds.Options.Job = serials; + } + var report = new MergeReport(); + var closing = new List(); + SheetExportResult? result = null; + string? writeError = null; + bool tooMany = false; + var progressReporter = new Progress(progress); + bool ok = await run(Loc.Get("Print.Sheet.Busy"), ct => Task.Run(() => + { + int lastItem = 0, writtenThrough = 0; + long count = 0; + IEnumerable Labels() + { + foreach (var merged in MergeBatch.Labels(doc, ds, report)) + { + ct.ThrowIfCancellationRequested(); + // Converted once per record: the copies are the same picture. + var label = LabelDocumentAdapter.ToRenderLabelResolved(doc, merged.Values); + for (int c = 0; c < merged.Copies; c++) + { + if (++count > MaxSheetLabels) { tooMany = true; yield break; } + yield return label; + } + lastItem = merged.ItemNumber; + if (report.Processed % 64 == 0) ((IProgress)progressReporter).Report(100.0 * report.Processed / ds.BatchCount); + } + } + try + { + result = SheetExporter.SavePdf(layout, Labels(), path, options); + if (tooMany) + { + // Cut short: the file is not what the operator asked for, so it is removed and every number goes back. + if (File.Exists(path)) File.Delete(path); + result = null; + } + else if (result.Pages > 0) writtenThrough = lastItem; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + writeError = Loc.Get("File.Error.Write", path); + } + finally + { + if (serials is not null) closing.AddRange(serials.Complete(writtenThrough)); + } + }, ct)); + if (!ok) return null; + ShowReport(report); + string notes = string.Join(" ", closing.Select(Loc.FormatIssue)); + if (writeError is not null) return new BatchOutcome(writeError, true); + if (tooMany) return new BatchOutcome(Loc.Get("Print.Sheet.TooMany", MaxSheetLabels), true); + if (result is null || result.Pages == 0) return new BatchOutcome(Loc.Get("Data.Batch.NothingProduced"), true); + string text = Loc.Get("Print.Sheet.Done", result.Labels, result.Pages, Path.GetFileName(path)); + if (report.SkippedRecords.Count > 0) text += " " + Loc.Get("Data.Batch.Skipped", report.SkippedRecords.Count); + if (report.Stopped && report.SkippedRecords.Count > 0) text += " " + Loc.Get("Data.Batch.Stopped", report.SkippedRecords[^1]); + if (result.Issues.Count > 0) text += " " + string.Join(" ", result.Issues.Select(i => i.Message).Take(3)); + return new BatchOutcome(text + (notes.Length > 0 ? " " + notes : string.Empty), report.HasErrors); + } +} diff --git a/src/Rotula.App/ViewModels/DataMergeViewModel.cs b/src/Rotula.App/ViewModels/DataMergeViewModel.cs index b99f267..8fa6562 100644 --- a/src/Rotula.App/ViewModels/DataMergeViewModel.cs +++ b/src/Rotula.App/ViewModels/DataMergeViewModel.cs @@ -1100,6 +1100,11 @@ private async Task PrintAllAsync() // Raw ZPL imported from bytes must reach the printer byte for byte; collected once, not per record. var rawBlocks = ByteExactBlocks.From(doc); var progressReporter = new Progress(progress); + // Every job is checked before it goes out (paper running out halfway stops the batch cleanly); the printer is asked whether it + // finished only once, after the last job, instead of waiting for each job to come out. + var batchTarget = target!; + var jobTarget = batchTarget.WithStatus(batchTarget.Status with { VerifyAfterPrinting = false }); + var verification = new List(); bool ok = await run(Loc.Get("Data.Busy.Printing"), async ct => { int inFlight = 0; // last record of the job being sent right now @@ -1111,7 +1116,9 @@ await Task.Run(async () => { ct.ThrowIfCancellationRequested(); inFlight = job.ItemsThrough; - var result = await _service.PrintZplAsync(target!, jobs == 0 ? setup + job.Zpl : job.Zpl, doc.Encoding, rawBlocks, ct); + var result = await _service.PrintZplAsync(jobTarget, jobs == 0 ? setup + job.Zpl : job.Zpl, doc.Encoding, rawBlocks, ct); + foreach (var note in result.Notices) + if (!verification.Any(v => v.MessageKey == note.MessageKey)) verification.Add(note); if (!result.Succeeded) { failure = result.Error; @@ -1137,10 +1144,13 @@ await Task.Run(async () => // Always confirms (also when the run failed or was cancelled), so a reservation is never left hanging. if (serials is not null) closing.AddRange(serials.Complete(confirmedThrough)); } + // After the numbers are confirmed: a fault reported now leaves them used, because the labels may have printed. + if (failure is null && !cancelled && jobs > 0 && batchTarget.Status.VerifyAfterPrinting) + verification.AddRange(await _service.VerifyPrintedAsync(batchTarget, ct)); }); if (!ok) return null; ShowReport(report); - string notes = string.Join(" ", closing.Select(Loc.FormatIssue)); + string notes = string.Join(" ", closing.Select(Loc.FormatIssue).Concat(verification.Select(PrintMessages.DescribeNotice))); if (failure is not null) { return new BatchOutcome(PrintMessages.Describe(failure) + " " + Loc.Get("Data.Batch.Partial", jobs) + (notes.Length > 0 ? " " + notes : string.Empty), @@ -1149,7 +1159,8 @@ await Task.Run(async () => if (cancelled) return new BatchOutcome(Loc.Get("Data.Batch.CancelledPartial", jobs) + (notes.Length > 0 ? " " + notes : string.Empty), false); Printer.Persist(); - return new BatchOutcome(BatchSummary("Data.PrintAll.Done", report, jobs) + (notes.Length > 0 ? " " + notes : string.Empty), report.HasErrors || report.Emitted == 0); + return new BatchOutcome(BatchSummary("Data.PrintAll.Done", report, jobs) + (notes.Length > 0 ? " " + notes : string.Empty), + report.HasErrors || report.Emitted == 0 || verification.Any(v => v.MessageKey == PrinterStatusKeys.FaultAfterJob)); } /// The result line of a batch print or export, and whether it reports a problem. diff --git a/src/Rotula.App/ViewModels/FieldsViewModel.cs b/src/Rotula.App/ViewModels/FieldsViewModel.cs index 2d44210..62bc7d4 100644 --- a/src/Rotula.App/ViewModels/FieldsViewModel.cs +++ b/src/Rotula.App/ViewModels/FieldsViewModel.cs @@ -429,9 +429,9 @@ private static bool IsPlaceholderContent(LabelElement e, string content) private static string DefaultTextIn(string language) { - var current = Loc.Instance.Culture.TwoLetterISOLanguageName; - if (current == language) return Loc.Get("Element.DefaultText"); - // The label may have been designed in the other language: compare with that language's default text too. + // Full culture names ("pt-BR"), as in Loc.SupportedLanguages: a two-letter name would never match them. + if (Loc.Instance.LanguageCode == language) return Loc.Get("Element.DefaultText"); + // The label may have been designed in another language: compare with that language's default text too. var rm = new System.Resources.ResourceManager("Rotula.App.Resources.Strings", typeof(Loc).Assembly); try { return rm.GetString("Element.DefaultText", CultureInfo.GetCultureInfo(language)) ?? string.Empty; } catch (System.Resources.MissingManifestResourceException) { return string.Empty; } diff --git a/src/Rotula.App/ViewModels/MainViewModel.cs b/src/Rotula.App/ViewModels/MainViewModel.cs index 250b697..1ab4d95 100644 --- a/src/Rotula.App/ViewModels/MainViewModel.cs +++ b/src/Rotula.App/ViewModels/MainViewModel.cs @@ -5,10 +5,12 @@ using CommunityToolkit.Mvvm.Input; using Rotula.App.Editing; using Rotula.Files; +using Rotula.Files.Policy; using Rotula.App.Localization; using Rotula.App.Services; using Rotula.Core.Elements; using Rotula.Core.Model; +using Rotula.Core.Sheets; using Rotula.Core.Validation; using Rotula.Core.Zpl; using Rotula.Printing; @@ -79,6 +81,11 @@ public sealed record ToolboxItem(ToolboxKind Kind, string NameKey, string Glyph) } /// The services the main view-model depends on. +/// +/// is what the organization fixed through Group Policy or Intune (null: nothing managed); +/// is the opt-in update check (null: no update check at all, as in tests that do not ask for one); +/// opens the release page; is the time source of the daily check (null: the system clock). +/// public sealed record MainServices( IFileDialogService Files, IMessageService Messages, @@ -90,7 +97,11 @@ public sealed record MainServices( ISettingsStore Settings, IToolDialogService Dialogs, IDatabaseServices? Database = null, - IVariableServices? Variables = null); + IVariableServices? Variables = null, + RotulaPolicy? Policy = null, + UpdateChecker? Updates = null, + IUrlLauncher? Links = null, + TimeProvider? Clock = null); /// /// View-model of the main window: the open label, its files, editing commands, view settings (zoom, unit, grid), @@ -109,6 +120,7 @@ public sealed partial class MainViewModel : ObservableObject private readonly MainServices _svc; private readonly AppSettings _settings; + private readonly RotulaPolicy _policy; private readonly List _allCommands = new(); private bool _syncingSelection; @@ -117,7 +129,8 @@ public MainViewModel(MainServices services, EditorSession? session = null) { _svc = services; _settings = services.Settings.Load(); - Loc.Instance.SetLanguage(_settings.Language); + _policy = services.Policy ?? RotulaPolicy.None; + Loc.Instance.SetLanguage(_settings.LanguageIn(_policy)); Session = session ?? new EditorSession(); Commands = new EditorCommands(Session); // Without a counter store (tests, headless use) a counter that remembers its value refuses to print instead of repeating numbers. @@ -128,7 +141,8 @@ public MainViewModel(MainServices services, EditorSession? session = null) ConnectFormatting(); // Handles and selection frames are measured with the record shown on the canvas, not only the sample values. Session.Bounds.DataProvider = () => Preview.Data; - Printer = new PrinterSettingsViewModel(services.Printing, _settings.Print, SaveSettings, () => Session.Document, applyLabelDpi: SetLabelDpi); + Printer = new PrinterSettingsViewModel(services.Printing, _settings.Print, SaveSettings, () => Session.Document, applyLabelDpi: SetLabelDpi, + defaultPrinter: _policy.DefaultPrinter); Data = new DataMergeViewModel(services.Printing, Printer, () => Session.Document, () => DocumentName, services.Files, services.Messages, services.Database, services.Dialogs, Variables); Data.SettingsEdited += (_, _) => Session.MarkDirty(); Data.PreviewDataChanged += (_, _) => OnPreviewDataChanged(); @@ -281,7 +295,7 @@ private void FillToolbox() /// A cancelled dialog or a file that fails to load leaves it in place. /// [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(ShowDataCommand), nameof(OpenDataFileCommand))] + [NotifyCanExecuteChangedFor(nameof(ShowDataCommand), nameof(OpenDataFileCommand), nameof(ShowSheetsCommand))] private bool _showStartScreen = true; /// @@ -379,8 +393,69 @@ partial void OnUnitChanged(LengthUnit value) /// Grid configuration for the canvas. public GridSettings Grid => new(LengthUnit.Millimeters, _settings.GridSpacingMm, GridVisible, SnapToGrid); - /// The language setting: "auto", "en" or "es". - public string LanguageSetting => _settings.Language; + /// + /// The language setting in use: "auto" or one of ("en", "es", "pt-BR", "fr-CA"); the + /// organization's choice when a policy sets one. + /// + public string LanguageSetting => _settings.LanguageIn(_policy); + + /// True when the organization fixed the language; the menu entries are then disabled with a note. + public bool IsLanguageManaged => _policy.IsLanguageManaged; + + /// True when the user may choose the language. + public bool CanChooseLanguage => !IsLanguageManaged; + + /// + /// The daily update check (Help menu). Off by default; switching it on starts the first check at once. When a policy + /// switches it on or off the menu entry is disabled and shows the policy's value. + /// + public bool UpdateCheckEnabled + { + get => _settings.UpdateCheckIn(_policy); + set + { + if (IsUpdateCheckManaged || value == _settings.UpdateCheckEnabled) return; + _settings.UpdateCheckEnabled = value; + SaveSettings(); + OnPropertyChanged(); + if (value) _ = CheckForUpdatesAsync(); + else UpdateNotice = null; + } + } + + /// True when the organization decides about the update check. + public bool IsUpdateCheckManaged => _policy.IsUpdateCheckManaged; + + /// True when the user may switch the update check on or off. + public bool CanChangeUpdateCheck => _svc.Updates is not null && !IsUpdateCheckManaged; + + /// The "newer version available" notice; null while there is nothing to say. + [ObservableProperty] + private UpdateNoticeViewModel? _updateNotice; + + /// + /// Runs the update check when it is switched on and the last one is a day old or more. The time is recorded before asking, + /// so a computer without internet access tries once a day, not at every start. Failures stay silent. The notice is not shown + /// again for a version whose notice the user closed. + /// + public async Task CheckForUpdatesAsync(CancellationToken cancellationToken = default) + { + if (_svc.Updates is not { } checker) return; + var now = (_svc.Clock ?? TimeProvider.System).GetUtcNow(); + bool enabled = _settings.UpdateCheckIn(_policy); + if (!enabled || !UpdateSchedule.IsDue(_settings.LastUpdateCheckUtc, now)) return; + _settings.LastUpdateCheckUtc = now; + SaveSettings(); + // Due-ness was decided above (before the time was recorded), so the checker is told there was no earlier check. + var result = await checker.CheckAsync(enabled, lastCheckUtc: null, now, cancellationToken); + if (result is not { Status: UpdateCheckStatus.Available, Release: { } release }) return; + // Switched off while the answer was on its way: say nothing. + if (!_settings.UpdateCheckIn(_policy)) return; + if (string.Equals(_settings.UpdateDismissedVersion, release.Version.ToString(), StringComparison.OrdinalIgnoreCase)) return; + UpdateNotice = new UpdateNoticeViewModel(release, url => _svc.Links?.Open(url) ?? false, + version => { _settings.UpdateDismissedVersion = version; SaveSettings(); }, + text => StatusMessage = text); + } /// Menu check state for the language entries. public bool IsLanguageAuto => LanguageSetting == "auto"; @@ -388,6 +463,10 @@ partial void OnUnitChanged(LengthUnit value) public bool IsLanguageEn => LanguageSetting == "en"; /// Menu check state for Spanish. public bool IsLanguageEs => LanguageSetting == "es"; + /// Menu check state for Portuguese (Brazil). + public bool IsLanguagePtBr => LanguageSetting == "pt-BR"; + /// Menu check state for French (Canada). + public bool IsLanguageFrCa => LanguageSetting == "fr-CA"; /// Raised when the view should zoom so the whole label fits. public event EventHandler? ZoomToFitRequested; @@ -503,6 +582,21 @@ private string SizeText() /// Right side of the status bar, for example "4.00 x 3.00 in · 203 dpi · ZPL". public string StatusLabelText => LabelSummaryText + " \u00B7 ZPL"; + /// + /// Status bar badge for a label the printer turns (^POI) or mirrors (^PMY), for example "Prints upside down", or empty for a normal label. + /// The canvas always shows the label upright for editing; this badge is how the operator knows the print differs. + /// + public string PrintOrientationText + { + get + { + var doc = Session.Document; + bool inverted = doc.Orientation == LabelOrientation.Inverted; + if (!inverted && !doc.MirrorImage) return string.Empty; + return Loc.Get(inverted && doc.MirrorImage ? "Status.PrintsInvertedMirrored" : inverted ? "Status.PrintsInverted" : "Status.PrintsMirrored"); + } + } + /// Left side of the status bar, for example "5 objects · 1 selected". public string ObjectsText { @@ -603,6 +697,7 @@ private void NotifyPresentation() OnPropertyChanged(nameof(TitleBarText)); OnPropertyChanged(nameof(LabelSummaryText)); OnPropertyChanged(nameof(StatusLabelText)); + OnPropertyChanged(nameof(PrintOrientationText)); OnPropertyChanged(nameof(ObjectsText)); OnPropertyChanged(nameof(SelectionTypeTag)); OnPropertyChanged(nameof(BarcodePreviewNote)); @@ -674,6 +769,8 @@ private void OnLanguageChanged() OnPropertyChanged(nameof(IsLanguageAuto)); OnPropertyChanged(nameof(IsLanguageEn)); OnPropertyChanged(nameof(IsLanguageEs)); + OnPropertyChanged(nameof(IsLanguagePtBr)); + OnPropertyChanged(nameof(IsLanguageFrCa)); } /// The label name used in titles and prompts. @@ -782,7 +879,7 @@ public bool LoadFile(string path) try { var content = RotulaFileSerializer.LoadFile(path); - OpenDocument(content.Document, path, merge: content.Merge); + OpenDocument(content.Document, path, merge: content.Merge, sheet: content.Sheet); // The label remembers where its data came from; read it again now so the canvas shows real values, not empty fields. The // same checks as the Reload buttons apply (local drive, read-only guard, saved connection by name). DataReload = ReloadRememberedDataQuietlyAsync(path); @@ -802,8 +899,10 @@ public bool LoadFile(string path) return false; } - private void OpenDocument(LabelDocument doc, string? path, bool markDirty = false, DataMergeSettings? merge = null) + private void OpenDocument(LabelDocument doc, string? path, bool markDirty = false, DataMergeSettings? merge = null, SheetSettings? sheet = null) { + // The sheet layout belongs to the label, like its data settings. + LabelSheet = sheet; // Data settings are replaced first: the loaded data belonged to the previous label, and only the settings are kept per file. Data.Apply(merge); Session.Load(doc, markDirty); @@ -833,7 +932,7 @@ private bool SaveTo(string path) { try { - RotulaFileSerializer.Save(Session.Document, path, Data.ToSettings()); + RotulaFileSerializer.Save(Session.Document, path, Data.ToSettings(), LabelSheet); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { @@ -989,14 +1088,79 @@ private void ImportZpl() _svc.Messages.ShowInfo(Loc.Get("Dialog.ImportNotes"), string.Join(Environment.NewLine, result.Warnings)); } + /// + /// Opens the print station for a folder of labels (its station.json, or every label in it with the printer used here). The station replaces the + /// designer on the screen until the operator leaves it; the open label is not touched. + /// + [RelayCommand] + private void OpenPrintStation() + { + string? folder = _svc.Files.PickFolder(Loc.Get("Station.PickFolder")); + if (folder is null) return; + var load = Rotula.Files.Station.StationConfigLoader.Load(folder); + if (!load.CanOpen) + { + _svc.Messages.ShowError(Loc.Get("Station.Open.Failed"), StationViewModel.DescribeIssues(load.Issues)); + return; + } + var station = new StationViewModel(load.Config!, load.Warnings, + new StationServices(_svc.Printing, Variables, _svc.Messages, _settings.Print, _svc.Database)); + _svc.Dialogs.ShowPrintStation(station); + } + + /// Opens the tool that makes the exit PIN line for a print station's station.json. + [RelayCommand] + private void CreateStationPin() => _svc.Dialogs.ShowStationPinTool(new StationPinViewModel(_svc.Clipboard)); + /// Opens the Print dialog (printer, printer profile, copies, test label, save as ZPL). [RelayCommand] private void Print() { - var vm = new PrintViewModel(_svc.Printing, Printer, () => Session.Document, () => Data.CurrentData, _svc.Files, DocumentName, Variables, Data); + var vm = new PrintViewModel(_svc.Printing, Printer, () => Session.Document, () => Data.CurrentData, _svc.Files, DocumentName, Variables, Data, + ShowSheets); _svc.Dialogs.ShowPrintDialog(vm); } + /// + /// The sheet layout saved with the label for the "Label sheets" export, or null when none was chosen yet. Changing it marks the label as changed, + /// so the choice is saved with it; it is not an undo step (it changes nothing on the label itself). + /// + public SheetSettings? LabelSheet { get; private set; } + + /// Opens the "Label sheets" dialog: the label (or the ticked records) on sheets for an office printer, saved as a PDF. + [RelayCommand(CanExecute = nameof(HasLabel))] + private void ShowSheets() + { + var vm = new SheetLayoutViewModel(() => Session.Document, () => Data.CurrentData, _svc.Files, Variables, LabelSheet ?? SuggestSheet(Session.Document), + DocumentName, Data, RememberSheet); + _svc.Dialogs.ShowSheetDialog(vm); + } + + private void RememberSheet(SheetSettings settings) + { + if (settings == LabelSheet) return; + LabelSheet = settings; + Session.MarkDirty(); + } + + /// + /// The layout to start from when the label has none: the template of the label's sheet stock, else a template with the label's size, else + /// the first template. The stock library is only read here, never required. + /// + internal static SheetSettings SuggestSheet(LabelDocument doc) + { + SheetTemplate? template = null; + if (doc.StockId is { Length: > 0 } id) + { + try { template = SheetTemplates.FindForStock(StockLibrary.Current.Catalog.Find(id)); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException) { /* no library: fall back to the size */ } + } + double mm(int dots) => dots * SheetLayout.MmPerInch / (int)doc.Dpi; + template ??= SheetTemplates.FindForLabelSize(mm(doc.WidthDots), mm(doc.HeightDots)); + template ??= SheetTemplates.All[0]; + return new SheetSettings { TemplateId = template.Id, Layout = template.Layout }; + } + /// Opens the Data merge window. [RelayCommand(CanExecute = nameof(HasLabel))] private void ShowData() => _svc.Dialogs.ShowDataWindow(Data); @@ -1258,11 +1422,15 @@ private bool ChooseImageFor(string elementId) [RelayCommand] private void ToggleGrid() => GridVisible = !GridVisible; - /// Switch the language: "auto", "en" or "es". The change is live; no restart is needed. - [RelayCommand] + /// + /// Switch the language: "auto" or one of ; anything else means "auto". The change is + /// live; no restart is needed. Disabled when a policy fixes the language. + /// + [RelayCommand(CanExecute = nameof(CanChooseLanguage))] private void SetLanguage(string code) { - _settings.Language = code is "en" or "es" ? code : "auto"; + if (IsLanguageManaged) return; + _settings.Language = Loc.SupportedLanguages.FirstOrDefault(l => string.Equals(l, code, StringComparison.OrdinalIgnoreCase)) ?? "auto"; Loc.Instance.SetLanguage(_settings.Language); // SetLanguage does nothing when the resolved language is unchanged (auto -> en on an English PC), but the menu // check marks still have to move. diff --git a/src/Rotula.App/ViewModels/PreviewViewModel.cs b/src/Rotula.App/ViewModels/PreviewViewModel.cs new file mode 100644 index 0000000..a5aaa8f --- /dev/null +++ b/src/Rotula.App/ViewModels/PreviewViewModel.cs @@ -0,0 +1,78 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Rotula.App.Editing; +using Rotula.App.Localization; +using Rotula.Core.Model; +using Rotula.Rendering; + +namespace Rotula.App.ViewModels; + +/// +/// The preview window: draws the label as the printer puts it on the media ("as printed", turned for ^POI and mirrored for ^PMY) or +/// the way it is edited on the canvas ("as designed"), and says in words which one is shown so an upside-down picture is never a surprise. +/// +public sealed partial class PreviewViewModel : ObservableObject +{ + private readonly PreviewCache _preview; + + /// Creates the view model for the label held by . + /// The preview cache of the open label (its current record data is used; its print orientation (^PO) and mirror (^PM) settings decide the view). + public PreviewViewModel(PreviewCache preview) + { + ArgumentNullException.ThrowIfNull(preview); + _preview = preview; + // The preview is a snapshot of the label when the window opens, so the settings are read once. + _inverted = preview.Document.Orientation == LabelOrientation.Inverted; + _mirrored = preview.Document.MirrorImage; + ChangesWhenPrinted = _inverted || _mirrored; + // The preview's job is to show what the printer draws, so it opens on "as printed". + _asPrinted = true; + } + + private readonly bool _inverted; + private readonly bool _mirrored; + + /// Raised when the picture must be drawn again (the view switched between "as printed" and "as designed"). + public event EventHandler? PictureChanged; + + /// + /// True when the printer turns or mirrors this label, so "as printed" and "as designed" differ and the switch is shown. + /// False for a normal label, where both views are the same picture. + /// + public bool ChangesWhenPrinted { get; } + + /// True to show the label as it comes out of the printer; false to show it as designed. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(AsDesigned), nameof(ModeNote))] + private bool _asPrinted; + + /// + /// The opposite of , for the second option of the switch. Only a write of true acts: a radio button that + /// is unchecked by its sibling writes false back, and reacting to that would flip the choice again. + /// + public bool AsDesigned + { + get => !AsPrinted; + set { if (value) AsPrinted = false; } + } + + /// + /// One sentence on which view is shown and why it looks the way it does, or empty for a label the printer does not turn or mirror. + /// + public string ModeNote + { + get + { + if (!ChangesWhenPrinted) return string.Empty; + if (!AsPrinted) return Loc.Get("Preview.Note.Designed"); + return Loc.Get(_inverted && _mirrored ? "Preview.Note.PrintedBoth" : _inverted ? "Preview.Note.PrintedInverted" : "Preview.Note.PrintedMirrored"); + } + } + + /// + /// Draws the label at one pixel per dot in the chosen view. The caller owns and must dispose the result. Returns null and sets + /// when the label cannot be drawn. + /// + public RenderResult? Render(out string? errorKey) => _preview.RenderPrinted(1.0, out errorKey, asPrinted: AsPrinted); + + partial void OnAsPrintedChanged(bool value) => PictureChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/src/Rotula.App/ViewModels/PrintMessages.cs b/src/Rotula.App/ViewModels/PrintMessages.cs index f055c74..c5afb5c 100644 --- a/src/Rotula.App/ViewModels/PrintMessages.cs +++ b/src/Rotula.App/ViewModels/PrintMessages.cs @@ -21,6 +21,14 @@ public static string Describe(PrintError error) // The detail differs per transport (queue, code, host...), so it is joined into one argument. args = new object[] { string.Join(", ", error.Args.Where(a => a.Length > 0)) }; break; + case PrintErrorKind.HeadOpen or PrintErrorKind.PaperOut or PrintErrorKind.RibbonOut or PrintErrorKind.HeadTooHot + or PrintErrorKind.DiagnosticMode or PrintErrorKind.BufferFull or PrintErrorKind.Paused: + { + // Arguments: the printer, then every condition it reported. The message names the worst one; the others follow. + string main = Loc.Get(error.Key, error.Args.Count > 0 ? error.Args[0] : string.Empty, string.Empty); + var others = error.Args.Skip(1).Where(c => !string.Equals(c, error.Kind.ToString(), StringComparison.Ordinal)).ToList(); + return others.Count == 0 ? main : main + " " + Loc.Get("Print.Status.AlsoReports", Conditions(others)); + } } // Messages may have up to two holes; pad so that a short argument list never throws. if (args.Length < 2) args = args.Concat(Enumerable.Repeat((object)string.Empty, 2 - args.Length)).ToArray(); @@ -35,7 +43,55 @@ public static string Describe(PrintError error) /// empty when there are none. /// public static string DescribeNotices(PrintResult result) => - string.Join(Environment.NewLine, result.Notices.Select(Loc.FormatIssue)); + string.Join(Environment.NewLine, result.Notices.Select(DescribeNotice)); + + /// + /// One note of a print result. The printer status notes carry condition and reason names, which are translated here into a phrase; + /// every other note goes through the usual issue text. + /// + public static string DescribeNotice(ValidationIssue notice) + { + ArgumentNullException.ThrowIfNull(notice); + string? arg = notice.MessageKey switch + { + PrinterStatusKeys.Warning or PrinterStatusKeys.FaultAfterJob => Conditions(notice.Args), + PrinterStatusKeys.NotChecked or PrinterStatusKeys.NotVerified => Reason(notice.Args.FirstOrDefault()), + PrinterStatusKeys.Verified => string.Empty, + _ => null, + }; + return arg is null ? Loc.FormatIssue(notice) : Loc.Get("Issue." + notice.MessageKey, arg); + } + + /// True when a print result says the printer reported a fault after the job (the result line is then shown as a problem). + public static bool HasFaultAfterJob(PrintResult result) => result.Notices.Any(n => n.MessageKey == PrinterStatusKeys.FaultAfterJob); + + /// + /// Describes a decoded printer status for the "Check printer status" button: ready, or what stops it, plus what it is doing. Returns + /// whether the status is a problem (blocked, or unknown) so the view can colour it. + /// + public static (string Text, bool IsProblem) DescribeStatus(PrinterStatus status) + { + ArgumentNullException.ThrowIfNull(status); + if (!status.IsAvailable) return (Loc.Get("Print.Status.Unavailable", Reason(status.Reason!.Value.ToString())), true); + var lines = new List(); + lines.Add(status.IsBlocked ? Loc.Get("Print.Status.Blocked", Conditions(status.Blocking.Select(c => c.ToString()))) + : Loc.Get("Print.Status.Ready")); + if (status.Warnings.Count > 0) lines.Add(Loc.Get("Print.Status.Warnings", Conditions(status.Warnings.Select(c => c.ToString())))); + if (status.LabelsRemaining is > 0 || status.FormatsInBuffer is > 0) + lines.Add(Loc.Get("Print.Status.Busy", status.LabelsRemaining ?? 0, status.FormatsInBuffer ?? 0)); + if (status.PrintMode is { } mode && status.ThermalTransfer is { } transfer && status.LabelLengthDots is { } length) + lines.Add(Loc.Get("Print.Status.Details", Loc.Get("Enum.PrinterPrintMode." + mode), + Loc.Get(transfer ? "Print.Status.ThermalTransfer" : "Print.Status.DirectThermal"), length)); + if (!status.IsComplete) lines.Add(Loc.Get("Print.Status.Partial")); + return (string.Join(Environment.NewLine, lines), status.IsBlocked); + } + + /// Joins condition names as a phrase ("out of labels, paused"); a name this version does not know is shown as it is. + private static string Conditions(IEnumerable names) => + string.Join(", ", names.Select(n => Loc.Instance.HasKey("Enum.PrinterCondition." + n) ? Loc.Get("Enum.PrinterCondition." + n) : n)); + + private static string Reason(string? name) => + name is not null && Loc.Instance.HasKey("Print.Status.Reason." + name) ? Loc.Get("Print.Status.Reason." + name) : Loc.Get("Print.Status.Reason.Unknown"); /// Lists the error-level problems of a label as short sentences (at most ). public static string DescribeIssues(IEnumerable issues, int max = 6) => diff --git a/src/Rotula.App/ViewModels/PrintViewModel.cs b/src/Rotula.App/ViewModels/PrintViewModel.cs index f3e4ea7..52960d6 100644 --- a/src/Rotula.App/ViewModels/PrintViewModel.cs +++ b/src/Rotula.App/ViewModels/PrintViewModel.cs @@ -26,6 +26,7 @@ public sealed partial class PrintViewModel : OperationViewModel private readonly IFileDialogService _files; private readonly string _documentName; private readonly IVariableServices _variables; + private readonly Action? _showSheets; /// Creates the dialog's view-model. /// Printing service (a fake in tests; tests never reach a real printer). @@ -36,10 +37,12 @@ public sealed partial class PrintViewModel : OperationViewModel /// File name stem suggested for the ZPL file. /// Counter memory and the print-time questions; null means no counter memory and no questions (tests). /// The data merge view-model that holds the loaded records and their ticks; null (or no records) keeps the single-label dialog. + /// Opens the "Label sheets" dialog (office printer sheets as a PDF); null hides the button. public PrintViewModel(IPrintService service, PrinterSettingsViewModel printer, Func document, Func?> data, IFileDialogService files, string documentName = "label", IVariableServices? variables = null, - DataMergeViewModel? records = null) + DataMergeViewModel? records = null, Action? showSheets = null) { + _showSheets = showSheets; Records = records; _variables = variables ?? new VariableServices(null, null); _service = service; @@ -229,8 +232,9 @@ private async Task SendAsync(string busyText, FuncTrue when the "Label sheets" button is offered. + public bool CanShowSheets => _showSheets is not null; + + /// Opens the "Label sheets" dialog, for labels printed on sheets by an office printer instead of a label printer. + [RelayCommand] + private void ShowSheets() + { + if (IsBusy) return; + _showSheets?.Invoke(); + } + /// Closes the dialog (stopping any running job first). [RelayCommand] private void Close() diff --git a/src/Rotula.App/ViewModels/PrinterSettingsViewModel.cs b/src/Rotula.App/ViewModels/PrinterSettingsViewModel.cs index 31a33c5..b36e827 100644 --- a/src/Rotula.App/ViewModels/PrinterSettingsViewModel.cs +++ b/src/Rotula.App/ViewModels/PrinterSettingsViewModel.cs @@ -36,6 +36,7 @@ public sealed partial class PrinterSettingsViewModel : ObservableObject /// The resolution the driver of the selected printer reported, or null when nothing was read (not asked yet, failed, or a network printer). public PrinterDpi? DetectedDpi => _detected.Dpi; private IReadOnlyList _printers = Array.Empty(); + private readonly string? _defaultPrinter; /// Creates the view-model. /// Printing service (used to list printers). @@ -44,9 +45,14 @@ public sealed partial class PrinterSettingsViewModel : ObservableObject /// The open label; its size and resolution seed the profile the first time. /// Reads the driver resolution and paper size of a printer; null uses (tests pass a fake). /// Switches the open label to a resolution (one undo step); null disables the "Switch label" action. + /// + /// A Windows printer chosen by the organization's policy, preselected until the user has picked a printer once; null for none. + /// public PrinterSettingsViewModel(IPrintService service, PrintSettings settings, Action save, Func document, - Func>? detect = null, Action? applyLabelDpi = null) + Func>? detect = null, Action? applyLabelDpi = null, + string? defaultPrinter = null) { + _defaultPrinter = string.IsNullOrWhiteSpace(defaultPrinter) ? null : defaultPrinter.Trim(); _detect = detect ?? ((name, ct) => PrinterDetector.DetectAsync(service, name, ct)); _applyLabelDpi = applyLabelDpi; _service = service; @@ -92,6 +98,8 @@ public PrinterSettingsViewModel(IPrintService service, PrintSettings settings, A partial void OnUseNetworkChanged(bool value) { OnPropertyChanged(nameof(UseWindowsPrinter)); + OnPropertyChanged(nameof(CheckStatus)); + ClearStatus(); NotifyToolbar(); RefreshDpiMismatch(); } @@ -102,11 +110,13 @@ partial void OnUseNetworkChanged(bool value) partial void OnSelectedPrinterNameChanged(string? value) { NotifyToolbar(); + ClearStatus(); _ = DetectAsync(); } partial void OnHostChanged(string value) { NotifyToolbar(); + ClearStatus(); RefreshDpiMismatch(); } partial void OnPortTextChanged(string value) => NotifyToolbar(); @@ -181,6 +191,94 @@ public bool UseWindowsPrinter [ObservableProperty] private string _printersNote = string.Empty; + // ------------------------------------------------------------------------------- printer status (~HS) + + private bool _statusCheckNetwork = true; + private bool _statusCheckWindows; + private CancellationTokenSource? _statusCts; + + /// + /// True to ask the printer for its status before and after printing. Kept separately for network and Windows printers: on by default + /// for a network printer, off for a Windows printer (many USB drivers cannot answer). + /// + public bool CheckStatus + { + get => UseNetwork ? _statusCheckNetwork : _statusCheckWindows; + set + { + if (CheckStatus == value) return; + if (UseNetwork) _statusCheckNetwork = value; else _statusCheckWindows = value; + OnPropertyChanged(); + } + } + + /// True while "Check printer status" waits for the answer. + [ObservableProperty] + private bool _isCheckingStatus; + + /// The decoded status in plain language, or empty before the first check. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasStatusText), nameof(HasStatusOk), nameof(HasStatusProblem))] + private string _statusText = string.Empty; + + /// True when reports a problem (the printer cannot print, or its status is unknown). + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasStatusOk), nameof(HasStatusProblem))] + private bool _statusIsProblem; + + /// True when there is a status result to show. + public bool HasStatusText => StatusText.Length > 0; + + /// True when the status result is good news (drives the green note). + public bool HasStatusOk => HasStatusText && !StatusIsProblem; + + /// True when the status result is a problem (drives the amber note). + public bool HasStatusProblem => HasStatusText && StatusIsProblem; + + /// Asks the chosen printer for its status (~HS) and shows the answer. A second click while waiting starts over. + [RelayCommand] + private async Task CheckPrinterStatusAsync() + { + string? error = TryBuildTarget(out var target); + if (error is not null) + { + StatusIsProblem = true; + StatusText = error; + return; + } + var previous = _statusCts; + previous?.Cancel(); + var cts = _statusCts = new CancellationTokenSource(); + IsCheckingStatus = true; + StatusText = string.Empty; + try + { + var status = await _service.GetStatusAsync(target!, cts.Token); + if (cts.IsCancellationRequested) return; + (string text, bool problem) = PrintMessages.DescribeStatus(status); + StatusIsProblem = problem; + StatusText = text; + } + catch (OperationCanceledException) { } + finally + { + if (ReferenceEquals(_statusCts, cts)) + { + _statusCts = null; + IsCheckingStatus = false; + } + cts.Dispose(); + } + } + + /// Forgets a shown status when another printer is chosen, so an old answer is never read as the new printer's. + private void ClearStatus() + { + _statusCts?.Cancel(); + StatusText = string.Empty; + StatusIsProblem = false; + } + // ------------------------------------------------------------------------------- driver detection /// True while the driver is being read (the view shows a small progress bar). @@ -369,7 +467,7 @@ private static string FormatMeasure(Measure w, Measure h) return $"{Math.Round(w.Value, 2).ToString("0.##", CultureInfo.CurrentCulture)} x {Math.Round(h.Value, 2).ToString("0.##", CultureInfo.CurrentCulture)} {unit}"; } - /// Short description of the chosen printer for summaries, for example "Zebra ZD420" or "192.168.1.50:9100". + /// Short description of the chosen printer for summaries, for example a Windows printer name or "192.168.1.50:9100". public string Summary => UseNetwork ? $"{Host}:{PortText}" : (SelectedPrinterName ?? string.Empty); private void LoadFromSettings() @@ -383,6 +481,8 @@ private void LoadFromSettings() WidthText = FormatMm(ToMm(doc.Width, doc.Dpi)); HeightText = FormatMm(ToMm(doc.Height, doc.Dpi)); SelectedMedia = doc.MediaTracking; + LoadStatusChoice(); + if (_defaultPrinter is not null) SelectedPrinterName = _defaultPrinter; return; } UseNetwork = string.Equals(s.Transport, "Tcp", StringComparison.OrdinalIgnoreCase); @@ -395,6 +495,14 @@ private void LoadFromSettings() SelectedMedia = Enum.TryParse(s.MediaTracking, out var m) ? m : MediaTracking.Web; DarknessText = s.Darkness?.ToString(CultureInfo.InvariantCulture) ?? string.Empty; SpeedText = s.SpeedIps?.ToString(CultureInfo.InvariantCulture) ?? string.Empty; + LoadStatusChoice(); + } + + private void LoadStatusChoice() + { + _statusCheckNetwork = _settings.StatusCheckNetwork; + _statusCheckWindows = _settings.StatusCheckWindows; + OnPropertyChanged(nameof(CheckStatus)); } private static double ToMm(Measure m, PrinterDpi dpi) => UnitConverter.Convert(m.Value, m.Unit, LengthUnit.Millimeters, (int)dpi); @@ -424,7 +532,8 @@ public async Task RefreshPrintersAsync(CancellationToken cancellationToken = def } if (SelectedPrinterName is { } saved && !found.Any(p => p.Name == saved)) { - PrintersNote = Loc.Get("Print.Printers.Missing", saved); + // The organization's default printer is only a suggestion: when this computer does not have it, fall back quietly. + if (!(saved == _defaultPrinter && !_settings.Configured)) PrintersNote = Loc.Get("Print.Printers.Missing", saved); SelectedPrinterName = null; } if (SelectedPrinterName is null && !UseNetwork) @@ -463,14 +572,16 @@ private static bool TryParseDouble(string text, out double value) => if (string.IsNullOrWhiteSpace(Host)) return Loc.Get("Print.Field.NoHost"); if (!int.TryParse(PortText.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out int port) || port is < 1 or > 65535) return Loc.Get("Print.Field.Port"); - target = PrinterTarget.TcpIp(Host.Trim(), port); + target = PrinterTarget.TcpIp(Host.Trim(), port).WithStatus(StatusOptions); return null; } if (string.IsNullOrWhiteSpace(SelectedPrinterName)) return Loc.Get("Print.Field.NoPrinter"); - target = PrinterTarget.Spooler(SelectedPrinterName); + target = PrinterTarget.Spooler(SelectedPrinterName).WithStatus(StatusOptions); return null; } + private PrinterStatusOptions StatusOptions => CheckStatus ? PrinterStatusOptions.Default : PrinterStatusOptions.Off; + /// Builds and validates the printer profile from the fields. /// The profile when valid. /// Character set used by the test label. @@ -523,6 +634,8 @@ public void Persist() _settings.MediaTracking = SelectedMedia.ToString(); _settings.Darkness = int.TryParse(DarknessText.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out int d) ? d : null; _settings.SpeedIps = int.TryParse(SpeedText.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out int sp) ? sp : null; + _settings.StatusCheckNetwork = _statusCheckNetwork; + _settings.StatusCheckWindows = _statusCheckWindows; _save(); } } diff --git a/src/Rotula.App/ViewModels/SheetLayoutViewModel.cs b/src/Rotula.App/ViewModels/SheetLayoutViewModel.cs new file mode 100644 index 0000000..e0bc9d9 --- /dev/null +++ b/src/Rotula.App/ViewModels/SheetLayoutViewModel.cs @@ -0,0 +1,565 @@ +using System.Collections.ObjectModel; +using System.Globalization; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Rotula.App.Editing; +using Rotula.App.Localization; +using Rotula.App.Services; +using Rotula.Core.Model; +using Rotula.Core.Sheets; +using Rotula.Core.Validation; +using Rotula.Printing; +using Rotula.Rendering.Export; +using Rotula.Rendering.Input; + +namespace Rotula.App.ViewModels; + +/// One entry of the template list: a built-in template, or null for "Custom". +/// The template, or null for a layout the user types. +/// The name shown, built from the dimensions (no brand names). +public sealed record SheetTemplateChoice(SheetTemplate? Template, string Name) +{ + /// + public override string ToString() => Name; +} + +/// One label position in the sheet preview, in preview pixels. +/// 1-based position on the sheet. +/// Left edge in the preview. +/// Top edge in the preview. +/// Width in the preview. +/// Height in the preview. +/// True for a position before the start position (already used on this sheet). +/// True for a position that gets a label on the first sheet. +/// The number of the label that goes there, or empty. +/// Tooltip and accessible name. +public sealed record SheetSlotItem(int Position, double Left, double Top, double Width, double Height, bool IsUsed, bool IsFilled, string Caption, string Tip); + +/// +/// The "Label sheets" dialog: choose a sheet for an office printer (a built-in template or a custom layout), see where the labels go, start at a +/// later position on a partly used sheet, and save the labels as a PDF at the sheet's real size. With data loaded the ticked records are used, +/// through the same path as printing; otherwise the label is repeated for the copies, each copy with its own serial number when it has a counter. +/// +/// +/// The PDF is printed from any PDF reader at actual size. Sending the sheets straight to a Windows printer is not offered: a driver's own margins +/// and "fit to page" scaling would move the labels, which a PDF printed at 100 % avoids. +/// +public sealed partial class SheetLayoutViewModel : OperationViewModel +{ + /// Largest size of the preview page, in pixels. + internal const double PreviewMaxWidth = 300, PreviewMaxHeight = 380; + + private readonly Func _document; + private readonly Func?> _data; + private readonly IFileDialogService _files; + private readonly IVariableServices _variables; + private readonly string _documentName; + private readonly Action? _remember; + private bool _applying; + + /// Creates the dialog's view-model. + /// The open label. + /// The record shown on the canvas (used when no records are loaded), or null. + /// File dialogs (where to save the PDF). + /// Counter memory, the print-time questions and the Windows-font drawing. + /// The layout to start from (the one saved with the label, or a suggestion). + /// File name stem suggested for the PDF. + /// The data merge view-model with the loaded records and their ticks; null (or no records) uses the label's copies. + /// Receives the layout when a PDF is saved or the dialog closes, so it is saved with the label. + public SheetLayoutViewModel(Func document, Func?> data, IFileDialogService files, + IVariableServices variables, SheetSettings initial, string documentName = "label", DataMergeViewModel? records = null, + Action? remember = null) + { + _document = document; + _data = data; + _files = files; + _variables = variables; + _documentName = documentName; + _remember = remember; + Records = records; + Templates = new[] { new SheetTemplateChoice(null, Loc.Get("Print.Sheet.Custom")) } + .Concat(SheetTemplates.All.Select(t => new SheetTemplateChoice(t, NameOf(t)))).ToList(); + PageKinds = Enum.GetValues().Select(k => new Choice(k, "Enum.SheetPageKind." + k)).ToList(); + Orders = Enum.GetValues().Select(k => new Choice(k, "Enum.SheetOrder." + k)).ToList(); + GuideChoices = Enum.GetValues().Select(k => new Choice(k, "Enum.SheetGuides." + k)).ToList(); + _copiesText = Math.Max(1, document().Print.Quantity).ToString(CultureInfo.InvariantCulture); + var template = SheetTemplates.Find(initial.TemplateId); + _useInches = template?.Inches ?? initial.Layout.PageKind is SheetPageKind.Letter or SheetPageKind.Legal; + _selectedOrder = Orders.First(o => o.Value == initial.Order); + _selectedGuides = GuideChoices.First(g => g.Value == initial.Guides); + _selectedTemplate = Templates.FirstOrDefault(t => t.Template is not null && t.Template.Id == template?.Id) ?? Templates[0]; + _selectedPage = PageKinds.First(p => p.Value == initial.Layout.PageKind); + ShowLayout(initial.Layout); + Refresh(); + } + + // ------------------------------------------------------------------------------------ choices + + /// "Custom" followed by the built-in templates. + public IReadOnlyList Templates { get; } + + /// Letter, A4, Legal, Custom. + public IReadOnlyList> PageKinds { get; } + + /// The fill orders. + public IReadOnlyList> Orders { get; } + + /// None, crop marks, outlines. + public IReadOnlyList> GuideChoices { get; } + + /// The loaded records and their ticks, or null. + public DataMergeViewModel? Records { get; } + + /// True when the ticked records are written (data is loaded). + public bool UsesRecords => Records is { HasRecords: true }; + + /// The chosen template ("Custom" when the user typed the layout). + [ObservableProperty] + private SheetTemplateChoice _selectedTemplate; + + partial void OnSelectedTemplateChanged(SheetTemplateChoice value) + { + if (_applying || value.Template is null) { OnPropertyChanged(nameof(IsCustom)); return; } + _applying = true; + try + { + UseInches = value.Template.Inches; + SelectedPage = PageKinds.First(p => p.Value == value.Template.Layout.PageKind); + ShowLayout(value.Template.Layout); + } + finally { _applying = false; } + OnPropertyChanged(nameof(IsCustom)); + Refresh(); + } + + /// True when the layout is the user's own (no template). + public bool IsCustom => SelectedTemplate.Template is null; + + /// The paper size. + [ObservableProperty] + private Choice _selectedPage; + + partial void OnSelectedPageChanged(Choice value) + { + OnPropertyChanged(nameof(IsCustomPage)); + if (SheetLayout.PageSize(value.Value) is { } size) + { + bool was = _applying; + _applying = true; + PageWidthText = Format(size.WidthMm); + PageHeightText = Format(size.HeightMm); + _applying = was; + } + FieldEdited(); + } + + /// True when the paper size is typed (the width and height boxes are enabled). + public bool IsCustomPage => SelectedPage.Value == SheetPageKind.Custom; + + /// The fill order. + [ObservableProperty] + private Choice _selectedOrder; + + partial void OnSelectedOrderChanged(Choice value) => Refresh(); + + /// The guides drawn on the sheet. + [ObservableProperty] + private Choice _selectedGuides; + + /// True when sizes are shown and typed in inches, false for millimetres. + [ObservableProperty] + private bool _useInches; + + partial void OnUseInchesChanged(bool value) + { + // The numbers are shown again in the new unit; the layout itself does not change. + if (TryBuildLayout(!value, out var layout, out _)) ShowLayout(layout); + OnPropertyChanged(nameof(UnitText)); + } + + /// "mm" or "in", shown next to the size boxes. + public string UnitText => UseInches ? Loc.Get("Print.Sheet.Unit.In") : Loc.Get("Print.Sheet.Unit.Mm"); + + // ------------------------------------------------------------------------------------ the numbers as typed + + /// Paper width. + [ObservableProperty] private string _pageWidthText = string.Empty; + /// Paper height. + [ObservableProperty] private string _pageHeightText = string.Empty; + /// Top margin. + [ObservableProperty] private string _topText = string.Empty; + /// Left margin. + [ObservableProperty] private string _leftText = string.Empty; + /// Label width. + [ObservableProperty] private string _labelWidthText = string.Empty; + /// Label height. + [ObservableProperty] private string _labelHeightText = string.Empty; + /// Labels across. + [ObservableProperty] private string _columnsText = string.Empty; + /// Labels down. + [ObservableProperty] private string _rowsText = string.Empty; + /// Distance between the left edges of two columns. + [ObservableProperty] private string _horizontalPitchText = string.Empty; + /// Distance between the top edges of two rows. + [ObservableProperty] private string _verticalPitchText = string.Empty; + + partial void OnPageWidthTextChanged(string value) => FieldEdited(); + partial void OnPageHeightTextChanged(string value) => FieldEdited(); + partial void OnTopTextChanged(string value) => FieldEdited(); + partial void OnLeftTextChanged(string value) => FieldEdited(); + partial void OnLabelWidthTextChanged(string value) => FieldEdited(); + partial void OnLabelHeightTextChanged(string value) => FieldEdited(); + partial void OnColumnsTextChanged(string value) => FieldEdited(); + partial void OnRowsTextChanged(string value) => FieldEdited(); + partial void OnHorizontalPitchTextChanged(string value) => FieldEdited(); + partial void OnVerticalPitchTextChanged(string value) => FieldEdited(); + + /// 1-based position on the first sheet where the first label goes. + [ObservableProperty] + private string _startPositionText = "1"; + + partial void OnStartPositionTextChanged(string value) => Refresh(); + + /// Copies of the label (or of each record when the Data window does not set them). + [ObservableProperty] + private string _copiesText; + + partial void OnCopiesTextChanged(string value) => Refresh(); + + // ------------------------------------------------------------------------------------ what the dialog shows + + /// The positions of the sheet for the preview. + public ObservableCollection Slots { get; } = new(); + + /// Width of the preview page in pixels. + [ObservableProperty] private double _previewWidth = PreviewMaxWidth; + + /// Height of the preview page in pixels. + [ObservableProperty] private double _previewHeight = PreviewMaxHeight; + + /// Problems and notes of the layout, one per line (empty when there is nothing to say). + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasProblems))] + private string _problemsText = string.Empty; + + /// True when has an error (the export is blocked). + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanExport))] + private bool _hasErrors; + + /// True when there is a problem or a note to show. + public bool HasProblems => ProblemsText.Length > 0; + + /// "12 labels on 2 sheets" or what is known about the count. + [ObservableProperty] + private string _summaryText = string.Empty; + + /// True when the PDF can be made: the layout and numbers are fine and something is selected. + public bool CanExport => !HasErrors; + + /// The layout as it would be saved with the label, or null while the numbers have an error. + public SheetSettings? Settings => + TryBuildLayout(UseInches, out var layout, out _) && layout.IsValid + ? new SheetSettings { TemplateId = SelectedTemplate.Template?.Id, Layout = layout, Order = SelectedOrder.Value, Guides = SelectedGuides.Value } + : null; + + // ------------------------------------------------------------------------------------ editing + + /// Makes the clicked position the start position. + [RelayCommand] + private void PickStart(SheetSlotItem? slot) + { + if (slot is null) return; + StartPositionText = slot.Position.ToString(CultureInfo.InvariantCulture); + } + + private void FieldEdited() + { + if (_applying) return; + // A typed number that no longer matches the template makes the layout the user's own. + if (SelectedTemplate.Template is { } t && (!TryBuildLayout(UseInches, out var layout, out _) || !Same(layout, t.Layout))) + { + _applying = true; + SelectedTemplate = Templates[0]; + _applying = false; + OnPropertyChanged(nameof(IsCustom)); + } + Refresh(); + } + + private static bool Same(SheetLayout a, SheetLayout b) + { + static bool Eq(double x, double y) => Math.Abs(x - y) < 0.01; + return a.PageKind == b.PageKind && a.Columns == b.Columns && a.Rows == b.Rows && Eq(a.PageWidthMm, b.PageWidthMm) && Eq(a.PageHeightMm, b.PageHeightMm) + && Eq(a.TopMarginMm, b.TopMarginMm) && Eq(a.LeftMarginMm, b.LeftMarginMm) && Eq(a.LabelWidthMm, b.LabelWidthMm) && Eq(a.LabelHeightMm, b.LabelHeightMm) + && Eq(a.HorizontalPitchMm, b.HorizontalPitchMm) && Eq(a.VerticalPitchMm, b.VerticalPitchMm); + } + + private void ShowLayout(SheetLayout layout) + { + bool was = _applying; + _applying = true; + try + { + PageWidthText = Format(layout.PageWidthMm); + PageHeightText = Format(layout.PageHeightMm); + TopText = Format(layout.TopMarginMm); + LeftText = Format(layout.LeftMarginMm); + LabelWidthText = Format(layout.LabelWidthMm); + LabelHeightText = Format(layout.LabelHeightMm); + ColumnsText = layout.Columns.ToString(CultureInfo.InvariantCulture); + RowsText = layout.Rows.ToString(CultureInfo.InvariantCulture); + HorizontalPitchText = Format(layout.HorizontalPitchMm); + VerticalPitchText = Format(layout.VerticalPitchMm); + } + finally { _applying = was; } + } + + private string Format(double mm) => UseInches + ? Math.Round(mm / SheetLayout.MmPerInch, 4).ToString("0.####", CultureInfo.InvariantCulture) + : Math.Round(mm, 2).ToString("0.##", CultureInfo.InvariantCulture); + + /// Reads the typed numbers (a comma is accepted as the decimal point) into a layout; false with the first problem when one cannot be read. + private bool TryBuildLayout(bool inches, out SheetLayout layout, out string? error) + { + layout = null!; + string? firstError = null; + double scale = inches ? SheetLayout.MmPerInch : 1; + bool Num(string text, string fieldKey, out double mm) + { + mm = 0; + if (double.TryParse(text.Trim().Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out double v) && double.IsFinite(v)) + { + mm = v * scale; + return true; + } + firstError ??= Loc.Get("Print.Sheet.Field.Invalid", Loc.Get(fieldKey)); + return false; + } + bool Int(string text, string fieldKey, out int n) + { + if (int.TryParse(text.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out n)) return true; + firstError ??= Loc.Get("Print.Sheet.Field.Invalid", Loc.Get(fieldKey)); + return false; + } + bool ok = Num(PageWidthText, "Print.Sheet.PageWidth", out double pw) & Num(PageHeightText, "Print.Sheet.PageHeight", out double ph) + & Num(TopText, "Print.Sheet.Top", out double top) & Num(LeftText, "Print.Sheet.Left", out double left) + & Num(LabelWidthText, "Print.Sheet.LabelWidth", out double lw) & Num(LabelHeightText, "Print.Sheet.LabelHeight", out double lh) + & Int(ColumnsText, "Print.Sheet.Columns", out int cols) & Int(RowsText, "Print.Sheet.Rows", out int rows) + & Num(HorizontalPitchText, "Print.Sheet.HorizontalPitch", out double hp) & Num(VerticalPitchText, "Print.Sheet.VerticalPitch", out double vp); + error = firstError; + if (!ok) return false; + layout = new SheetLayout + { + PageKind = SelectedPage.Value, PageWidthMm = pw, PageHeightMm = ph, TopMarginMm = top, LeftMarginMm = left, + LabelWidthMm = lw, LabelHeightMm = lh, Columns = cols, Rows = rows, HorizontalPitchMm = hp, VerticalPitchMm = vp, + }; + // A named paper is its real size, whatever rounding the boxes show. + if (SelectedPage.Value != SheetPageKind.Custom) layout = layout.WithPage(SelectedPage.Value); + return true; + } + + private bool TryStart(SheetLayout layout, out int start, out string? error) + { + error = null; + if (int.TryParse(StartPositionText.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out start) && layout.ValidateStart(start) is null) return true; + error = Loc.FormatIssue(layout.ValidateStart(0)!); + return false; + } + + private bool TryCopies(out int copies) + { + return int.TryParse(CopiesText.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out copies) && copies >= 1 && copies <= DataMergeViewModel.MaxSheetLabels; + } + + /// How many labels the export makes, or null when the data decides (copies from a column). + private long? LabelCount(int copies) => UsesRecords ? Records!.EstimateSheetLabels(copies) : copies; + + /// Checks everything and rebuilds the preview and the summary. + public void Refresh() + { + var lines = new List(); + bool errors = false; + Slots.Clear(); + if (!TryBuildLayout(UseInches, out var layout, out string? readError)) + { + ProblemsText = readError!; + HasErrors = true; + SummaryText = string.Empty; + return; + } + foreach (var issue in layout.Validate()) + { + lines.Add(Loc.FormatIssue(issue)); + errors |= issue.Severity == ValidationSeverity.Error; + } + if (errors) + { + ProblemsText = string.Join(Environment.NewLine, lines); + HasErrors = true; + SummaryText = string.Empty; + return; + } + var doc = _document(); + if (layout.CheckDesignSize(Mm(doc.WidthDots, doc.Dpi), Mm(doc.HeightDots, doc.Dpi)) is { } fit) lines.Add(Loc.FormatIssue(fit)); + bool startOk = TryStart(layout, out int start, out string? startError); + if (!startOk) { lines.Add(startError!); errors = true; start = 1; } + bool copiesOk = TryCopies(out int copies); + if (!copiesOk) { lines.Add(Loc.Get("Print.Sheet.Copies.Invalid", DataMergeViewModel.MaxSheetLabels)); errors = true; copies = 1; } + if (UsesRecords && Records!.SelectedCount == 0) { lines.Add(Loc.Get("Print.Records.NoneSelected")); errors = true; } + + long? count = LabelCount(copies); + SummaryText = count is { } n + ? Loc.Get("Print.Sheet.Summary", n, SheetImposition.PageCount(layout, n, start), layout.SlotCount) + : Loc.Get("Print.Sheet.Summary.FromData", layout.SlotCount); + BuildPreview(layout, start, count, SelectedOrder.Value); + ProblemsText = string.Join(Environment.NewLine, lines); + HasErrors = errors; + } + + private static double Mm(int dots, PrinterDpi dpi) => dots * SheetLayout.MmPerInch / (int)dpi; + + private void BuildPreview(SheetLayout layout, int start, long? count, SheetOrder order) + { + double scale = Math.Min(PreviewMaxWidth / layout.PageWidthMm, PreviewMaxHeight / layout.PageHeightMm); + PreviewWidth = layout.PageWidthMm * scale; + PreviewHeight = layout.PageHeightMm * scale; + foreach (var slot in layout.Slots(order)) + { + bool used = slot.Position < start; + long number = slot.Position - start + 1; + bool filled = !used && (count is null || number <= count); + string caption = filled ? number.ToString(CultureInfo.InvariantCulture) : string.Empty; + string tip = used ? Loc.Get("Print.Sheet.Slot.Used", slot.Position) + : filled ? Loc.Get("Print.Sheet.Slot.Label", slot.Position, number) + : Loc.Get("Print.Sheet.Slot.Empty", slot.Position); + Slots.Add(new SheetSlotItem(slot.Position, slot.XMm * scale, slot.YMm * scale, slot.WidthMm * scale, slot.HeightMm * scale, used, filled, caption, tip)); + } + } + + // ------------------------------------------------------------------------------------ export + + /// Asks where to save, then writes the PDF. + [RelayCommand] + private async Task ExportPdfAsync() + { + if (IsBusy) return; + Refresh(); + if (HasErrors || !TryBuildLayout(UseInches, out var layout, out _) || !TryStart(layout, out int start, out _) || !TryCopies(out int copies)) + { + SetResult(ProblemsText, true); + return; + } + if (Records is { IsBusy: true }) return; + var options = new SheetExportOptions { StartPosition = start, Order = SelectedOrder.Value, Guides = SelectedGuides.Value }; + string? path = _files.PickExportFile(ExportKind.Pdf, _documentName + ".pdf"); + if (path is null) return; + RememberLayout(); + if (UsesRecords) + { + var outcome = await Records!.ExportSheetsCoreAsync(RunAsync, p => Progress = p, copies, layout, options, path); + if (outcome is { } o) SetResult(o.Text, o.IsError); + return; + } + await ExportLabelAsync(layout, options, copies, path); + } + + /// + /// The label without records: its copies through , exactly as the Print dialog would send them, so every copy of a + /// label with a counter carries its own number and the numbers are confirmed only when the file is complete. + /// + private async Task ExportLabelAsync(SheetLayout layout, SheetExportOptions options, int copies, string path) + { + var doc = ElementCloner.CloneDocument(_document()); + doc.Print.Quantity = copies; + var data = _data(); + var answers = _variables.Ask(Rotula.Core.Variables.Prompts.PromptSet.Build(doc, data, _variables.PromptMemory)); + if (answers is null) return; + var run = LabelPrintRun.Begin(doc, data, VariableServices.Options(_variables, answers)); + if (!run.CanPrint) + { + SetResult(Loc.Get("Print.Error.InvalidLabel") + Environment.NewLine + PrintMessages.DescribeIssues(run.Issues), true); + return; + } + SheetExportResult? result = null; + string? writeError = null; + var closing = new List(); + bool ok = await RunAsync(Loc.Get("Print.Sheet.Busy"), ct => Task.Run(() => + { + long written = 0; + try + { + IReadOnlyDictionary? lastValues = null; + RenderLabel? lastLabel = null; + IEnumerable Labels() + { + foreach (var values in run.LabelValues()) + { + ct.ThrowIfCancellationRequested(); + // Copies without a counter share one dictionary, so the design is converted once. + if (lastLabel is null || !ReferenceEquals(values, lastValues)) + { + lastLabel = LabelDocumentAdapter.ToRenderLabelResolved(doc, values); + lastValues = values; + } + yield return lastLabel; + } + } + result = SheetExporter.SavePdf(layout, Labels(), path, options); + if (result.Pages > 0 && !run.StoppedEarly) written = result.Labels; + else if (run.StoppedEarly && File.Exists(path)) File.Delete(path); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + writeError = Loc.Get("File.Error.Write", path); + } + finally + { + // Only a complete file uses up its serial numbers; anything else gives them back. + closing.AddRange(run.Complete(written)); + } + }, ct)); + if (!ok) return; + string notes = string.Join(Environment.NewLine, run.Issues.Where(i => i.Severity != ValidationSeverity.Error).Concat(closing).Select(Loc.FormatIssue)); + if (writeError is not null) { SetResult(writeError, true); return; } + if (run.StoppedEarly) + { + SetResult(Loc.Get("Print.Error.InvalidLabel") + Environment.NewLine + PrintMessages.DescribeIssues(run.Issues), true); + return; + } + if (result is null || result.Pages == 0) { SetResult(Loc.Get("Data.Batch.NothingProduced"), true); return; } + string text = Loc.Get("Print.Sheet.Done", result.Labels, result.Pages, Path.GetFileName(path)); + if (result.Issues.Count > 0) text += Environment.NewLine + string.Join(Environment.NewLine, result.Issues.Select(i => i.Message).Take(3)); + SetResult(notes.Length == 0 ? text : text + Environment.NewLine + notes, false); + } + + /// Hands a valid layout to the label, so it is saved with it. Called when the dialog closes, however it is closed. + public void RememberLayout() + { + if (Settings is { } settings) _remember?.Invoke(settings); + } + + /// Closes the dialog. + [RelayCommand] + private void Close() + { + RememberLayout(); + RequestClose(); + } + + // ------------------------------------------------------------------------------------ names + + /// The name of a template, from its dimensions: "Letter, 30 per sheet (3 x 10), 2.625 x 1 in". + internal static string NameOf(SheetTemplate t) + { + var l = t.Layout; + string size = t.Inches + ? Loc.Get("Print.Sheet.Size.In", Inch(l.LabelWidthMm), Inch(l.LabelHeightMm)) + : Loc.Get("Print.Sheet.Size.Mm", MmText(l.LabelWidthMm), MmText(l.LabelHeightMm)); + return Loc.Get("Print.Sheet.Template.Name", Loc.Get("Enum.SheetPageKind." + l.PageKind), l.SlotCount, l.Columns, l.Rows, size); + } + + private static string Inch(double mm) => Math.Round(mm / SheetLayout.MmPerInch, 3).ToString("0.###", CultureInfo.CurrentCulture); + + private static string MmText(double mm) => Math.Round(mm, 1).ToString("0.#", CultureInfo.CurrentCulture); +} diff --git a/src/Rotula.App/ViewModels/StationPinViewModel.cs b/src/Rotula.App/ViewModels/StationPinViewModel.cs new file mode 100644 index 0000000..231dbdb --- /dev/null +++ b/src/Rotula.App/ViewModels/StationPinViewModel.cs @@ -0,0 +1,68 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Rotula.App.Localization; +using Rotula.App.Services; +using Rotula.Files.Station; + +namespace Rotula.App.ViewModels; + +/// +/// The small tool that turns a new exit PIN into the line for station.json. The PIN itself is never shown or stored: only its salted hash is +/// put on the screen for the person setting up the station to copy. +/// +public sealed partial class StationPinViewModel : ObservableObject +{ + private readonly IClipboardService? _clipboard; + private readonly int _iterations; + + /// Creates the tool. + /// Puts the line on the clipboard; null copies nothing (tests). + /// PBKDF2 iterations; tests pass a smaller number. + public StationPinViewModel(IClipboardService? clipboard, int iterations = StationPin.DefaultIterations) + { + _clipboard = clipboard; + _iterations = iterations; + } + + /// The new PIN (copied from the password box; never shown). + public string Pin { get; set; } = string.Empty; + + /// The PIN typed a second time. + public string Confirm { get; set; } = string.Empty; + + /// The line to paste into station.json, once made. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasResult))] + private string _resultLine = string.Empty; + + /// True when is ready. + public bool HasResult => ResultLine.Length > 0; + + /// Why no line was made, or "Copied."; empty otherwise. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasMessage))] + private string _message = string.Empty; + + /// True when has text. + public bool HasMessage => Message.Length > 0; + + /// Makes the hash line from the two PIN boxes. + [RelayCommand] + private void Create() + { + ResultLine = string.Empty; + if (!StationPin.IsValidPin(Pin)) { Message = Loc.Get("Station.NewPin.Invalid", StationPin.MinLength, StationPin.MaxLength); return; } + if (!string.Equals(Pin.Trim(), Confirm.Trim(), StringComparison.Ordinal)) { Message = Loc.Get("Station.NewPin.Mismatch"); return; } + Message = string.Empty; + ResultLine = "\"exitPinHash\": \"" + StationPin.Hash(Pin, _iterations) + "\""; + } + + /// Copies the line to the clipboard. + [RelayCommand] + private void Copy() + { + if (!HasResult) return; + _clipboard?.SetText(ResultLine); + Message = Loc.Get("Station.NewPin.Copied"); + } +} diff --git a/src/Rotula.App/ViewModels/StationViewModel.cs b/src/Rotula.App/ViewModels/StationViewModel.cs new file mode 100644 index 0000000..ad50539 --- /dev/null +++ b/src/Rotula.App/ViewModels/StationViewModel.cs @@ -0,0 +1,843 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Rotula.App.Editing; +using Rotula.App.Localization; +using Rotula.App.Services; +using Rotula.Core.Model; +using Rotula.Core.Validation; +using Rotula.Core.Variables; +using Rotula.Core.Variables.Prompts; +using Rotula.Core.Zpl; +using Rotula.Files; +using Rotula.Files.Station; +using Rotula.Printing; + +namespace Rotula.App.ViewModels; + +/// What the big status panel of the print station says. +public enum StationStatusKind +{ + /// Nothing to report yet. + None, + /// The printer is being asked for its status. + Checking, + /// The printer answered and can print. + Ready, + /// The printer status could not be read; printing is still possible. + Unknown, + /// A job is being sent (and checked). + Printing, + /// The printer reported a fault (head open, out of labels...), so nothing was sent. + HeldBack, + /// The labels were sent; the printer was not asked afterwards. + Printed, + /// The labels were sent and the printer says it finished without a fault. + Verified, + /// The labels were sent but the printer could not confirm them. + NotVerified, + /// Nothing (or not everything) was printed: a wrong answer, a label problem, a printer fault after the job, a connection failure. + Problem, +} + +/// A picture of a label drawn by the preview renderer, as premultiplied BGRA pixels (the view turns it into a bitmap). +/// Width in pixels. +/// Height in pixels. +/// The pixels, row after row. +/// Bytes per row. +public sealed record StationImage(int Width, int Height, byte[] Pixels, int Stride) +{ + /// Draws the label of as it prints, at most on its longer side. Null when it cannot be drawn. + public static StationImage? Render(PreviewCache preview, LabelDocument doc, int maxPixels) + { + var label = preview.GetLabel(); + int longest = Math.Max(label.WidthDots, label.HeightDots); + if (longest <= 0) return null; + double scale = Math.Min(1.0, maxPixels / (double)longest); + using var result = preview.RenderPrinted(scale, out _); + if (result is null) return null; + var bmp = result.Bitmap; + return new StationImage(bmp.Width, bmp.Height, bmp.Bytes, bmp.RowBytes); + } +} + +/// What the print station needs from the application, behind interfaces so the view-model runs in tests with fakes. +/// The print service (the only way anything is printed). +/// Counter memory, remembered answers and the text rasterizer, shared with the designer. +/// Message boxes (the "Print N labels?" question of a data print). +/// The printer last used in the designer; labels without a printer of their own use it. +/// Saved database connections, for labels whose data comes from a database; null uses the real ones. +/// Reads a .rotula file; null uses . +/// Clock for the PIN lock-out; null uses the computer's. +public sealed record StationServices(IPrintService Printing, IVariableServices Variables, IMessageService Messages, PrintSettings? DesignerPrinter = null, + IDatabaseServices? Database = null, Func? LoadLabel = null, TimeProvider? Time = null); + +/// One label tile of the print station: its name, scan key, thumbnail, and why it cannot be used when it cannot. +public sealed partial class StationLabelViewModel : ObservableObject +{ + internal StationLabelViewModel(StationLabel entry) => Entry = entry; + + /// The label as the configuration describes it. + public StationLabel Entry { get; } + + /// The name on the tile. + public string Name => Entry.Name; + + /// The scan key, or empty. + public string Key => Entry.Key ?? string.Empty; + + /// True when the label has a scan key (shown on the tile). + public bool HasKey => Key.Length > 0; + + /// The design, or null when the file could not be read. + public LabelDocument? Document { get; internal set; } + + /// The data settings saved with the label, or null. + public Rotula.Files.DataMergeSettings? Merge { get; internal set; } + + /// The thumbnail, or null when the label could not be drawn. + [ObservableProperty] + private StationImage? _thumbnail; + + /// Why the label cannot be opened, in plain language; empty when it can. + public string LoadError { get; internal set; } = string.Empty; + + /// True when the label was read and can be opened. + public bool IsAvailable => Document is not null; + + /// True when the search text finds this label (by name, key or file name). + internal bool Matches(string search) => + search.Length == 0 || Name.Contains(search, StringComparison.OrdinalIgnoreCase) || Key.Contains(search, StringComparison.OrdinalIgnoreCase) + || Path.GetFileName(Entry.Path).Contains(search, StringComparison.OrdinalIgnoreCase); +} + +/// +/// The print station: a locked, print-only screen for the warehouse floor. The operator picks a label (or scans its code), answers its questions +/// (typed or scanned; Enter or Tab moves on), sets the quantity and prints. Printing goes through exactly like the Print +/// dialog, with the printer status check, and the result is shown in large plain words. Leaving the station needs the PIN when one is set. +/// +public sealed partial class StationViewModel : OperationViewModel +{ + /// Wrong PINs in a row before the PIN box locks. + public const int MaxPinAttempts = 5; + + /// How long the PIN box stays locked after too many wrong PINs. + public static readonly TimeSpan PinLockout = TimeSpan.FromSeconds(30); + + private const int ThumbnailPixels = 240; + private const int PreviewPixels = 900; + + private readonly StationConfig _config; + private readonly StationServices _services; + private readonly IPrintService _service; + private readonly StationAnswers _answers; + private readonly TimeProvider _time; + private PromptSet? _set; + private EditorSession? _session; + private PreviewCache? _previewCache; + private int _statusVersion; + private int _pinFailures; + private DateTimeOffset _lockedUntil = DateTimeOffset.MinValue; + + /// Creates the station and reads every label of the configuration (thumbnails included). + /// The checked configuration. + /// Warnings of the configuration (labels left out), shown on the label list. + /// Printing and the rest. + public StationViewModel(StationConfig config, IReadOnlyList warnings, StationServices services) + { + _config = config; + _services = services; + _service = services.Printing; + _time = services.Time ?? TimeProvider.System; + _answers = new StationAnswers(this, services.Variables); + Scanner = new ScanSequencer(config.PrintOnLastScan, config.ScanToSelect); + var notes = warnings.Select(Loc.FormatIssue).ToList(); + foreach (var entry in config.Labels) + { + var label = new StationLabelViewModel(entry); + Read(label); + if (!label.IsAvailable) notes.Add(Loc.Get("Station.Label.LoadFailed", label.Name, label.LoadError)); + Labels.Add(label); + } + WarningsText = string.Join(Environment.NewLine, notes); + RefreshVisible(); + PropertyChanged += (_, e) => { if (e.PropertyName == nameof(IsBusy)) { OnPropertyChanged(nameof(CanPrint)); OnPropertyChanged(nameof(CanPrintRecords)); } }; + } + + /// Lists configuration problems in plain language, one per line (errors first). + public static string DescribeIssues(IEnumerable issues) => + string.Join(Environment.NewLine, issues.OrderBy(i => i.Severity == ValidationSeverity.Error ? 0 : 1).Select(Loc.FormatIssue)); + + /// Opens the only label at once when the station offers exactly one; call once the window is shown. + public Task StartAsync() => Labels.Count(l => l.IsAvailable) == 1 && Labels.Count == 1 ? OpenAsync(Labels[0]) : Task.CompletedTask; + + // ------------------------------------------------------------------------------------ label list + + /// The terminator logic for the scanner (used by the window for every Enter and Tab). + public ScanSequencer Scanner { get; } + + /// Title at the top of the screen. + public string Title => _config.Title.Length > 0 ? _config.Title : Loc.Get("Station.Title"); + + /// True when the station fills the screen without a title bar. + public bool FullScreen => _config.FullScreen; + + /// Every label of the configuration. + public ObservableCollection Labels { get; } = new(); + + /// The labels the search finds. + public ObservableCollection VisibleLabels { get; } = new(); + + /// Labels left out or unreadable, one per line; empty when everything is fine. + public string WarningsText { get; } + + /// True when has something to say. + public bool HasWarnings => WarningsText.Length > 0; + + /// What is typed (or scanned) into the search box. + [ObservableProperty] + private string _searchText = string.Empty; + + /// Why the last scan on the label list opened nothing; empty otherwise. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasSearchMessage))] + private string _searchMessage = string.Empty; + + /// True when has text. + public bool HasSearchMessage => SearchMessage.Length > 0; + + /// True when the search finds nothing. + public bool NoLabelsFound => VisibleLabels.Count == 0; + + partial void OnSearchTextChanged(string value) + { + SearchMessage = string.Empty; + RefreshVisible(); + } + + private void RefreshVisible() + { + string search = ScanSequencer.Clean(SearchText); + VisibleLabels.Clear(); + foreach (var l in Labels.Where(l => l.Matches(search))) VisibleLabels.Add(l); + OnPropertyChanged(nameof(NoLabelsFound)); + } + + /// + /// Enter in the search box: a scanned key opens its label, a search that leaves one label opens it, anything else says that no label has that code. + /// + public async Task SubmitSearchAsync() + { + var hits = VisibleLabels.ToList(); + var kind = Scanner.SelectLabel(SearchText, Labels, l => l.Entry.Key, hits, out var match); + if (kind == LabelScanKind.Open && match is not null) + { + await OpenAsync(match); + return; + } + if (kind == LabelScanKind.Unknown) SearchMessage = Loc.Get("Station.UnknownCode", ScanSequencer.Clean(SearchText)); + // The code is still in the box: the next scan replaces it. + FocusSearchRequested?.Invoke(this, EventArgs.Empty); + } + + /// Opens a label tile (a click or a tap). + [RelayCommand] + private Task OpenLabel(StationLabelViewModel? label) => label is null ? Task.CompletedTask : OpenAsync(label); + + /// True when the label list can be shown again (the station offers more than one label). + public bool CanGoBack => Labels.Count > 1; + + /// Goes back to the label list. + [RelayCommand] + private void BackToList() + { + if (IsBusy || !CanGoBack) return; + CloseLabel(); + SearchText = string.Empty; + FocusSearchRequested?.Invoke(this, EventArgs.Empty); + } + + private void Read(StationLabelViewModel label) + { + try + { + var content = (_services.LoadLabel ?? RotulaFileSerializer.LoadFile)(label.Entry.Path); + label.Document = content.Document; + label.Merge = content.Merge; + var cache = new PreviewCache(new EditorSession(content.Document)); + label.Thumbnail = StationImage.Render(cache, content.Document, ThumbnailPixels); + cache.Dispose(); + } + catch (RotulaFileException ex) + { + label.LoadError = Loc.Get(ex.MessageKey, ex.Args.Cast().Prepend(Path.GetFileName(label.Entry.Path)).ToArray()); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + label.LoadError = Loc.Get("File.Error.Read", Path.GetFileName(label.Entry.Path)); + } + } + + // ------------------------------------------------------------------------------------ the open label + + /// The open label, or null while the list is shown. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsLabelOpen), nameof(IsChoosing), nameof(CanPrint))] + private StationLabelViewModel? _current; + + /// True while a label is open. + public bool IsLabelOpen => Current is not null; + + /// True while the label list is shown. + public bool IsChoosing => Current is null; + + /// The questions of the open label, in the order the label declares them. + public ObservableCollection Fields { get; } = new(); + + /// True when the open label asks no questions. + public bool HasNoFields => Fields.Count == 0; + + /// The printer of the open label (target and profile), or null when none is set up. + public PrinterSettingsViewModel? Printer { get; private set; } + + /// "Printer: ..." for the open label. + public string PrinterText => Printer is null ? Loc.Get("Station.Printer.None") : Loc.Get("Station.Printer", Printer.Summary); + + /// The quantity as typed. + [ObservableProperty] + private string _copiesText = "1"; + + /// The most copies one print of the open label may have. + public int MaxCopies => Current?.Entry.MaxCopies ?? 1; + + /// "1 to N labels" under the quantity box. + public string CopiesHint => Loc.Get("Station.Copies.Hint", MaxCopies); + + /// The live preview of the open label with the answers typed so far. + [ObservableProperty] + private StationImage? _preview; + + /// True when Print may run. + public bool CanPrint => IsIdle && Current is not null && Printer is not null; + + /// The data the label was saved with, when the station allows printing its records; null otherwise. + public DataMergeViewModel? Records { get; private set; } + + /// True when the open label's records can be printed. + public bool CanPrintRecords => IsIdle && Records is { HasRecords: true } && Printer is not null; + + /// True when the open label may print its records (whether or not they could be loaded). + public bool ShowRecords => Records is not null; + + /// "12 records from the label's data", or why they could not be read. + public string RecordsText => Records is null ? string.Empty + : Records.HasRecords ? Loc.Get("Station.Records", Records.RecordCount) + : Loc.Get("Station.Records.NotLoaded", Records.ResultText.Length > 0 ? Records.ResultText : Loc.Get("Station.Records.None")); + + /// Raised when the keyboard should go to the question at the given index. + public event EventHandler? FocusFieldRequested; + + /// Raised when the keyboard should go to the quantity and the Print button. + public event EventHandler? FocusPrintRequested; + + /// Raised when the keyboard should go to the search box. + public event EventHandler? FocusSearchRequested; + + /// Raised when an answer changed and the preview should be drawn again (the window waits a moment, then calls ). + public event EventHandler? PreviewRefreshRequested; + + /// Opens a label: its questions, quantity, printer, preview, and the printer status check. + public async Task OpenAsync(StationLabelViewModel label) + { + if (IsBusy) return; + if (!label.IsAvailable) + { + SearchMessage = Loc.Get("Station.Label.LoadFailed", label.Name, label.LoadError); + return; + } + CloseLabel(); + var doc = label.Document!; + _session = new EditorSession(doc); + _previewCache = new PreviewCache(_session); + Printer = BuildPrinter(label, doc); + _set = PromptSet.Build(doc, null, _services.Variables.PromptMemory); + foreach (var item in _set.Items) + { + var field = new PromptFieldViewModel(item); + field.PropertyChanged += OnFieldChanged; + Fields.Add(field); + } + int copies = label.Entry.DefaultCopies ?? Math.Max(1, doc.Print.Quantity); + CopiesText = Math.Clamp(copies, 1, label.Entry.MaxCopies).ToString(CultureInfo.InvariantCulture); + Current = label; + ResultText = string.Empty; + SetStatus(StationStatusKind.None, string.Empty, string.Empty); + OnPropertyChanged(nameof(HasNoFields)); + OnPropertyChanged(nameof(PrinterText)); + OnPropertyChanged(nameof(MaxCopies)); + OnPropertyChanged(nameof(CopiesHint)); + OnPropertyChanged(nameof(CanPrint)); + RenderPreview(); + RequestFirstFocus(); + if (Printer is null) SetStatus(StationStatusKind.Problem, Loc.Get("Station.Status.Problem"), Loc.Get("Station.Printer.None")); + + if (label.Entry.AllowDataMerge && label.Merge is not null && Printer is not null) + { + Records = new DataMergeViewModel(_service, Printer, () => doc, () => label.Name, NoFileDialogs.Instance, _services.Messages, _services.Database, + null, _answers); + Records.Apply(label.Merge); + OnPropertyChanged(nameof(ShowRecords)); + await Records.ReloadRememberedDataAsync(); + if (!ReferenceEquals(Current, label)) return; + OnPropertyChanged(nameof(RecordsText)); + OnPropertyChanged(nameof(CanPrintRecords)); + } + await CheckPrinterAsync(); + } + + private void CloseLabel() + { + _statusVersion++; + foreach (var f in Fields) f.PropertyChanged -= OnFieldChanged; + Fields.Clear(); + _previewCache?.Dispose(); + _previewCache = null; + _session = null; + _set = null; + Records = null; + Printer = null; + Preview = null; + Current = null; + OnPropertyChanged(nameof(ShowRecords)); + OnPropertyChanged(nameof(RecordsText)); + OnPropertyChanged(nameof(CanPrintRecords)); + OnPropertyChanged(nameof(HasNoFields)); + SetStatus(StationStatusKind.None, string.Empty, string.Empty); + } + + private PrinterSettingsViewModel? BuildPrinter(StationLabelViewModel label, LabelDocument doc) + { + PrintSettings settings; + if (label.Entry.Printer is { } p) + { + settings = new PrintSettings + { + Configured = true, + Transport = p.Host is null ? "Spooler" : "Tcp", + PrinterName = p.WindowsPrinter ?? string.Empty, + Host = p.Host ?? string.Empty, + Port = p.Port, + Dpi = (p.Dpi is { } dpi ? (PrinterDpi)dpi : doc.Dpi).ToString(), + MediaTracking = doc.MediaTracking.ToString(), + Darkness = p.Darkness, + SpeedIps = p.Speed, + StatusCheckNetwork = p.CheckStatus, + StatusCheckWindows = p.CheckStatus, + }; + } + else if (_services.DesignerPrinter is { Configured: true } d) + { + // A copy: the station never changes the designer's own settings. + settings = new PrintSettings + { + Configured = true, Transport = d.Transport, PrinterName = d.PrinterName, Host = d.Host, Port = d.Port, Dpi = d.Dpi, WidthMm = d.WidthMm, + HeightMm = d.HeightMm, MediaTracking = d.MediaTracking, Darkness = d.Darkness, SpeedIps = d.SpeedIps, + StatusCheckNetwork = d.StatusCheckNetwork, StatusCheckWindows = d.StatusCheckWindows, + }; + } + else return null; + // No driver detection: the station prints at the resolution it was set up with, the same on every PC. + var vm = new PrinterSettingsViewModel(_service, settings, () => { }, () => doc, (_, _) => Task.FromResult(PrinterDetection.None)); + vm.UseDesignSize(); + return vm; + } + + private void OnFieldChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(PromptFieldViewModel.Value)) return; + PreviewRefreshRequested?.Invoke(this, EventArgs.Empty); + } + + /// Draws the preview again with the current answers (and the shown data record, when the label's data is loaded). + public void RenderPreview() + { + if (_previewCache is null || _session is null || Current?.Document is not { } doc) return; + var data = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (Records?.CurrentData is { } record) foreach (var kv in record) data[kv.Key] = kv.Value; + // Typed answers show as the record's values; an empty one keeps the label's default in the picture. + foreach (var f in Fields) if (f.Value.Length > 0) data[f.Name] = f.Item.Uppercase ? f.Value.ToUpperInvariant() : f.Value; + _previewCache.Data = data; + _session.Touch(); + Preview = StationImage.Render(_previewCache, doc, PreviewPixels); + } + + private void RequestFirstFocus() + { + if (Fields.Count > 0) FocusFieldRequested?.Invoke(this, 0); + else FocusPrintRequested?.Invoke(this, EventArgs.Empty); + } + + // ------------------------------------------------------------------------------------ scanner + + /// + /// Enter, Tab or Shift+Tab in a question field (typed, or sent by the scanner after its code). Checks that field's answer, moves the keyboard on, + /// and prints after the last field when "print on last scan" is on. + /// + public async Task FieldTerminatedAsync(PromptFieldViewModel field, ScanTerminator terminator) + { + int index = Fields.IndexOf(field); + if (index < 0 || _set is null) return; + bool accepted = true; + if (terminator != ScanTerminator.ShiftTab) + { + var check = _set.Validate(Typed()); + string error = ErrorsFor(field.Name, check); + field.Error = error; + accepted = error.Length == 0; + } + var step = Scanner.Next(index, Fields.Count, terminator, accepted); + switch (step.Kind) + { + case ScanStepKind.Stay: + case ScanStepKind.MoveTo: + FocusFieldRequested?.Invoke(this, step.FieldIndex); + break; + case ScanStepKind.FocusPrint: + FocusPrintRequested?.Invoke(this, EventArgs.Empty); + break; + case ScanStepKind.Print: + await PrintAsync(); + break; + } + } + + private Dictionary Typed() => Fields.ToDictionary(f => f.Name, f => f.Value, StringComparer.OrdinalIgnoreCase); + + private static string ErrorsFor(string name, PromptValidation check) => + string.Join(" ", check.Issues.Where(i => i.Args.Count > 0 && string.Equals(i.Args[0], name, StringComparison.OrdinalIgnoreCase)).Select(Loc.FormatIssue)); + + /// + /// Checks every answer against the label's own rules (required, length, pattern, type) and marks the refused ones. Returns the accepted answers, + /// or null when one is refused (the keyboard then goes to the first refused field). + /// + private IReadOnlyDictionary? CheckAnswers(PromptSet set) + { + var typed = Typed(); + var check = set.Validate(typed); + foreach (var f in Fields) f.Error = ErrorsFor(f.Name, check); + if (check.IsValid) return check.Values; + int first = Fields.ToList().FindIndex(f => f.HasError); + if (first >= 0) FocusFieldRequested?.Invoke(this, first); + SetStatus(StationStatusKind.Problem, Loc.Get("Station.Status.Problem"), Loc.Get("Station.Fields.Fix")); + return null; + } + + // ------------------------------------------------------------------------------------ printing + + /// Prints the open label with the answers and the quantity, through the print service with the printer status check. + [RelayCommand] + public async Task PrintAsync() + { + if (IsBusy || Current?.Document is not { } source || _set is null) return; + if (Printer is null) + { + SetStatus(StationStatusKind.Problem, Loc.Get("Station.Status.Problem"), Loc.Get("Station.Printer.None")); + return; + } + var answers = CheckAnswers(_set); + if (answers is null) return; + if (!int.TryParse(CopiesText.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out int copies) || copies < 1 || copies > MaxCopies) + { + SetStatus(StationStatusKind.Problem, Loc.Get("Station.Status.Problem"), Loc.Get("Station.Copies.Invalid", MaxCopies)); + FocusPrintRequested?.Invoke(this, EventArgs.Empty); + return; + } + string? error = Printer.TryBuildTarget(out var target); + PrinterProfile? profile = null; + error ??= Printer.TryBuildProfile(out profile, source.Encoding); + if (error is not null) { SetStatus(StationStatusKind.Problem, Loc.Get("Station.Status.Problem"), error); return; } + + // A private copy: the quantity and the resolution conversion never touch the label the station keeps. + var doc = ElementCloner.CloneDocument(source); + doc.Print.Quantity = copies; + var converted = Printer.ConvertForPrinter(doc, out _, out string? convertError); + if (converted is null) { SetStatus(StationStatusKind.Problem, Loc.Get("Station.Status.Problem"), convertError!); return; } + doc = converted; + // The design is checked with the answers before anything is reserved or sent, so the operator sees the real reason. + var variables = _services.Variables; + var gen = VariableEngine.HasDefinitions(doc) + ? ZplGenerator.GenerateWithVariables(doc, new VariableContext { PromptAnswers = answers, CounterStore = variables.CounterStore }, null, variables.TextRasterizer) + : ZplGenerator.Generate(doc, null, variables.TextRasterizer); + if (gen.HasErrors) + { + SetStatus(StationStatusKind.Problem, Loc.Get("Station.Status.Problem"), Loc.Get("Print.Error.InvalidLabel") + Environment.NewLine + PrintMessages.DescribeIssues(gen.Issues)); + return; + } + + var options = VariableServices.Options(variables, answers); + _statusVersion++; + SetStatus(StationStatusKind.Printing, Loc.Get("Station.Status.Printing"), target!.Status.Enabled ? Loc.Get("Station.Status.Printing.Checked") : string.Empty); + PrintResult? result = null; + bool finished = await RunAsync(Loc.Get("Station.Status.Printing"), async ct => result = await _service.PrintAsync(target, doc, null, profile, options, ct)); + if (!finished || result is null) + { + // Stopped or crashed: RunAsync wrote the reason; nothing is reset so the operator can try again. + SetStatus(ResultIsError ? StationStatusKind.Problem : StationStatusKind.None, ResultIsError ? Loc.Get("Station.Status.Problem") : string.Empty, ResultText); + return; + } + if (ShowResult(result, Loc.Get("Print.Done.Label", copies))) + { + _set.Remember(answers, variables.PromptMemory); + ResetFields(); + } + } + + /// Prints every ticked record of the label's data (through the Data merge batch print), with the answers typed here. + [RelayCommand] + private async Task PrintRecordsAsync() + { + if (IsBusy || Records is not { HasRecords: true } records || _set is null) return; + if (!int.TryParse(CopiesText.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out int copies) || copies < 1 || copies > MaxCopies) + { + SetStatus(StationStatusKind.Problem, Loc.Get("Station.Status.Problem"), Loc.Get("Station.Copies.Invalid", MaxCopies)); + return; + } + SetStatus(StationStatusKind.Printing, Loc.Get("Station.Status.Printing"), string.Empty); + var outcome = await records.PrintBatchAsync(RunAsync, p => Progress = p, copies); + if (outcome is not { } o) + { + SetStatus(ResultIsError ? StationStatusKind.Problem : StationStatusKind.None, ResultIsError ? Loc.Get("Station.Status.Problem") : string.Empty, ResultText); + return; + } + SetStatus(o.IsError ? StationStatusKind.Problem : StationStatusKind.Printed, Loc.Get(o.IsError ? "Station.Status.Problem" : "Station.Status.Printed"), o.Text); + if (!o.IsError) ResetFields(); + } + + /// Shows a print result in the status panel. Returns true when the labels went out (the questions are then reset for the next item). + private bool ShowResult(PrintResult result, string doneText) + { + string notes = PrintMessages.DescribeNotices(result); + if (!result.Succeeded) + { + var error = result.Error!; + string text = PrintMessages.Describe(error) + (notes.Length > 0 ? Environment.NewLine + notes : string.Empty); + bool heldBack = error.Kind is PrintErrorKind.HeadOpen or PrintErrorKind.PaperOut or PrintErrorKind.RibbonOut or PrintErrorKind.HeadTooHot + or PrintErrorKind.DiagnosticMode or PrintErrorKind.BufferFull or PrintErrorKind.Paused; + SetStatus(heldBack ? StationStatusKind.HeldBack : StationStatusKind.Problem, Loc.Get(heldBack ? "Station.Status.HeldBack" : "Station.Status.Problem"), text); + return false; + } + var keys = result.Notices.Select(n => n.MessageKey).ToHashSet(StringComparer.Ordinal); + var kind = keys.Contains(PrinterStatusKeys.FaultAfterJob) ? StationStatusKind.Problem + : keys.Contains(PrinterStatusKeys.Verified) ? StationStatusKind.Verified + : keys.Contains(PrinterStatusKeys.NotVerified) || keys.Contains(PrinterStatusKeys.NotChecked) ? StationStatusKind.NotVerified + : StationStatusKind.Printed; + string title = Loc.Get(kind switch + { + StationStatusKind.Problem => "Station.Status.Fault", + StationStatusKind.Verified => "Station.Status.Verified", + StationStatusKind.NotVerified => "Station.Status.NotVerified", + _ => "Station.Status.Printed", + }); + SetStatus(kind, title, notes.Length == 0 ? doneText : doneText + Environment.NewLine + notes); + return true; + } + + /// + /// Gets the questions ready for the next item: answers go back to their defaults (or the remembered last answer), except those the station keeps. + /// + private void ResetFields() + { + if (Current?.Document is not { } doc) return; + var entry = Current.Entry; + var keep = new HashSet(entry.KeepFields, StringComparer.OrdinalIgnoreCase); + _set = PromptSet.Build(doc, null, _services.Variables.PromptMemory); + var fresh = _set.Items.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); + foreach (var f in Fields) + { + if (!entry.KeepAllValues && !keep.Contains(f.Name) && fresh.TryGetValue(f.Name, out var item)) f.Value = item.InitialValue; + f.Error = string.Empty; + } + RenderPreview(); + RequestFirstFocus(); + } + + /// Asks the printer of the open label whether it can print, and shows the answer. Does nothing when the status check is off. + [RelayCommand] + public async Task CheckPrinterAsync() + { + if (Printer is null || Printer.TryBuildTarget(out var target) is not null || target is null || !target.Status.Enabled) return; + int version = ++_statusVersion; + SetStatus(StationStatusKind.Checking, Loc.Get("Station.Status.Checking"), string.Empty); + PrinterStatus status; + try { status = await _service.GetStatusAsync(target); } + catch (OperationCanceledException) { return; } + // A print that started meanwhile has its own, newer answer. + if (version != _statusVersion) return; + (string text, bool problem) = PrintMessages.DescribeStatus(status); + var kind = !status.IsAvailable ? StationStatusKind.Unknown : problem ? StationStatusKind.HeldBack : StationStatusKind.Ready; + SetStatus(kind, Loc.Get(kind switch + { + StationStatusKind.Unknown => "Station.Status.Unknown", + StationStatusKind.HeldBack => "Station.Status.NeedsAttention", + _ => "Station.Status.Ready", + }), text); + } + + // ------------------------------------------------------------------------------------ status panel + + /// What the status panel shows. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasStatus), nameof(StatusIsGood), nameof(StatusIsBad), nameof(StatusIsNeutral))] + private StationStatusKind _statusKind; + + /// The short, large line of the status panel ("Printed and verified"). + [ObservableProperty] + private string _statusTitle = string.Empty; + + /// The explanation under it: what happened and what to do. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasStatusDetail))] + private string _statusDetail = string.Empty; + + /// True when the status panel shows something. + public bool HasStatus => StatusKind != StationStatusKind.None; + + /// True when has text. + public bool HasStatusDetail => StatusDetail.Length > 0; + + /// Green panel: ready, printed, verified. + public bool StatusIsGood => StatusKind is StationStatusKind.Ready or StationStatusKind.Printed or StationStatusKind.Verified; + + /// Red panel: held back or not printed. + public bool StatusIsBad => StatusKind is StationStatusKind.HeldBack or StationStatusKind.Problem; + + /// Amber panel: checking, printing, status unknown, not confirmed. + public bool StatusIsNeutral => StatusKind is StationStatusKind.Checking or StationStatusKind.Printing or StationStatusKind.Unknown or StationStatusKind.NotVerified; + + private void SetStatus(StationStatusKind kind, string title, string detail) + { + StatusKind = kind; + StatusTitle = title; + StatusDetail = detail; + } + + // ------------------------------------------------------------------------------------ leaving the station + + /// True when leaving the station needs the PIN. + public bool HasPin => _config.ExitPinHash is not null; + + /// True once leaving was allowed (the PIN was right, or there is none); the window may then close. + public bool ExitAuthorized { get; private set; } + + /// True while the PIN box is shown. + [ObservableProperty] + private bool _isPinPromptOpen; + + /// Why the PIN was refused; empty otherwise. + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasPinError))] + private string _pinError = string.Empty; + + /// True when has text. + public bool HasPinError => PinError.Length > 0; + + /// Raised when the station may close. + public event EventHandler? ExitRequested; + + /// The Exit button (and closing the window): leaves at once without a PIN, otherwise shows the PIN box. + [RelayCommand] + public void RequestExit() + { + if (!HasPin) + { + Leave(); + return; + } + PinError = string.Empty; + IsPinPromptOpen = true; + } + + /// Closes the PIN box and stays in the station. + [RelayCommand] + public void CancelExit() + { + IsPinPromptOpen = false; + PinError = string.Empty; + } + + /// + /// Checks a typed PIN. A right PIN leaves the station; after wrong ones in a row the box is locked for + /// , so trying every PIN by hand is not practical. Returns true when the station may close. + /// + public async Task SubmitPinAsync(string pin) + { + if (!HasPin) { Leave(); return true; } + var now = _time.GetUtcNow(); + if (now < _lockedUntil) + { + PinError = Loc.Get("Station.Pin.Locked", (int)Math.Ceiling((_lockedUntil - now).TotalSeconds)); + return false; + } + string hash = _config.ExitPinHash!; + // PBKDF2 takes a moment on purpose; it runs off the UI thread so the screen stays responsive. + bool ok = await Task.Run(() => StationPin.Verify(pin, hash)); + if (ok) + { + _pinFailures = 0; + IsPinPromptOpen = false; + Leave(); + return true; + } + if (++_pinFailures >= MaxPinAttempts) + { + _pinFailures = 0; + _lockedUntil = _time.GetUtcNow() + PinLockout; + PinError = Loc.Get("Station.Pin.Locked", (int)PinLockout.TotalSeconds); + } + else PinError = Loc.Get("Station.Pin.Wrong"); + return false; + } + + private void Leave() + { + ExitAuthorized = true; + CancelOperation(); + ExitRequested?.Invoke(this, EventArgs.Empty); + } + + // ------------------------------------------------------------------------------------ helpers + + /// + /// Answers the label's questions for the Data merge batch print from the station's own fields, so a data print never opens the prompt dialog. + /// + private sealed class StationAnswers(StationViewModel station, IVariableServices inner) : IVariableServices + { + public Rotula.Core.Variables.Counters.ICounterStore? CounterStore => inner.CounterStore; + public IPromptMemory PromptMemory => inner.PromptMemory; + public Rotula.Core.Text.ITextRasterizer? TextRasterizer => inner.TextRasterizer; + + public IReadOnlyDictionary? Ask(PromptSet set) + { + if (set.IsEmpty) return new Dictionary(); + var answers = station.CheckAnswers(set); + if (answers is null) return null; + set.Remember(answers, inner.PromptMemory); + return answers; + } + } + + /// The station opens no file dialogs: everything it prints comes from its configured folder. + private sealed class NoFileDialogs : IFileDialogService + { + public static readonly NoFileDialogs Instance = new(); + public string? PickOpenLabel() => null; + public string? PickSaveLabel(string suggestedFileName) => null; + public string? PickExportFile(ExportKind kind, string suggestedFileName) => null; + public string? PickOpenZpl() => null; + public string? PickOpenImage() => null; + public string? PickOpenData() => null; + public string? PickOpenDatabaseFile() => null; + } +} diff --git a/src/Rotula.App/ViewModels/UpdateNoticeViewModel.cs b/src/Rotula.App/ViewModels/UpdateNoticeViewModel.cs new file mode 100644 index 0000000..36ad15f --- /dev/null +++ b/src/Rotula.App/ViewModels/UpdateNoticeViewModel.cs @@ -0,0 +1,56 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Rotula.App.Localization; +using Rotula.App.Services; + +namespace Rotula.App.ViewModels; + +/// +/// The non-modal "a newer version is available" strip under the toolbar. It only links to the release page; Rotula never +/// downloads or installs anything itself. +/// +public sealed partial class UpdateNoticeViewModel : ObservableObject +{ + private readonly Func _open; + private readonly Action _dismissed; + private readonly Action _status; + + /// Creates the notice. + /// The newer release. + /// Opens a web address; returns false when that failed. + /// Called with the version when the user closes the notice (so it is not shown again for that version). + /// Shows a sentence in the status bar. + public UpdateNoticeViewModel(ReleaseInfo release, Func open, Action dismissed, Action status) + { + Release = release ?? throw new ArgumentNullException(nameof(release)); + _open = open; + _dismissed = dismissed; + _status = status; + Loc.Instance.SubscribeWeak(this, static self => self.OnPropertyChanged(nameof(NoticeText))); + } + + /// The newer release. + public ReleaseInfo Release { get; } + + /// The sentence of the notice, in the current language. + public string NoticeText => Loc.Get("Update.Notice", Release.Version.ToString()); + + /// False once the user closed the notice. + [ObservableProperty] + private bool _isVisible = true; + + /// Opens the release page in the browser. + [RelayCommand] + private void OpenRelease() + { + if (!_open(Release.PageUrl)) _status(Loc.Get("Update.OpenFailed", Release.PageUrl)); + } + + /// Hides the notice and remembers that this version was seen. + [RelayCommand] + private void Dismiss() + { + IsVisible = false; + _dismissed(Release.Version.ToString()); + } +} diff --git a/src/Rotula.App/Views/PreviewWindow.xaml b/src/Rotula.App/Views/PreviewWindow.xaml index fc8dafa..dc47648 100644 --- a/src/Rotula.App/Views/PreviewWindow.xaml +++ b/src/Rotula.App/Views/PreviewWindow.xaml @@ -6,11 +6,29 @@ Title="{loc:T Preview.Title}" Width="760" Height="820" MinWidth="420" MinHeight="360" WindowStartupLocation="CenterOwner" ShowInTaskbar="False" AutomationProperties.Name="{loc:T Preview.Title}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +