From 32f39200bd008c92db75203760609b1a3e0bab53 Mon Sep 17 00:00:00 2001 From: maruson08 Date: Thu, 20 Aug 2026 22:09:58 +0900 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=90=9B[Fix]=20Apply=20reviewed=20mult?= =?UTF-8?q?ilingual=20copy=20corrections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/locales/de.js | 196 +++++------ js/locales/es.js | 194 +++++------ js/locales/fr.js | 158 ++++----- js/locales/ja.js | 890 +++++++++++++++++++++++------------------------ js/locales/ko.js | 86 ++--- 5 files changed, 762 insertions(+), 762 deletions(-) diff --git a/js/locales/de.js b/js/locales/de.js index 4a607d9..b04df8a 100644 --- a/js/locales/de.js +++ b/js/locales/de.js @@ -63,7 +63,7 @@ export const de = { }, "common": { "skip": "Zum Hauptinhalt springen", - "brandTagline": "Private Dienstprogramme, verarbeitet auf Ihrem Gerät", + "brandTagline": "Private Dateitools, direkt auf Ihrem Gerät", "nav": { "tools": "Werkzeuge", "privacy": "Datenschutz", @@ -80,10 +80,10 @@ export const de = { "footerStatement": "Dateien werden nicht vom Gerät übertragen." }, "hero": { - "eyebrow": "Dienstprogramme für lokale Dateien", - "title": "Private Tools für alltägliche Dateien.", - "description": "Konvertieren, prüfen und verarbeiten Sie Dateien direkt auf Ihrem Gerät. Keine Uploads. Keine Konten. Keine Nachverfolgung.", - "primary": "Entdecken Sie Tools", + "eyebrow": "Lokale Dateitools", + "title": "Private Tools für alltägliche Dateien", + "description": "Dateien direkt auf Ihrem Gerät konvertieren, prüfen und verarbeiten. Keine Uploads, Konten oder Nachverfolgung", + "primary": "Tools entdecken", "secondary": "Datenschutz überprüfen", "trust": "Lokale Verarbeitung · Open Source · Kein Tracking", "proofLabel": "Was Ihr Gerät verlässt", @@ -92,8 +92,8 @@ export const de = { }, "tools": { "eyebrow": "Werkzeuge", - "title": "Beginnen Sie mit dem, woran Sie arbeiten müssen.", - "description": "Durchsuchen Sie gezielte Kategorien und wählen Sie dann ein Werkzeug aus. Die Verfügbarkeit wird immer deutlich angezeigt.", + "title": "Wählen Sie, womit Sie arbeiten möchten", + "description": "Öffnen Sie eine Kategorie und wählen Sie das passende Tool. Verfügbarkeit wird klar angezeigt", "categories": { "pdf": "PDF", "image": "Bild", @@ -108,10 +108,10 @@ export const de = { "scan": "Verwandeln Sie Scans in nützliche Dokumente.", "media": "Lokal mit Audio und Video arbeiten." }, - "browseCategory": "Kategorie durchsuchen →", - "availableEyebrow": "Gebrauchsfertig", + "browseCategory": "Kategorie öffnen →", + "availableEyebrow": "Sofort nutzbar", "availableTitle": "Jetzt verfügbar", - "availableDescription": "Produktionstools, die die Dateiverarbeitung auf Ihrem Gerät beibehalten.", + "availableDescription": "Tools, die Dateien direkt auf Ihrem Gerät verarbeiten", "imagesToPdf": "Bilder in PDF", "mergePdf": "PDF zusammenführen", "splitPdf": "PDF teilen", @@ -180,9 +180,9 @@ export const de = { } }, "why": { - "eyebrow": "Warum sichere Tools", - "title": "Ein einfacheres Datenschutzmodell.", - "description": "Secure Tools minimiert das, was vertrauenswürdig sein muss, indem es den Server aus den zentralen Dateiverarbeitungs-Workflows entfernt.", + "eyebrow": "Warum Secure Tools", + "title": "Ein einfacheres Datenschutzmodell", + "description": "Secure Tools reduziert die nötige Vertrauensbasis, indem die zentrale Dateiverarbeitung ohne Server auskommt", "local": { "title": "Lokale Verarbeitung", "body": "Dateien werden auf Ihrem Gerät im Browser verarbeitet." @@ -202,8 +202,8 @@ export const de = { }, "verify": { "eyebrow": "Datenschutz überprüfen", - "title": "Vertrauen Sie uns nicht beim Wort.", - "description": "Sie können die Netzwerkaktivität selbst überprüfen und bestätigen, dass eine Datei Ihr Gerät nie verlässt.", + "title": "Prüfen Sie es selbst", + "description": "Über die Netzwerkaktivität können Sie selbst nachvollziehen, dass Dateien das Gerät nicht verlassen", "step1": "Öffnen Sie die Entwicklertools Ihres Browsers.", "step2": "Wählen Sie die Registerkarte Netzwerk.", "step3": "Führen Sie ein sicheres Tool mit einer Beispieldatei aus.", @@ -213,9 +213,9 @@ export const de = { }, "openSource": { "eyebrow": "Open Source", - "title": "Offen durch Design.", - "description": "Secure Tools ist so konzipiert, dass seine Datenschutzansprüche überprüft und nicht einfach nur als vertrauenswürdig eingestuft werden können.", - "action": "Durchsuchen Sie die Quelle" + "title": "Offen by Design", + "description": "Die Datenschutzversprechen von Secure Tools lassen sich im Quellcode überprüfen, statt nur geglaubt werden zu müssen", + "action": "Quellcode ansehen" }, "privacy": { "eyebrow": "Datenschutzmodell", @@ -225,18 +225,18 @@ export const de = { "localBody": "Unterstützte Tools lesen und transformieren Dateien im Browserspeicher. Dateiinhalte werden nicht an einen Secure Tools-Server gesendet.", "collectionTitle": "Keine Konten oder Analysen", "collectionBody": "Der Web-Hub verfügt über keine Benutzerkonten, Analysen, Verhaltensverfolgung, Werbung oder Tracking-Pixel.", - "storageTitle": "Bevorzugter Speicher", + "storageTitle": "Gespeicherte Einstellungen", "storageBody": "localStorage wird nur verwendet, um Ihre Sprach- und Themenauswahl auf diesem Gerät zu speichern.", "linksTitle": "Externe Links", "linksBody": "Wenn Sie einem externen Link wie GitHub folgen, verlassen Sie Secure Tools und unterliegen den eigenen Richtlinien dieses Dienstes." }, "about": { "eyebrow": "Über", - "title": "Kleine Werkzeuge. Überprüfbares Vertrauen.", + "title": "Kleine Tools, überprüfbares Vertrauen", "intro": "Secure Tools ist eine datenschutzorientierte Sammlung alltäglicher Dateidienstprogramme, die für die lokale Ausführung im Browser entwickelt wurden.", "purposeTitle": "Warum es existiert", "purposeBody": "Für allgemeine Dateiaufgaben sollte es nicht erforderlich sein, persönliche Dokumente auf einen unbekannten Server hochzuladen oder ein Konto zu erstellen.", - "philosophyTitle": "Lokal zuerst und offen", + "philosophyTitle": "Lokal und offen by Design", "philosophyBody": "Das Projekt bevorzugt statischen, verständlichen Code und ein Datenschutzmodell, das jeder durch Quellen- und Netzwerkinspektion überprüfen kann." }, "splitPdf": { @@ -244,21 +244,21 @@ export const de = { "title": "PDF teilen", "description": "Extrahieren Sie ausgewählte Seiten oder teilen Sie eine PDF-Datei in vorhersehbare lokale Dateien auf.", "drop": { - "title": "Fügen Sie ein PDF hinzu", + "title": "PDF hinzufügen", "description": "Legen Sie hier ein PDF ab oder verwenden Sie die Auswahl. Die Quelle ist auf diesem Gerät schreibgeschützt.", - "choose": "Wählen Sie PDF", - "localTitle": "Auf diesem Gerät verarbeitet.", - "localBody": "Das PDF wird nicht vom Gerät übertragen.", + "choose": "PDF auswählen", + "localTitle": "Direkt auf diesem Gerät verarbeitet", + "localBody": "Das PDF verlässt dieses Gerät nicht", "privacyLink": "So funktioniert Datenschutz" }, "source": { "title": "Quell-PDF", "empty": "Noch kein PDF ausgewählt.", - "meta": "{size} · {pages} Seite(n)", + "meta": "{size} · {pages} Seiten", "remove": "Quell-PDF entfernen" }, "modes": { - "title": "Split-Modus", + "title": "Teilungsmodus", "legend": "Wählen Sie aus, wie das PDF geteilt werden soll", "extract": { "title": "Seitenbereich extrahieren", @@ -283,20 +283,20 @@ export const de = { }, "settings": { "title": "Ausgabeeinstellungen", - "filename": "Basisname ausgeben", + "filename": "Basisname", "filenameHelp": "Erweiterungen und sortierbare Seitenzahlen werden automatisch hinzugefügt.", "autoDownload": "Automatischer Download nach der Aufteilung", - "clearAfterSave": "Löschen Sie den Quelldateiverweis nach dem Speichern", + "clearAfterSave": "Löschen Sie den Quelldatei nach dem Speichern", "pdfFile": "PDF-Dokument", "zipFile": "ZIP-Archiv" }, "actions": { "split": "Teilen und speichern", - "clear": "Quelle löschen" + "clear": "PDF entfernen" }, "status": { "reading": "Das PDF lokal lesen…", - "loaded": "PDF bereit: {pages} Seite(n).", + "loaded": "PDF bereit: {pages} Seiten.", "cleared": "Quell-PDF gelöscht.", "preparing": "Seiten werden vorbereitet…", "archive": "Erstellen des ZIP-Archivs…", @@ -332,39 +332,39 @@ export const de = { "title": "PDF zusammenführen", "description": "Ordnen Sie PDF-Dateien an und kombinieren Sie jede Seite in einem Dokument. Dateien bleiben auf diesem Gerät.", "drop": { - "title": "Fügen Sie Ihre PDFs hinzu", + "title": "PDFs hinzufügen", "description": "Legen Sie PDF-Dateien hier ab oder verwenden Sie die Auswahl. Die Warteschlangenreihenfolge wird zur Dokumentenreihenfolge.", - "choose": "Wählen Sie PDFs", - "localTitle": "Auf diesem Gerät verarbeitet.", - "localBody": "PDF-Dateien werden nicht vom Gerät übertragen.", + "choose": "PDFs auswählen", + "localTitle": "Direkt auf diesem Gerät verarbeitet", + "localBody": "Die PDFs verlassen dieses Gerät nicht", "privacyLink": "So funktioniert Datenschutz" }, "queue": { - "title": "PDF-Bestellung", + "title": "PDF-Reihenfolge", "count": "PDFs: {count}", "empty": "Noch keine PDFs hinzugefügt.", - "meta": "{size} · {pages} Seite(n)", + "meta": "{size} · {pages} Seiten", "moveUp": "PDF nach oben verschieben", "moveDown": "PDF nach unten verschieben", "remove": "PDF entfernen" }, "settings": { - "title": "Einstellungen zusammenführen", + "title": "Optionen zum Zusammenführen", "filename": "Name der Ausgabedatei", "filenameHelp": "Eine .pdf-Erweiterung wird automatisch hinzugefügt.", "autoDownload": "Nach dem Zusammenführen automatisch herunterladen", - "clearAfterSave": "Dateiverweise nach dem Speichern löschen", + "clearAfterSave": "Dateiliste nach dem Speichern löschen", "pdfFile": "PDF-Dokument" }, "actions": { "merge": "PDF zusammenführen und speichern", "add": "Weitere PDFs hinzufügen", - "clear": "Alles löschen" + "clear": "Alles entfernen" }, "status": { "validating": "PDFs lokal prüfen…", - "added": "{count} PDF-Datei(en) hinzugefügt.", - "addedWithRejected": "{added} PDF-Datei(en) hinzugefügt; übersprungene {rejected} nicht unterstützte oder nicht lesbare Datei(en).", + "added": "{count} PDF-Dateien hinzugefügt.", + "addedWithRejected": "{added} PDF-Dateien hinzugefügt; übersprungene {rejected} nicht unterstützte oder nicht lesbare Dateien.", "removed": "PDF entfernt.", "reordered": "PDF-Bestellung aktualisiert.", "cleared": "PDF-Liste gelöscht.", @@ -372,8 +372,8 @@ export const de = { "progress": "Datei {completed} von {total} wird zusammengeführt…", "progressLabel": "PDF-Zusammenführungsfortschritt", "saved": "Ein {pages}-Seiten-PDF gespeichert.", - "downloaded": "{name} mit {pages}-Seite(n) heruntergeladen.", - "savedAndCleared": "PDF gespeichert und Dateiverweise gelöscht.", + "downloaded": "{name} mit {pages}-Seiten heruntergeladen.", + "savedAndCleared": "PDF gespeichert und Dateiliste gelöscht.", "saveCancelled": "Speichern abgebrochen. Es wurde keine Datei geschrieben." }, "errors": { @@ -391,11 +391,11 @@ export const de = { "title": "Bilder in PDF", "description": "Ordnen Sie JPEG-, PNG- und WebP-Bilder an und speichern Sie sie als ein PDF. Alles passiert auf diesem Gerät.", "drop": { - "title": "Fügen Sie Ihre Bilder hinzu", + "title": "Bilder hinzufügen", "description": "Legen Sie Dateien hier ab oder verwenden Sie die Auswahl. JPEG, PNG und WebP können gemischt werden.", - "choose": "Wählen Sie Bilder aus", - "localTitle": "Auf diesem Gerät verarbeitet.", - "localBody": "Bilder werden nicht vom Gerät übertragen.", + "choose": "Bilder auswählen", + "localTitle": "Direkt auf diesem Gerät verarbeitet", + "localBody": "Die Bilder verlassen dieses Gerät nicht", "privacyLink": "So funktioniert Datenschutz" }, "queue": { @@ -414,33 +414,33 @@ export const de = { "filenameHelp": "Eine .pdf-Erweiterung wird automatisch hinzugefügt.", "pageSize": "Seitengröße", "a4": "A4", - "letter": "Brief", - "imageSize": "Bild anpassen", + "letter": "Letter", + "imageSize": "Bildgröße übernehmen", "orientation": "Ausrichtung", "auto": "Automatisch", - "portrait": "Porträt", - "landscape": "Landschaft", + "portrait": "Hochformat", + "landscape": "Querformat", "fit": "Bildanpassung", "contain": "Gesamtes Bild anpassen", "cover": "Seite füllen", "margin": "Rand (mm)", "quality": "Bildqualität", - "qualityBest": "Am besten", + "qualityBest": "Beste", "qualityHigh": "Hoch", - "qualityBalanced": "Ausgeglichen", + "qualityBalanced": "Ausgewogen", "qualitySmall": "Kleinere Datei", "autoDownload": "Nach der Generierung automatisch herunterladen", - "clearAfterSave": "Bildreferenzen nach dem Speichern löschen", + "clearAfterSave": "Bildliste nach dem Speichern löschen", "pdfFile": "PDF-Dokument" }, "actions": { "generate": "PDF generieren und speichern", "add": "Weitere Bilder hinzufügen", - "clear": "Alles löschen" + "clear": "Alles entfernen" }, "status": { "added": "{count}-Bilder hinzugefügt.", - "addedWithRejected": "Von {added} unterstützte Bilder hinzugefügt; {rejected} nicht unterstützte Datei(en) übersprungen.", + "addedWithRejected": "Von {added} unterstützte Bilder hinzugefügt; {rejected} nicht unterstützte Dateien übersprungen.", "addedWithReasons": "{added}-Bilder hinzugefügt. Abgelehnt: {reasons}", "rejectedItem": "{name}: {reason}", "removed": "Bild entfernt.", @@ -473,28 +473,28 @@ export const de = { }, "pdfMetadata": { "eyebrow": "PDF-Datenschutztool", - "title": "PDF-Metadaten-Inspektor und -Reiniger", + "title": "PDF-Metadaten prüfen und entfernen", "description": "Überprüfen Sie allgemeine Dokumentmetadaten und erstellen Sie eine bereinigte Kopie, ohne Ihr PDF hochzuladen.", "scope": { - "title": "Unterstützter Bereich", - "body": "Dieses Tool deckt die unten gezeigten Standard-Dokumentinfofelder ab. Es ist kein forensischer Scrubber und erhebt nicht den Anspruch, XMP-Pakete, Anhänge, Anmerkungen, versteckte Inhalte oder jede identifizierende Struktur zu entfernen.", + "title": "Unterstützter Umfang", + "body": "Dieses Tool deckt die unten aufgeführten Standardfelder der Dokumentinformationen ab. Es ist kein vollständiges forensisches Bereinigungswerkzeug und entfernt nicht zwangsläufig XMP-Daten, Anhänge, Anmerkungen, versteckte Inhalte oder jede andere identifizierende Struktur.", "cleaning": "Beim Bereinigen werden nur die oben aufgeführten unterstützten Dokumentmetadaten entfernt." }, "drop": { - "title": "Fügen Sie ein PDF hinzu", + "title": "PDF hinzufügen", "description": "Legen Sie hier ein PDF ab oder verwenden Sie die Auswahl. Durch die Auswahl einer anderen Datei wird die aktuelle Quelle ersetzt.", - "choose": "Wählen Sie PDF", - "localTitle": "Auf diesem Gerät verarbeitet.", - "localBody": "Das PDF wird nicht vom Gerät übertragen.", + "choose": "PDF auswählen", + "localTitle": "Direkt auf diesem Gerät verarbeitet", + "localBody": "Das PDF verlässt dieses Gerät nicht", "privacyLink": "So funktioniert Datenschutz" }, "source": { "title": "Quell-PDF", "empty": "Noch kein PDF ausgewählt.", - "summary": "{name} · {size} · {pages} Seite(n) · {fields} unterstützte Felder vorhanden" + "summary": "{name} · {size} · {pages} Seiten · {fields} unterstützte Felder vorhanden" }, "inspector": { - "title": "Dokumentmetadaten erkannt", + "title": "Gefundene Dokumentmetadaten", "help": "Wählen Sie die zu entfernenden Felder aus. Die Werte „Ersteller“, „Produzent“, „Autor“ und „Datum“ können eine Person, ein Gerät oder eine Anwendung identifizieren.", "caption": "Unterstützte Metadatenfelder im PDF gefunden", "remove": "Entfernen", @@ -509,8 +509,8 @@ export const de = { "author": "Autor", "subject": "Betreff", "keywords": "Schlüsselwörter", - "creator": "Creator-Anwendung", - "producer": "PDF-Ersteller", + "creator": "Erstellende Anwendung", + "producer": "PDF-Produzent", "creationDate": "Erstellungsdatum", "modificationDate": "Änderungsdatum" }, @@ -522,18 +522,18 @@ export const de = { "pdfFile": "PDF-Dokument" }, "actions": { - "clearSource": "PDF löschen", - "selectAll": "Alle Anwesenden auswählen", - "clearSelection": "Auswahl löschen", + "clearSource": "PDF entfernen", + "selectAll": "Alle vorhandenen auswählen", + "clearSelection": "Auswahl aufheben", "removeAll": "Alle unterstützten Felder entfernen", - "clean": "PDF bereinigen und speichern" + "clean": "Bereinigen und speichern" }, "comparison": { - "title": "Vorher und nachher überprüft", + "title": "Geprüftes Ergebnis", "before": "Vorher", - "after": "Nach gespeicherter Kopie", - "cleared": "Verifiziert: {count} ausgewählte Felder fehlen in der serialisierten Kopie.", - "retained": "Warnung: {count} ausgewählte Felder verbleiben in der serialisierten Kopie." + "after": "Gespeicherte Kopie", + "cleared": "Verifiziert: {count} ausgewählte Felder fehlen in der gespeicherten Kopie.", + "retained": "Warnung: {count} ausgewählte Felder verbleiben in der gespeicherten Kopie." }, "status": { "reading": "Metadaten lokal lesen…", @@ -541,8 +541,8 @@ export const de = { "noneFound": "In dieser {pages}-Seiten-PDF wurden keine unterstützten Dokumentinfo-Metadaten gefunden.", "cleared": "Quell-PDF gelöscht.", "cleaning": "Ausgewählte Metadaten entfernen und die gespeicherten Bytes überprüfen…", - "saved": "Gespeichert {name}; Verifiziertes {count}-Feld(er) entfernt.", - "downloaded": "Heruntergeladen {name}; Verifiziertes {count}-Feld(er) entfernt.", + "saved": "Gespeichert {name}; Verifiziertes {count}-Felder entfernt.", + "downloaded": "Heruntergeladen {name}; Verifiziertes {count}-Felder entfernt.", "partiallyCleaned": "{name} gespeichert, aber bei der Überprüfung wurde festgestellt, dass die ausgewählten {count}-Felder noch vorhanden sind.", "cancelled": "Speichern abgebrochen. Es wurde keine Datei geschrieben." }, @@ -562,17 +562,17 @@ export const de = { "title": "PDF in Bilder", "description": "Konvertieren Sie PDF-Seiten in PNG-, JPEG- oder WebP-Dateien, ohne das Dokument hochzuladen.", "drop": { - "title": "Fügen Sie ein PDF hinzu", + "title": "PDF hinzufügen", "description": "Legen Sie hier ein PDF ab oder verwenden Sie die Auswahl. Durch die Auswahl einer neuen PDF-Datei wird die aktuelle Quelle sicher ersetzt.", - "choose": "Wählen Sie PDF", - "localTitle": "Auf diesem Gerät verarbeitet.", - "localBody": "Das PDF wird nicht vom Gerät übertragen.", + "choose": "PDF auswählen", + "localTitle": "Direkt auf diesem Gerät verarbeitet", + "localBody": "Das PDF verlässt dieses Gerät nicht", "privacyLink": "So funktioniert Datenschutz" }, "source": { "title": "Quell-PDF", "empty": "Noch kein PDF ausgewählt.", - "meta": "{size} · {pages} Seite(n)", + "meta": "{size} · {pages} Seiten", "remove": "Quell-PDF entfernen" }, "pages": { @@ -591,7 +591,7 @@ export const de = { }, "settings": { "title": "Bildeinstellungen", - "filename": "Basisname ausgeben", + "filename": "Basisname", "filenameHelp": "Seitenzahlen und Dateierweiterungen werden automatisch hinzugefügt.", "format": "Bildformat", "quality": "Bildqualität", @@ -604,18 +604,18 @@ export const de = { "actions": { "convert": "Konvertieren und speichern", "cancel": "Abbrechen", - "clear": "Quelle löschen" + "clear": "PDF entfernen" }, "status": { "reading": "Das PDF lokal lesen…", - "loaded": "PDF bereit: {pages} Seite(n).", + "loaded": "PDF bereit: {pages} Seiten.", "rendering": "Ausgewählte Seiten lokal rendern…", "progress": "Rendering-Ausgabe {completed} von {total} (Quellenseite {page})…", "progressLabel": "Fortschritt der PDF-Bildkonvertierung", "archive": "Vorbereiten des ZIP-Archivs…", "saving": "Das Bild wird vorbereitet…", - "saved": "{name} mit {count}-Bild(ern) gespeichert.", - "downloaded": "{name} mit {count}-Bild(ern) heruntergeladen.", + "saved": "{name} mit {count}-Bildern gespeichert.", + "downloaded": "{name} mit {count}-Bildern heruntergeladen.", "saveCancelled": "Speichern abgebrochen. Es wurde keine Datei geschrieben.", "cancelled": "Konvertierung abgebrochen. Es wurde keine Datei geschrieben.", "cleared": "Quell-PDF gelöscht." @@ -649,17 +649,17 @@ export const de = { "title": "PDF organisieren", "description": "Seiten neu anordnen, drehen oder entfernen, ohne das Original-PDF zu ändern. Alles bleibt auf diesem Gerät.", "drop": { - "title": "Fügen Sie ein PDF hinzu", + "title": "PDF hinzufügen", "description": "Legen Sie hier ein PDF ab oder verwenden Sie die Auswahl.", - "choose": "Wählen Sie PDF", - "localTitle": "Auf diesem Gerät verarbeitet.", - "localBody": "Das PDF wird nicht vom Gerät übertragen.", + "choose": "PDF auswählen", + "localTitle": "Direkt auf diesem Gerät verarbeitet", + "localBody": "Das PDF verlässt dieses Gerät nicht", "privacyLink": "So funktioniert Datenschutz" }, "workspace": { - "title": "Seitenarbeitsbereich", + "title": "Seitenübersicht", "empty": "Noch kein PDF ausgewählt.", - "summary": "{name} · {pages} verbleibende Seite(n) · {size}", + "summary": "{name} · {pages} verbleibende Seiten · {size}", "changed": "Nicht gespeicherte Änderungen", "gridLabel": "PDF-Seiten" }, @@ -682,14 +682,14 @@ export const de = { }, "actions": { "reset": "Änderungen zurücksetzen", - "clear": "PDF löschen", + "clear": "PDF entfernen", "export": "Organisiertes PDF exportieren" }, "status": { "loading": "PDF wird lokal geladen…", "preparing": "Seite {completed} von {total} wird vorbereitet…", "progressLabel": "Fortschritt der Miniaturbildvorbereitung", - "ready": "PDF bereit: {pages} Seite(n).", + "ready": "PDF bereit: {pages} Seiten.", "moved": "Seite {page} verschoben.", "rotated": "Seite {page} gedreht.", "removed": "Seite {page} entfernt.", @@ -712,8 +712,8 @@ export const de = { }, "notFound": { "eyebrow": "404", - "title": "Diese Seite ist nicht hier.", - "description": "Die Adresse ist möglicherweise veraltet oder hat einen Tippfehler.", + "title": "Diese Seite gibt es nicht", + "description": "Die Adresse ist möglicherweise veraltet oder falsch eingegeben", "action": "Rückkehr nach Hause" } }; diff --git a/js/locales/es.js b/js/locales/es.js index 15e91fd..fbc53f9 100644 --- a/js/locales/es.js +++ b/js/locales/es.js @@ -37,7 +37,7 @@ export const es = { "description": "Convierta páginas PDF a imágenes PNG, JPEG o WebP localmente en su navegador." }, "pdfMetadata": { - "title": "Inspector y limpiador de metadatos de PDF: Secure Tools", + "title": "Metadatos del PDF — Secure Tools", "description": "Inspeccione y elimine metadatos PDF compatibles localmente en su navegador." }, "pdfCategory": { @@ -63,7 +63,7 @@ export const es = { }, "common": { "skip": "Saltar al contenido principal", - "brandTagline": "Servicios públicos privados, procesados en tu dispositivo", + "brandTagline": "Herramientas privadas, procesadas en tu dispositivo", "nav": { "tools": "Herramientas", "privacy": "Privacidad", @@ -80,9 +80,9 @@ export const es = { "footerStatement": "Los archivos permanecen en el dispositivo." }, "hero": { - "eyebrow": "Utilidades de archivos locales primero", - "title": "Herramientas privadas para archivos cotidianos.", - "description": "Convierta, inspeccione y procese archivos directamente en su dispositivo. Sin cargas. Sin cuentas. Sin seguimiento.", + "eyebrow": "Herramientas de archivos locales", + "title": "Herramientas privadas para tus archivos", + "description": "Convierte, revisa y procesa archivos directamente en tu dispositivo. Sin subidas, cuentas ni seguimiento", "primary": "Explorar herramientas", "secondary": "Verificar privacidad", "trust": "Procesamiento local · Código abierto · Sin seguimiento", @@ -92,8 +92,8 @@ export const es = { }, "tools": { "eyebrow": "Herramientas", - "title": "Comience con lo que necesita trabajar.", - "description": "Explore las categorías enfocadas y luego elija una herramienta. La disponibilidad siempre se muestra claramente.", + "title": "Elige el tipo de archivo con el que quieres trabajar", + "description": "Explora las categorías y elige una herramienta. La disponibilidad se muestra de forma clara", "categories": { "pdf": "PDF", "image": "Imagen", @@ -108,10 +108,10 @@ export const es = { "scan": "Convierta los escaneos en documentos útiles.", "media": "Trabaja con audio y video localmente." }, - "browseCategory": "Buscar categoría →", - "availableEyebrow": "Listo para usar", + "browseCategory": "Ver categoría →", + "availableEyebrow": "Disponible ahora", "availableTitle": "Disponible ahora", - "availableDescription": "Herramientas de producción que mantienen el procesamiento de archivos en su dispositivo.", + "availableDescription": "Herramientas que procesan los archivos directamente en su dispositivo", "imagesToPdf": "Imágenes a PDF", "mergePdf": "Fusionar PDF", "splitPdf": "Dividir PDF", @@ -180,9 +180,9 @@ export const es = { } }, "why": { - "eyebrow": "¿Por qué proteger las herramientas?", - "title": "Un modelo de privacidad más simple.", - "description": "Secure Tools minimiza lo que se debe confiar al eliminar el servidor de los flujos de trabajo principales de procesamiento de archivos.", + "eyebrow": "Por qué Secure Tools", + "title": "Un modelo de privacidad más simple", + "description": "Secure Tools reduce lo que necesitas confiar al mantener el servidor fuera del procesamiento principal de archivos", "local": { "title": "Procesamiento local", "body": "Los archivos se procesan en su dispositivo, dentro del navegador." @@ -202,8 +202,8 @@ export const es = { }, "verify": { "eyebrow": "Verificar privacidad", - "title": "No confíe en nuestra palabra.", - "description": "Puede inspeccionar la actividad de la red usted mismo y confirmar que un archivo nunca sale de su dispositivo.", + "title": "Compruébalo tú mismo", + "description": "Puedes revisar la actividad de red y confirmar que tus archivos no salen del dispositivo", "step1": "Abra las Herramientas de desarrollo de su navegador.", "step2": "Seleccione la pestaña Red.", "step3": "Ejecute una herramienta segura con un archivo de muestra.", @@ -213,15 +213,15 @@ export const es = { }, "openSource": { "eyebrow": "Código abierto", - "title": "Abierto por diseño.", - "description": "Secure Tools está diseñado para que sus afirmaciones de privacidad puedan ser inspeccionadas en lugar de simplemente confiar en ellas.", - "action": "Explorar la fuente" + "title": "Abierto por diseño", + "description": "Secure Tools está pensado para que sus garantías de privacidad puedan comprobarse, no solo darse por sentadas", + "action": "Ver código fuente" }, "privacy": { "eyebrow": "Modelo de privacidad", "title": "La privacidad es una elección arquitectónica.", "intro": "Secure Tools está diseñado para procesar el contenido de los archivos localmente en su navegador. Esta página describe el modelo técnico actual, no es una promesa sobre características inacabadas.", - "localTitle": "Procesamiento de archivos locales", + "localTitle": "Procesamiento local de archivos", "localBody": "Las herramientas compatibles leen y transforman archivos en la memoria del navegador. El contenido del archivo no se envía a un servidor de Secure Tools.", "collectionTitle": "Sin cuentas ni análisis", "collectionBody": "El centro web no tiene cuentas de usuario, análisis, seguimiento de comportamiento, publicidad ni píxeles de seguimiento.", @@ -232,11 +232,11 @@ export const es = { }, "about": { "eyebrow": "Acerca de", - "title": "Pequeñas herramientas. Confianza inspeccionable.", + "title": "Herramientas pequeñas, confianza verificable", "intro": "Secure Tools es una colección de utilidades de archivos cotidianos que prioriza la privacidad y está diseñada para ejecutarse localmente en el navegador.", "purposeTitle": "Por qué existe", "purposeBody": "Las tareas de archivos comunes no deberían requerir cargar documentos personales a un servidor desconocido o crear una cuenta.", - "philosophyTitle": "Local primero y abierto", + "philosophyTitle": "Local y abierto por diseño", "philosophyBody": "El proyecto favorece el código estático y comprensible y un modelo de privacidad que cualquiera puede verificar mediante la inspección de fuentes y redes." }, "splitPdf": { @@ -244,21 +244,21 @@ export const es = { "title": "Dividir PDF", "description": "Extraiga páginas seleccionadas o divida un PDF en archivos locales predecibles.", "drop": { - "title": "Agregar un PDF", + "title": "Añadir PDF", "description": "Suelta un PDF aquí o usa el selector. La fuente es de solo lectura en este dispositivo.", - "choose": "Elija PDF", - "localTitle": "Procesado en el dispositivo.", - "localBody": "El PDF no se envía fuera del dispositivo.", + "choose": "Elegir PDF", + "localTitle": "Procesado en este dispositivo", + "localBody": "El PDF no sale de este dispositivo", "privacyLink": "Cómo funciona la privacidad" }, "source": { - "title": "Código fuente PDF", + "title": "PDF de origen", "empty": "Aún no se ha seleccionado ningún PDF.", - "meta": "{size} · {pages} página(s)", + "meta": "{size} · {pages} páginas", "remove": "Eliminar PDF fuente" }, "modes": { - "title": "Modo dividido", + "title": "Modo de división", "legend": "Elige cómo dividir el PDF", "extract": { "title": "Extraer rango de páginas", @@ -283,7 +283,7 @@ export const es = { }, "settings": { "title": "Configuración de salida", - "filename": "Nombre base de salida", + "filename": "Nombre base", "filenameHelp": "Las extensiones y los números de página ordenables se agregan automáticamente.", "autoDownload": "Descargar automáticamente después de dividir", "clearAfterSave": "Borrar la referencia del archivo fuente después de guardar", @@ -292,17 +292,17 @@ export const es = { }, "actions": { "split": "Dividir y guardar", - "clear": "Borrar fuente" + "clear": "Quitar PDF" }, "status": { "reading": "Leyendo el PDF localmente…", - "loaded": "PDF listo: página(s) {pages}.", + "loaded": "PDF listo: páginas {pages}.", "cleared": "PDF fuente borrado.", "preparing": "Preparando páginas…", "archive": "Creando el archivo ZIP…", "progress": "Creando archivo {completed} de {total}…", "progressLabel": "Progreso de división de PDF", - "saved": "{name} guardado con archivo(s) de salida {count}.", + "saved": "{name} guardado con archivos de salida {count}.", "downloaded": "Se descargó {name} con archivos de salida {count}.", "savedAndCleared": "Se guardó el resultado dividido y se borró la referencia de origen.", "saveCancelled": "Guardado cancelado. No se escribió ningún archivo." @@ -324,7 +324,7 @@ export const es = { "rangeReversed": "Los rangos deben ir desde una página inferior a una página superior.", "intervalInvalid": "Ingrese un número entero de al menos 1 para páginas por archivo.", "mode": "Elija un modo de división válido.", - "save": "No se pudo abrir la ubicación para guardar. Por favor inténtalo de nuevo." + "save": "No se pudo abrir la ubicación para guardar. Inténtelo de nuevo." } }, "mergePdf": { @@ -332,24 +332,24 @@ export const es = { "title": "Fusionar PDF", "description": "Organice archivos PDF y combine cada página en un solo documento. Los archivos permanecen en este dispositivo.", "drop": { - "title": "Añade tus archivos PDF", + "title": "Añadir PDF", "description": "Suelte archivos PDF aquí o use el selector. El orden de la cola se convierte en el orden de los documentos.", - "choose": "Elija archivos PDF", - "localTitle": "Procesado en el dispositivo.", - "localBody": "Los PDF no se envían fuera del dispositivo.", + "choose": "Elegir PDF", + "localTitle": "Procesado en este dispositivo", + "localBody": "Los PDF no salen de este dispositivo", "privacyLink": "Cómo funciona la privacidad" }, "queue": { - "title": "Pedido PDF", + "title": "Orden de los PDF", "count": "PDF: {count}", "empty": "Aún no hay archivos PDF.", - "meta": "{size} · {pages} página(s)", + "meta": "{size} · {pages} páginas", "moveUp": "Mover PDF hacia arriba", "moveDown": "Mover PDF hacia abajo", "remove": "Eliminar PDF" }, "settings": { - "title": "Fusionar configuraciones", + "title": "Opciones de fusión", "filename": "Nombre del archivo de salida", "filenameHelp": "Se agrega automáticamente una extensión .pdf.", "autoDownload": "Descargar automáticamente después de fusionar", @@ -359,7 +359,7 @@ export const es = { "actions": { "merge": "Fusionar y guardar PDF", "add": "Agregar más archivos PDF", - "clear": "Borrar todo" + "clear": "Vaciar todo" }, "status": { "validating": "Comprobando archivos PDF localmente…", @@ -372,7 +372,7 @@ export const es = { "progress": "Fusionando el archivo {completed} de {total}…", "progressLabel": "Progreso de fusión de PDF", "saved": "Se guardó un PDF de página {pages}.", - "downloaded": "Descargado {name} con página(s) {pages}.", + "downloaded": "Descargado {name} con páginas {pages}.", "savedAndCleared": "PDF guardado y referencias de archivos borradas.", "saveCancelled": "Guardado cancelado. No se escribió ningún archivo." }, @@ -383,7 +383,7 @@ export const es = { "unreadable": "No se pudo leer {name}. El PDF puede estar dañado o no ser compatible.", "library": "No se pudo cargar la biblioteca PDF local.", "generation": "El PDF no se pudo fusionar. Pruebe con menos archivos o más pequeños.", - "save": "No se pudo abrir la ubicación para guardar. Por favor inténtalo de nuevo." + "save": "No se pudo abrir la ubicación para guardar. Inténtelo de nuevo." } }, "imageToPdf": { @@ -391,11 +391,11 @@ export const es = { "title": "Imágenes a PDF", "description": "Organice imágenes JPEG, PNG y WebP y guárdelas como un PDF. Todo sucede en este dispositivo.", "drop": { - "title": "Añade tus imágenes", + "title": "Añadir imágenes", "description": "Suelta archivos aquí o usa el selector. Se pueden combinar JPEG, PNG y WebP.", - "choose": "Elige imágenes", - "localTitle": "Procesado en el dispositivo.", - "localBody": "Las imágenes no se envían fuera del dispositivo.", + "choose": "Elegir imágenes", + "localTitle": "Procesado en este dispositivo", + "localBody": "Las imágenes no salen de este dispositivo", "privacyLink": "Cómo funciona la privacidad" }, "queue": { @@ -436,7 +436,7 @@ export const es = { "actions": { "generate": "Generar y guardar PDF", "add": "Agregar más imágenes", - "clear": "Borrar todo" + "clear": "Vaciar todo" }, "status": { "added": "Se agregaron imágenes {count}.", @@ -468,33 +468,33 @@ export const es = { "canvas": "Este navegador no puede preparar imágenes para la generación de PDF.", "library": "No se pudo cargar la biblioteca PDF local.", "generation": "No se pudo generar el PDF. Pruebe con menos imágenes o más pequeñas.", - "save": "No se pudo abrir la ubicación para guardar. Por favor inténtalo de nuevo." + "save": "No se pudo abrir la ubicación para guardar. Inténtelo de nuevo." } }, "pdfMetadata": { "eyebrow": "Herramienta de privacidad de PDF", - "title": "Inspector y limpiador de metadatos PDF", + "title": "Metadatos del PDF", "description": "Revise los metadatos de documentos comunes y cree una copia limpia sin cargar su PDF.", "scope": { - "title": "Alcance admitido", - "body": "Esta herramienta cubre los campos estándar de información del documento que se muestran a continuación. No es un depurador forense y no pretende eliminar paquetes XMP, archivos adjuntos, anotaciones, contenido oculto o cualquier estructura de identificación.", + "title": "Alcance compatible", + "body": "Esta herramienta cubre los campos estándar de información del documento que se muestran abajo. No es una herramienta de limpieza forense completa y no pretende eliminar XMP, archivos adjuntos, anotaciones, contenido oculto ni cualquier otro identificador posible.", "cleaning": "La limpieza elimina solo los metadatos de documentos admitidos enumerados anteriormente." }, "drop": { - "title": "Agregar un PDF", + "title": "Añadir PDF", "description": "Suelta un PDF aquí o usa el selector. Elegir otro archivo reemplaza la fuente actual.", - "choose": "Elija PDF", - "localTitle": "Procesado en el dispositivo.", - "localBody": "El PDF no se envía fuera del dispositivo.", + "choose": "Elegir PDF", + "localTitle": "Procesado en este dispositivo", + "localBody": "El PDF no sale de este dispositivo", "privacyLink": "Cómo funciona la privacidad" }, "source": { - "title": "Código fuente PDF", + "title": "PDF de origen", "empty": "Aún no se ha seleccionado ningún PDF.", - "summary": "{name} · {size} · Página(s) {pages} · Campo(s) compatible(s) {fields} presentes" + "summary": "{name} · {size} · Páginas {pages} · campos compatible(s) {fields} presentes" }, "inspector": { - "title": "Metadatos del documento detectado", + "title": "Metadatos encontrados", "help": "Seleccione los campos a eliminar. Los valores de creador, productor, autor y fecha pueden identificar a una persona, dispositivo o aplicación.", "caption": "Campos de metadatos admitidos que se encuentran en el PDF", "remove": "Eliminar", @@ -509,8 +509,8 @@ export const es = { "author": "Autor", "subject": "Asunto", "keywords": "Palabras clave", - "creator": "Aplicación de creador", - "producer": "Productor de PDF", + "creator": "Aplicación de creación", + "producer": "Productor PDF", "creationDate": "Fecha de creación", "modificationDate": "Fecha de modificación" }, @@ -522,18 +522,18 @@ export const es = { "pdfFile": "Documento PDF" }, "actions": { - "clearSource": "Borrar PDF", - "selectAll": "Seleccionar todos los presentes", - "clearSelection": "Borrar selección", + "clearSource": "Quitar PDF", + "selectAll": "Seleccionar los presentes", + "clearSelection": "Deseleccionar", "removeAll": "Eliminar todos los campos compatibles", - "clean": "Limpiar y guardar PDF" + "clean": "Limpiar y guardar" }, "comparison": { - "title": "Verificado antes y después", + "title": "Resultado verificado", "before": "Antes", - "after": "Después de la copia guardada", - "cleared": "Verificado: {count} los campos seleccionados no están en la copia serializada.", - "retained": "Advertencia: Los campos seleccionados de {count} permanecen en la copia serializada." + "after": "Copia guardada", + "cleared": "Verificado: {count} los campos seleccionados no están en la copia guardada.", + "retained": "Advertencia: Los campos seleccionados de {count} permanecen en la copia guardada." }, "status": { "reading": "Leyendo metadatos localmente…", @@ -549,12 +549,12 @@ export const es = { "errors": { "oneFile": "Elija un PDF a la vez.", "unsupported": "Elija un archivo PDF.", - "protected": "Este PDF está protegido con contraseña o encriptado y no se puede inspeccionar.", + "protected": "Este PDF está protegido con contraseña o cifrado y no se puede inspeccionar.", "unreadable": "Este PDF está dañado o es ilegible.", "noSelection": "Seleccione al menos un campo de metadatos actual para eliminar.", "pagesChanged": "La copia limpiada no conservó todas las dimensiones y rotación de la página, por lo que no se guardó.", "failure": "Los metadatos del PDF no se pudieron procesar de forma segura.", - "save": "No se pudo abrir la ubicación para guardar. Por favor inténtalo de nuevo." + "save": "No se pudo abrir la ubicación para guardar. Inténtelo de nuevo." } }, "pdfToImages": { @@ -562,17 +562,17 @@ export const es = { "title": "PDF a imágenes", "description": "Convierta páginas PDF a archivos PNG, JPEG o WebP sin cargar el documento.", "drop": { - "title": "Agregar un PDF", + "title": "Añadir PDF", "description": "Suelta un PDF aquí o usa el selector. Elegir un nuevo PDF reemplaza de forma segura la fuente actual.", - "choose": "Elija PDF", - "localTitle": "Procesado en el dispositivo.", - "localBody": "El PDF no se envía fuera del dispositivo.", + "choose": "Elegir PDF", + "localTitle": "Procesado en este dispositivo", + "localBody": "El PDF no sale de este dispositivo", "privacyLink": "Cómo funciona la privacidad" }, "source": { - "title": "Código fuente PDF", + "title": "PDF de origen", "empty": "Aún no se ha seleccionado ningún PDF.", - "meta": "{size} · {pages} página(s)", + "meta": "{size} · {pages} páginas", "remove": "Eliminar PDF fuente" }, "pages": { @@ -591,7 +591,7 @@ export const es = { }, "settings": { "title": "Configuración de imagen", - "filename": "Nombre base de salida", + "filename": "Nombre base", "filenameHelp": "Los números de página y las extensiones de archivo se agregan automáticamente.", "format": "Formato de imagen", "quality": "Calidad de imagen", @@ -604,13 +604,13 @@ export const es = { "actions": { "convert": "Convertir y guardar", "cancel": "Cancelar", - "clear": "Borrar fuente" + "clear": "Quitar PDF" }, "status": { "reading": "Leyendo el PDF localmente…", - "loaded": "PDF listo: página(s) {pages}.", - "rendering": "Representando páginas seleccionadas localmente…", - "progress": "Renderizado de salida {completed} de {total} (página fuente {page})…", + "loaded": "PDF listo: páginas {pages}.", + "rendering": "Convirtiendo las páginas seleccionadas…", + "progress": "Convirtiendo {completed} de {total} (página {page})…", "progressLabel": "Progreso de conversión de imágenes PDF", "archive": "Preparando el archivo ZIP…", "saving": "Preparando la imagen…", @@ -624,7 +624,7 @@ export const es = { "oneFile": "Elija un PDF a la vez.", "unsupported": "Elija un archivo PDF.", "noFile": "Agregue un PDF antes de convertir.", - "protected": "Este PDF está protegido con contraseña o encriptado y no se puede convertir.", + "protected": "Este PDF está protegido con contraseña o cifrado y no se puede convertir.", "unreadable": "Este PDF está dañado o es ilegible.", "noPages": "Este PDF no contiene ninguna página.", "rangeRequired": "Ingrese al menos una página o rango.", @@ -635,13 +635,13 @@ export const es = { "scale": "Elija una escala de salida compatible.", "dimension": "Una página renderizada excedería el límite de dimensión de 16,384 píxeles.", "pixels": "Una página renderizada excedería el límite de 50 megapíxeles.", - "canvas": "Este navegador no puede crear el lienzo de imágenes.", + "canvas": "Este navegador no puede crear el lienzo necesario para la imagen.", "encoder": "Este navegador no pudo codificar el formato de imagen seleccionado.", "render": "No se pudo procesar una página PDF de forma segura.", "archiveLibrary": "No se pudo cargar la biblioteca de archivos local.", "archive": "No se pudo crear el archivo ZIP.", "failure": "No se pudo convertir el PDF. Pruebe con menos páginas o una escala más pequeña.", - "save": "No se pudo abrir la ubicación para guardar. Por favor inténtalo de nuevo." + "save": "No se pudo abrir la ubicación para guardar. Inténtelo de nuevo." } }, "organizePdf": { @@ -649,15 +649,15 @@ export const es = { "title": "Organizar PDF", "description": "Reordene, gire o elimine páginas sin cambiar el PDF original. Todo queda en este dispositivo.", "drop": { - "title": "Agregar un PDF", + "title": "Añadir PDF", "description": "Suelta un PDF aquí o usa el selector.", - "choose": "Elija PDF", - "localTitle": "Procesado en el dispositivo.", - "localBody": "El PDF no se envía fuera del dispositivo.", + "choose": "Elegir PDF", + "localTitle": "Procesado en este dispositivo", + "localBody": "El PDF no sale de este dispositivo", "privacyLink": "Cómo funciona la privacidad" }, "workspace": { - "title": "Espacio de trabajo de página", + "title": "Espacio de trabajo", "empty": "Aún no se ha seleccionado ningún PDF.", "summary": "{name} · {pages} páginas restantes · {size}", "changed": "Cambios no guardados", @@ -682,14 +682,14 @@ export const es = { }, "actions": { "reset": "Restablecer cambios", - "clear": "Borrar PDF", + "clear": "Quitar PDF", "export": "Exportar PDF organizado" }, "status": { "loading": "Cargando PDF localmente…", "preparing": "Preparando la página {completed} de {total}…", "progressLabel": "Progreso de preparación de miniaturas", - "ready": "PDF listo: página(s) {pages}.", + "ready": "PDF listo: páginas {pages}.", "moved": "Página {page} movida.", "rotated": "Página {page} rotada.", "removed": "Página {page} eliminada.", @@ -703,17 +703,17 @@ export const es = { "errors": { "oneFile": "Elija un PDF a la vez.", "unsupported": "Elija un archivo PDF.", - "protected": "Este PDF está protegido con contraseña o encriptado y no se puede organizar.", + "protected": "Este PDF está protegido con contraseña o cifrado y no se puede organizar.", "unreadable": "Este PDF está dañado o es ilegible.", "noPages": "Conserve al menos una página antes de exportar.", "failure": "El PDF no se pudo cargar ni procesar de forma segura.", - "save": "No se pudo abrir la ubicación para guardar. Por favor inténtalo de nuevo." + "save": "No se pudo abrir la ubicación para guardar. Inténtelo de nuevo." } }, "notFound": { "eyebrow": "404", - "title": "Esta página no está aquí.", - "description": "La dirección puede estar desactualizada o mal escrita.", - "action": "Regreso a casa" + "title": "Esta página no existe", + "description": "La dirección puede estar desactualizada o contener un error", + "action": "Volver al inicio" } }; diff --git a/js/locales/fr.js b/js/locales/fr.js index a77d125..b86c034 100644 --- a/js/locales/fr.js +++ b/js/locales/fr.js @@ -2,7 +2,7 @@ export const fr = { "metadata": { "home": { "title": "Secure Tools — Outils privés pour les fichiers du quotidien", - "description": "Convertissez, inspectez et traitez les fichiers en privé dans votre navigateur. Aucun téléchargement, compte, analyse ou suivi." + "description": "Convertissez, inspectez et traitez les fichiers en privé dans votre navigateur. Aucun envoi de fichier, compte, analyse ou suivi." }, "privacy": { "title": "Confidentialité — Secure Tools", @@ -22,7 +22,7 @@ export const fr = { }, "mergePdf": { "title": "Fusionner des PDF — Secure Tools", - "description": "Organisez et fusionnez entièrement les fichiers PDF dans votre navigateur sans les télécharger." + "description": "Organisez et fusionnez entièrement les fichiers PDF dans votre navigateur sans les envoyer." }, "splitPdf": { "title": "Fractionner un PDF — Secure Tools", @@ -63,7 +63,7 @@ export const fr = { }, "common": { "skip": "Passer au contenu principal", - "brandTagline": "Utilitaires privés, traités sur votre appareil", + "brandTagline": "Outils privés, traités sur votre appareil", "nav": { "tools": "Outils", "privacy": "Confidentialité", @@ -80,9 +80,9 @@ export const fr = { "footerStatement": "Les fichiers ne sont pas envoyés hors de l’appareil." }, "hero": { - "eyebrow": "Utilitaires de fichiers locaux d'abord", - "title": "Outils privés pour les fichiers du quotidien.", - "description": "Convertissez, inspectez et traitez les fichiers directement sur votre appareil. Aucun téléchargement. Aucun compte. Aucun suivi.", + "eyebrow": "Outils de fichiers locaux", + "title": "Des outils privés pour vos fichiers", + "description": "Convertissez, vérifiez et traitez vos fichiers directement sur votre appareil. Aucun envoi, aucun compte, aucun suivi", "primary": "Explorer les outils", "secondary": "Vérifier la confidentialité", "trust": "Traitement local · Open source · Pas de suivi", @@ -92,8 +92,8 @@ export const fr = { }, "tools": { "eyebrow": "Outils", - "title": "Commencez par ce sur quoi vous devez travailler.", - "description": "Parcourez les catégories ciblées, puis choisissez un outil. La disponibilité est toujours clairement indiquée.", + "title": "Choisissez le type de fichier à traiter", + "description": "Parcourez les catégories puis choisissez un outil. La disponibilité est indiquée clairement", "categories": { "pdf": "PDF", "image": "Image", @@ -108,10 +108,10 @@ export const fr = { "scan": "Transformez les numérisations en documents utiles.", "media": "Travaillez avec l'audio et la vidéo localement." }, - "browseCategory": "Parcourir la catégorie →", - "availableEyebrow": "Prêt à l'emploi", + "browseCategory": "Voir la catégorie →", + "availableEyebrow": "Disponible maintenant", "availableTitle": "Disponible maintenant", - "availableDescription": "Outils de production qui maintiennent le traitement des fichiers sur votre appareil.", + "availableDescription": "Outils qui traitent les fichiers directement sur votre appareil", "imagesToPdf": "Images au format PDF", "mergePdf": "Fusionner le PDF", "splitPdf": "Diviser le PDF", @@ -180,9 +180,9 @@ export const fr = { } }, "why": { - "eyebrow": "Pourquoi sécuriser les outils", - "title": "Un modèle de confidentialité plus simple.", - "description": "Secure Tools minimise ce qui doit être fiable en supprimant le serveur des flux de travail de traitement de fichiers principaux.", + "eyebrow": "Pourquoi Secure Tools", + "title": "Un modèle de confidentialité plus simple", + "description": "Secure Tools réduit ce qui doit être digne de confiance en retirant le serveur du traitement principal des fichiers", "local": { "title": "Traitement local", "body": "Les fichiers sont traités sur votre appareil, dans le navigateur." @@ -202,8 +202,8 @@ export const fr = { }, "verify": { "eyebrow": "Vérifier la confidentialité", - "title": "Ne nous croyez pas sur parole.", - "description": "Vous pouvez inspecter vous-même l'activité du réseau et confirmer qu'un fichier ne quitte jamais votre appareil.", + "title": "Vérifiez par vous-même", + "description": "Vous pouvez examiner l’activité réseau et confirmer que vos fichiers ne quittent pas l’appareil", "step1": "Ouvrez les outils de développement de votre navigateur.", "step2": "Sélectionnez l'onglet Réseau.", "step3": "Exécutez un outil sécurisé avec un exemple de fichier.", @@ -212,10 +212,10 @@ export const fr = { "architecture": "Architecture de confidentialité" }, "openSource": { - "eyebrow": "Source ouverte", - "title": "Ouvert par conception.", - "description": "Secure Tools est conçu de manière à ce que ses déclarations de confidentialité puissent être inspectées plutôt que simplement fiables.", - "action": "Parcourir la source" + "eyebrow": "Open source", + "title": "Ouvert par conception", + "description": "Les garanties de confidentialité de Secure Tools peuvent être vérifiées dans le code source, plutôt que simplement acceptées", + "action": "Voir le code source" }, "privacy": { "eyebrow": "Modèle de confidentialité", @@ -225,18 +225,18 @@ export const fr = { "localBody": "Les outils pris en charge lisent et transforment les fichiers dans la mémoire du navigateur. Le contenu du fichier n'est pas envoyé à un serveur Secure Tools.", "collectionTitle": "Aucun compte ni analyse", "collectionBody": "Le hub Web n'a pas de comptes d'utilisateurs, d'analyses, de suivi comportemental, de publicité ou de pixels de suivi.", - "storageTitle": "Stockage des préférences", + "storageTitle": "Préférences enregistrées", "storageBody": "localStorage est utilisé uniquement pour mémoriser vos sélections de langue et de thème sur cet appareil.", "linksTitle": "Liens externes", "linksBody": "Suivre un lien externe, tel que GitHub, quitte Secure Tools et est régi par les propres politiques de ce service." }, "about": { "eyebrow": "À propos", - "title": "Petits outils. Une confiance inspectable.", + "title": "De petits outils, une confiance vérifiable", "intro": "Secure Tools est une collection d'utilitaires de fichiers quotidiens axés sur la confidentialité, conçus pour s'exécuter localement dans le navigateur.", "purposeTitle": "Pourquoi ça existe", "purposeBody": "Les tâches de fichiers courantes ne devraient pas nécessiter le téléchargement de documents personnels sur un serveur inconnu ou la création d'un compte.", - "philosophyTitle": "Local d'abord et ouvert", + "philosophyTitle": "Local et ouvert par conception", "philosophyBody": "Le projet privilégie un code statique et compréhensible et un modèle de confidentialité que tout le monde peut vérifier via une inspection des sources et du réseau." }, "splitPdf": { @@ -246,9 +246,9 @@ export const fr = { "drop": { "title": "Ajouter un PDF", "description": "Déposez un PDF ici ou utilisez le sélecteur. La source est en lecture seule sur cet appareil.", - "choose": "Choisir PDF", - "localTitle": "Traitement sur cet appareil.", - "localBody": "Le PDF n’est pas envoyé hors de l’appareil.", + "choose": "Choisir un PDF", + "localTitle": "Traité sur cet appareil", + "localBody": "Le PDF ne quitte pas cet appareil", "privacyLink": "Comment fonctionne la confidentialité" }, "source": { @@ -258,7 +258,7 @@ export const fr = { "remove": "Supprimer le PDF source" }, "modes": { - "title": "Mode partagé", + "title": "Mode de division", "legend": "Choisissez comment diviser le PDF", "extract": { "title": "Extraire la plage de pages", @@ -283,7 +283,7 @@ export const fr = { }, "settings": { "title": "Paramètres de sortie", - "filename": "Nom de la base de sortie", + "filename": "Nom de base", "filenameHelp": "Les extensions et les numéros de page triables sont ajoutés automatiquement.", "autoDownload": "Téléchargement automatique après le fractionnement", "clearAfterSave": "Effacer la référence du fichier source après l'enregistrement", @@ -292,18 +292,18 @@ export const fr = { }, "actions": { "split": "Diviser et enregistrer", - "clear": "Effacer la source" + "clear": "Retirer le PDF" }, "status": { "reading": "Lecture du PDF localement…", - "loaded": "PDF prêt : page(s) {pages}.", + "loaded": "PDF prêt : pages {pages}.", "cleared": "PDF source effacé.", "preparing": "Préparation des pages…", "archive": "Création de l'archive ZIP…", "progress": "Création du fichier {completed} de {total}…", "progressLabel": "Progression du fractionnement du PDF", - "saved": "{name} enregistré avec le(s) fichier(s) de sortie {count}.", - "downloaded": "{name} téléchargé avec le(s) fichier(s) de sortie {count}.", + "saved": "{name} enregistré avec le(s) fichiers de sortie {count}.", + "downloaded": "{name} téléchargé avec le(s) fichiers de sortie {count}.", "savedAndCleared": "Résultat du fractionnement enregistré et référence source effacée.", "saveCancelled": "Sauvegarde annulée. Aucun fichier n'a été écrit." }, @@ -332,15 +332,15 @@ export const fr = { "title": "Fusionner le PDF", "description": "Organisez les fichiers PDF et combinez chaque page en un seul document. Les fichiers restent sur cet appareil.", "drop": { - "title": "Ajoutez vos PDF", + "title": "Ajouter des PDF", "description": "Déposez les fichiers PDF ici ou utilisez le sélecteur. L'ordre de la file d'attente devient l'ordre des documents.", - "choose": "Choisir les PDF", - "localTitle": "Traitement sur cet appareil.", - "localBody": "Les PDF ne sont pas envoyés hors de l’appareil.", + "choose": "Choisir des PDF", + "localTitle": "Traité sur cet appareil", + "localBody": "Les PDF ne quittent pas cet appareil", "privacyLink": "Comment fonctionne la confidentialité" }, "queue": { - "title": "Commande PDF", + "title": "Ordre des PDF", "count": "PDF : {count}", "empty": "Aucun PDF ajouté.", "meta": "{size} · Pages {pages}", @@ -349,7 +349,7 @@ export const fr = { "remove": "Supprimer le PDF" }, "settings": { - "title": "Fusionner les paramètres", + "title": "Options de fusion", "filename": "Nom du fichier de sortie", "filenameHelp": "Une extension .pdf est ajoutée automatiquement.", "autoDownload": "Télécharger automatiquement après la fusion", @@ -359,12 +359,12 @@ export const fr = { "actions": { "merge": "Fusionner et enregistrer le PDF", "add": "Ajouter plus de PDF", - "clear": "Tout effacer" + "clear": "Tout retirer" }, "status": { "validating": "Vérification des PDF localement…", "added": "Ajout du ou des fichiers PDF {count}.", - "addedWithRejected": "Ajout du ou des fichiers PDF {added} ; fichier(s) {rejected} ignoré(s) non pris en charge ou illisibles.", + "addedWithRejected": "Ajout du ou des fichiers PDF {added} ; fichiers {rejected} ignoré(s) non pris en charge ou illisibles.", "removed": "PDF supprimé.", "reordered": "Commande PDF mise à jour.", "cleared": "Liste PDF effacée.", @@ -391,11 +391,11 @@ export const fr = { "title": "Images au format PDF", "description": "Organisez les images JPEG, PNG et WebP et enregistrez-les en un seul PDF. Tout se passe sur cet appareil.", "drop": { - "title": "Ajoutez vos images", + "title": "Ajouter des images", "description": "Déposez les fichiers ici ou utilisez le sélecteur. JPEG, PNG et WebP peuvent être mélangés.", "choose": "Choisir des images", - "localTitle": "Traitement sur cet appareil.", - "localBody": "Les images ne sont pas envoyées hors de l’appareil.", + "localTitle": "Traité sur cet appareil", + "localBody": "Les images ne quittent pas cet appareil", "privacyLink": "Comment fonctionne la confidentialité" }, "queue": { @@ -436,11 +436,11 @@ export const fr = { "actions": { "generate": "Générer et enregistrer un PDF", "add": "Ajouter plus d'images", - "clear": "Tout effacer" + "clear": "Tout retirer" }, "status": { "added": "Ajout d'images {count}.", - "addedWithRejected": "Ajout d'images prises en charge par {added} ; fichier(s) non pris en charge {rejected} ignoré.", + "addedWithRejected": "Ajout d'images prises en charge par {added} ; fichiers non pris en charge {rejected} ignoré.", "addedWithReasons": "Ajout d'une ou plusieurs images {added}. Rejeté : {reasons}", "rejectedItem": "{name} : {reason}", "removed": "Image supprimée.", @@ -473,28 +473,28 @@ export const fr = { }, "pdfMetadata": { "eyebrow": "Outil de confidentialité PDF", - "title": "Inspecteur et nettoyeur de métadonnées PDF", + "title": "Métadonnées PDF", "description": "Examinez les métadonnées courantes du document et créez une copie nettoyée sans télécharger votre PDF.", "scope": { - "title": "Portée prise en charge", - "body": "Cet outil couvre les champs d'informations sur le document standard indiqués ci-dessous. Il ne s'agit pas d'un épurateur médico-légal et ne prétend pas supprimer les paquets XMP, les pièces jointes, les annotations, le contenu caché ou toute structure d'identification.", + "title": "Périmètre pris en charge", + "body": "Cet outil couvre les champs standard d’informations du document affichés ci-dessous. Il ne s’agit pas d’un outil de nettoyage forensique complet et il ne prétend pas supprimer les données XMP, les pièces jointes, les annotations, le contenu masqué ni toute autre information potentiellement identifiable.", "cleaning": "Le nettoyage supprime uniquement les métadonnées de document prises en charge répertoriées ci-dessus." }, "drop": { "title": "Ajouter un PDF", "description": "Déposez un PDF ici ou utilisez le sélecteur. Le choix d'un autre fichier remplace la source actuelle.", - "choose": "Choisir PDF", - "localTitle": "Traitement sur cet appareil.", - "localBody": "Le PDF n’est pas envoyé hors de l’appareil.", + "choose": "Choisir un PDF", + "localTitle": "Traité sur cet appareil", + "localBody": "Le PDF ne quitte pas cet appareil", "privacyLink": "Comment fonctionne la confidentialité" }, "source": { "title": "PDF source", "empty": "Aucun PDF sélectionné pour le moment.", - "summary": "{name} · {size} · Page(s) {pages} · Champ(s) pris en charge {fields} présent(s)" + "summary": "{name} · {size} · Pages {pages} · champs pris en charge {fields} présent(s)" }, "inspector": { - "title": "Métadonnées du document détectées", + "title": "Métadonnées détectées", "help": "Sélectionnez les champs à supprimer. Les valeurs de créateur, de producteur, d'auteur et de date peuvent identifier une personne, un appareil ou une application.", "caption": "Champs de métadonnées pris en charge trouvés dans le PDF", "remove": "Supprimer", @@ -509,8 +509,8 @@ export const fr = { "author": "Auteur", "subject": "Sujet", "keywords": "Mots-clés", - "creator": "Application Créateur", - "producer": "Producteur de PDF", + "creator": "Application de création", + "producer": "Producteur PDF", "creationDate": "Date de création", "modificationDate": "Date de modification" }, @@ -522,22 +522,22 @@ export const fr = { "pdfFile": "Document PDF" }, "actions": { - "clearSource": "Effacer le PDF", - "selectAll": "Sélectionner tous les présents", - "clearSelection": "Effacer la sélection", + "clearSource": "Retirer le PDF", + "selectAll": "Tout sélectionner", + "clearSelection": "Désélectionner", "removeAll": "Supprimer tous les champs pris en charge", - "clean": "Nettoyer et enregistrer le PDF" + "clean": "Nettoyer et enregistrer" }, "comparison": { - "title": "Vérifié avant et après", + "title": "Résultat vérifié", "before": "Avant", - "after": "Après copie enregistrée", - "cleared": "Vérifié : les champs sélectionnés {count} sont absents de la copie sérialisée.", - "retained": "Avertissement : les champs sélectionnés {count} restent dans la copie sérialisée." + "after": "Copie enregistrée", + "cleared": "Vérifié : les champs sélectionnés {count} sont absents de la copie enregistrée.", + "retained": "Avertissement : les champs sélectionnés {count} restent dans la copie enregistrée." }, "status": { "reading": "Lecture des métadonnées localement…", - "loaded": "Champ(s) de métadonnées pris en charge par {count} trouvé(s) dans un PDF de page {pages}.", + "loaded": "champs de métadonnées pris en charge par {count} trouvé(s) dans un PDF de page {pages}.", "noneFound": "Aucune métadonnée d'informations sur le document prise en charge n'a été trouvée dans ce PDF de page {pages}.", "cleared": "PDF source effacé.", "cleaning": "Suppression des métadonnées sélectionnées et vérification des octets enregistrés…", @@ -564,9 +564,9 @@ export const fr = { "drop": { "title": "Ajouter un PDF", "description": "Déposez un PDF ici ou utilisez le sélecteur. Choisir un nouveau PDF remplace en toute sécurité la source actuelle.", - "choose": "Choisir PDF", - "localTitle": "Traitement sur cet appareil.", - "localBody": "Le PDF n’est pas envoyé hors de l’appareil.", + "choose": "Choisir un PDF", + "localTitle": "Traité sur cet appareil", + "localBody": "Le PDF ne quitte pas cet appareil", "privacyLink": "Comment fonctionne la confidentialité" }, "source": { @@ -591,7 +591,7 @@ export const fr = { }, "settings": { "title": "Paramètres d'image", - "filename": "Nom de la base de sortie", + "filename": "Nom de base", "filenameHelp": "Les numéros de page et les extensions de fichiers sont ajoutés automatiquement.", "format": "Format d'image", "quality": "Qualité d'image", @@ -604,11 +604,11 @@ export const fr = { "actions": { "convert": "Convertir et enregistrer", "cancel": "Annuler", - "clear": "Effacer la source" + "clear": "Retirer le PDF" }, "status": { "reading": "Lecture du PDF localement…", - "loaded": "PDF prêt : page(s) {pages}.", + "loaded": "PDF prêt : pages {pages}.", "rendering": "Rendu local des pages sélectionnées…", "progress": "Sortie de rendu {completed} de {total} (page source {page})…", "progressLabel": "Progression de la conversion de l'image PDF", @@ -651,15 +651,15 @@ export const fr = { "drop": { "title": "Ajouter un PDF", "description": "Déposez un PDF ici ou utilisez le sélecteur.", - "choose": "Choisir PDF", - "localTitle": "Traitement sur cet appareil.", - "localBody": "Le PDF n’est pas envoyé hors de l’appareil.", + "choose": "Choisir un PDF", + "localTitle": "Traité sur cet appareil", + "localBody": "Le PDF ne quitte pas cet appareil", "privacyLink": "Comment fonctionne la confidentialité" }, "workspace": { - "title": "Espace de travail des pages", + "title": "Espace de travail", "empty": "Aucun PDF sélectionné pour le moment.", - "summary": "{name} · {pages} page(s) restante(s) · {size}", + "summary": "{name} · {pages} pages restante(s) · {size}", "changed": "Modifications non enregistrées", "gridLabel": "Pages PDF" }, @@ -682,14 +682,14 @@ export const fr = { }, "actions": { "reset": "Réinitialiser les modifications", - "clear": "Effacer le PDF", + "clear": "Retirer le PDF", "export": "Exporter un PDF organisé" }, "status": { "loading": "Chargement du PDF localement…", "preparing": "Préparation de la page {completed} de {total}…", "progressLabel": "Progression de la préparation des vignettes", - "ready": "PDF prêt : page(s) {pages}.", + "ready": "PDF prêt : pages {pages}.", "moved": "Page {page} déplacée.", "rotated": "Page {page} pivotée.", "removed": "Page {page} supprimée.", @@ -712,8 +712,8 @@ export const fr = { }, "notFound": { "eyebrow": "404", - "title": "Cette page n'est pas ici.", - "description": "L'adresse est peut-être obsolète ou mal saisie.", + "title": "Cette page n’existe pas", + "description": "L’adresse est peut-être obsolète ou comporte une erreur", "action": "Retour à la maison" } }; diff --git a/js/locales/ja.js b/js/locales/ja.js index 22fe11a..2d93aa4 100644 --- a/js/locales/ja.js +++ b/js/locales/ja.js @@ -1,74 +1,74 @@ export const ja = { "metadata": { "home": { - "title": "安全なツール — 日常的なファイルのためのプライベート ツール", - "description": "ブラウザでファイルをプライベートに変換、検査、処理します。アップロード、アカウント、分析、追跡はありません。" + "title": "Secure Tools — 日常のファイルを安全に扱うプライバシーツール", + "description": "ファイルをアップロードせず、ブラウザ上で変換・確認・処理できます。アカウント、アクセス解析、追跡はありません。" }, "privacy": { - "title": "プライバシー — 安全なツール", - "description": "Secure Tools がファイル処理をローカルに維持し、追跡を回避し、ローカル ストレージを環境設定のみに使用する方法。" + "title": "プライバシー — Secure Tools", + "description": "Secure Toolsがファイルを端末内で処理し、追跡を行わず、言語とテーマの設定だけを保存する仕組みを紹介します。" }, "about": { - "title": "について — 安全なツール", - "description": "Secure Tools が、単に信頼するだけでなく検査できるオープンなローカルファーストのファイル ユーティリティを構築する理由。" + "title": "Secure Toolsについて", + "description": "Secure Toolsが、ただ信頼するのではなく自分で確認できるオープンソースのローカルファイルツールを目指す理由を紹介します。" }, "notFound": { - "title": "ページが見つかりません — 安全なツール", - "description": "要求されたセキュア ツール ページが見つかりませんでした。" + "title": "ページが見つかりません — Secure Tools", + "description": "指定されたSecure Toolsのページが見つかりません。" }, "imageToPdf": { - "title": "画像を PDF に変換 — 安全なツール", - "description": "JPEG、PNG、WebP 画像をブラウザ内で完全に PDF に結合します。" + "title": "画像をPDFに変換 — Secure Tools", + "description": "JPEG、PNG、WebP画像を端末内で処理し、1つのPDFにまとめます。" }, "mergePdf": { - "title": "PDF を結合 — 安全なツール", - "description": "PDF ファイルをアップロードせずにブラウザ内で完全に配置および結合します。" + "title": "PDFを結合 — Secure Tools", + "description": "PDFをアップロードせず、選んだ順番で1つのPDFに結合します。" }, "splitPdf": { - "title": "PDF の分割 — 安全なツール", - "description": "ソースをアップロードせずに、ブラウザーでローカルに PDF ページを抽出または分割します。" + "title": "PDFを分割 — Secure Tools", + "description": "PDFをアップロードせず、必要なページの抽出や分割を端末内で行います。" }, "organizePdf": { - "title": "PDF を整理する — 安全なツール", - "description": "PDF ページをブラウザーでローカルに並べ替え、回転、削除します。" + "title": "PDFを整理 — Secure Tools", + "description": "PDFページの並べ替え、回転、削除を端末内で行います。" }, "pdfToImages": { - "title": "PDF から画像へ — 安全なツール", - "description": "ブラウザーでローカルに PDF ページを PNG、JPEG、または WebP 画像に変換します。" + "title": "PDFを画像に変換 — Secure Tools", + "description": "PDFページをPNG、JPEG、WebP画像に端末内で変換します。" }, "pdfMetadata": { - "title": "PDF メタデータ インスペクター & クリーナー — 安全なツール", - "description": "サポートされている PDF メタデータをブラウザーでローカルに検査して削除します。" + "title": "PDFメタデータの確認・削除 — Secure Tools", + "description": "対応しているPDFメタデータを端末内で確認し、選んで削除できます。" }, "pdfCategory": { - "title": "PDF ツール — 安全なツール", - "description": "ブラウザーでローカルに PDF ドキュメントを作成および整理します。" + "title": "PDFツール — Secure Tools", + "description": "PDFの作成・結合・分割・整理を端末内で行えます。" }, "imageCategory": { - "title": "画像ツール — 安全なツール", - "description": "ブラウザ内でローカルに実行するように設計されたプライベート イメージ ツール。" + "title": "画像ツール — Secure Tools", + "description": "ブラウザ上で端末内処理する、プライバシー重視の画像ツールです。" }, "privacyCategory": { - "title": "プライバシー ツール — 安全なツール", - "description": "ソース ファイルをアップロードせずに、隠しファイルの詳細を検査して削除します。" + "title": "プライバシーツール — Secure Tools", + "description": "元のファイルをアップロードせず、ファイルに含まれる情報を確認・削除します。" }, "scanCategory": { - "title": "スキャンおよび OCR ツール — 安全なツール", - "description": "プライベート文書用のローカル スキャンおよびテキスト認識ツール。" + "title": "スキャン・OCRツール — Secure Tools", + "description": "個人文書を端末内で扱うスキャン・文字認識ツールです。" }, "mediaCategory": { - "title": "メディア ツール — 安全なツール", - "description": "Secure Tools のローカル オーディオおよびビデオ ユーティリティ。" + "title": "メディアツール — Secure Tools", + "description": "音声や動画を端末内で処理するSecure Toolsのユーティリティです。" } }, "common": { - "skip": "メインコンテンツにスキップ", - "brandTagline": "プライベート ユーティリティ、デバイス上で処理されます", + "skip": "メインコンテンツへ移動", + "brandTagline": "端末内で処理するプライバシーツール", "nav": { "tools": "ツール", "privacy": "プライバシー", - "source": "ソース", - "about": "について" + "source": "ソースコード", + "about": "概要" }, "language": "言語", "theme": "テーマ", @@ -77,335 +77,335 @@ export const ja = { "light": "ライト", "dark": "ダーク" }, - "footerStatement": "ファイルが外部へ送信されることはありません。" + "footerStatement": "ファイルはこの端末の外へ送信されません" }, "hero": { - "eyebrow": "ローカルファーストのファイルユーティリティ", - "title": "日常のファイルのためのプライベートツール。", - "description": "デバイス上でファイルを直接変換、検査、処理します。アップロードはありません。アカウントがありません。追跡はありません。", - "primary": "探索ツール", - "secondary": "プライバシーを確認する", - "trust": "ローカル処理 · オープンソース · 追跡なし", - "proofLabel": "デバイスに残ったもの", - "proofValue": "何もありません", - "proofNote": "コアツールはブラウザメモリ内のファイルコンテンツを処理します。" + "eyebrow": "ローカルファーストのファイルツール", + "title": "日常のファイルを、もっと安全に", + "description": "ファイルをこの端末上で変換・確認・処理できます。アップロードも、アカウントも、追跡もありません", + "primary": "ツールを見る", + "secondary": "プライバシーを確認", + "trust": "端末内処理 · オープンソース · 追跡なし", + "proofLabel": "端末の外へ送信されるファイル", + "proofValue": "なし", + "proofNote": "主要なツールはブラウザのメモリ上でファイルを処理します" }, "tools": { "eyebrow": "ツール", - "title": "取り組むべきことから始めましょう。", - "description": "焦点を当てたカテゴリを参照して、ツールを選択します。可用性は常に明確に表示されます。", + "title": "必要な作業からツールを選べます", + "description": "カテゴリから目的に合うツールを選んでください。利用できる機能はわかりやすく表示します", "categories": { "pdf": "PDF", "image": "画像", "privacy": "プライバシー", - "scan": "スキャンと OCR", + "scan": "スキャン・OCR", "media": "メディア" }, "categoryDescriptions": { - "pdf": "ドキュメントを作成、結合、整理します。", - "image": "日常の画像を変換して準備します。", - "privacy": "隠しファイルの詳細を検査して消去します。", - "scan": "スキャンを有用なドキュメントに変換します。", - "media": "オーディオとビデオをローカルで操作します。" - }, - "browseCategory": "カテゴリを見る→", - "availableEyebrow": "すぐに使用できます", - "availableTitle": "発売中", - "availableDescription": "デバイス上でファイル処理を続ける制作ツール。", - "imagesToPdf": "画像を PDF に変換", - "mergePdf": "PDF を結合", - "splitPdf": "PDFの分割", - "organizePdf": "PDF を整理する", - "pdfToImages": "PDF から画像へ", - "pdfMetadata": "PDF メタデータ", - "imageConverter": "画像コンバーター", - "imageCompressor": "画像圧縮器", - "imageResizer": "画像リサイザー", - "metadataInspector": "メタデータインスペクター", - "metadataCleaner": "メタデータ クリーナー", - "available": "在庫あり", - "comingSoon": "近日公開予定" + "pdf": "PDFの作成・結合・整理", + "image": "画像の変換や調整", + "privacy": "ファイルに含まれる情報の確認・削除", + "scan": "スキャン画像を使いやすい文書へ変換", + "media": "音声や動画を端末内で処理" + }, + "browseCategory": "ツールを見る →", + "availableEyebrow": "すぐ使えるツール", + "availableTitle": "利用可能", + "availableDescription": "ファイルを外部へ送らず、この端末上で処理します", + "imagesToPdf": "画像をPDFに", + "mergePdf": "PDFを結合", + "splitPdf": "PDFを分割", + "organizePdf": "PDFを整理", + "pdfToImages": "PDFを画像に", + "pdfMetadata": "PDFメタデータ", + "imageConverter": "画像変換", + "imageCompressor": "画像圧縮", + "imageResizer": "画像サイズ変更", + "metadataInspector": "メタデータ確認", + "metadataCleaner": "メタデータ削除", + "available": "利用可能", + "comingSoon": "準備中" }, "categories": { - "eyebrow": "工具カテゴリ", + "eyebrow": "ツールカテゴリ", "nav": { "pdf": "PDF", "image": "画像", "privacy": "プライバシー", - "scan": "スキャンと OCR", + "scan": "スキャン・OCR", "media": "メディア" }, - "localNote": "利用可能なツールは、ブラウザのメモリ内のファイルの内容を処理します。計画された項目は明確にマークされます。", - "plannedNote": "このカテゴリーは企画中です。使用できないアイテムは動作しているものとして表示されません。", + "localNote": "利用可能なツールはブラウザのメモリ上でファイルを処理します。準備中の機能は別に表示します。", + "plannedNote": "このカテゴリは準備中です。まだ利用できない機能は「準備中」と表示します。", "pdf": { - "title": "PDF ツール", - "description": "プライベート ファイルをアップロードせずに、PDF ドキュメントをローカルで作成および整理します。", - "imagesToPdf": "JPEG、PNG、WebP 画像を 1 つの PDF に配置します。", - "merge": "PDF ファイルを選択した順序で結合します。", - "split": "選択したページを抽出するか、ドキュメントをローカル ファイルに分割します。", - "rotateTitle": "PDF を回転", - "rotate": "ドキュメントの品質を変更せずにページの向きを修正します。", - "organize": "1 つの PDF からページをプレビュー、並べ替え、回転、削除します。", - "toImages": "注文した PDF ページを PNG、JPEG、または WebP 画像に変換します。", - "metadata": "共通のドキュメントのメタデータを検査し、検証済みのクリーンなコピーを作成します。" + "title": "PDFツール", + "description": "個人ファイルをアップロードせず、PDFを端末内で作成・整理できます", + "imagesToPdf": "JPEG、PNG、WebP画像を好きな順番で1つのPDFにまとめます", + "merge": "PDFを選んだ順番で1つに結合します", + "split": "必要なページを抽出したり、PDFを複数のファイルに分割したりできます", + "rotateTitle": "PDFを回転", + "rotate": "画質を変えずにページの向きを調整します", + "organize": "PDFのページをプレビューし、並べ替え・回転・削除できます", + "toImages": "PDFページをPNG、JPEG、WebP画像に変換します", + "metadata": "一般的な文書メタデータを確認し、削除後の内容を検証したコピーを作成します" }, "image": { "title": "画像ツール", - "description": "日常の画像をブラウザ上でプライベートに準備します。", - "convert": "一般的な画像形式をローカルで変換します。", - "compress": "明確な品質管理によりファイル サイズを削減します。", - "resize": "画像のサイズを正確なピクセル寸法に変更します。" + "description": "画像ファイルをブラウザ上でプライベートに処理します", + "convert": "一般的な画像形式を端末内で変換します", + "compress": "画質を調整しながらファイルサイズを小さくします", + "resize": "画像を指定したピクセルサイズに変更します" }, "privacy": { - "title": "プライバシー ツール", - "description": "ソースファイルを共有せずに、隠しファイルの詳細を検査して削除します。", - "inspect": "ファイルにどのような識別情報が含まれているかを確認します。", - "clean": "サポートされているメタデータを削除してコピーを作成します。" + "title": "プライバシーツール", + "description": "元のファイルを共有せず、ファイルに含まれる情報を確認・削除します", + "inspect": "ファイルに含まれる識別情報を確認します", + "clean": "対応しているメタデータを削除したコピーを作成します" }, "scan": { - "title": "スキャンおよび OCR ツール", - "description": "ソース素材をデバイスに保存しながら、スキャンを有用なドキュメントに変換します。", - "ocrTitle": "画像をテキストに変換", - "ocr": "画像からテキストをローカルで認識します。", - "documentTitle": "ドキュメントスキャナー", - "document": "撮影したドキュメントをトリミングしてクリーニングします。" + "title": "スキャン・OCRツール", + "description": "元の資料を端末内に保ったまま、スキャン画像を使いやすい文書に変換します", + "ocrTitle": "画像からテキスト", + "ocr": "画像内の文字を端末内で認識します", + "documentTitle": "文書スキャナー", + "document": "撮影した文書を切り抜き、見やすく補正します" }, "media": { "title": "メディアツール", - "description": "ローカル処理用に設計された小さなオーディオおよびビデオ ユーティリティ。", - "audioTitle": "オーディオコンバータ", - "audio": "デバイス上の一般的なオーディオ形式を変換します。", - "videoTitle": "ビデオトリム", - "video": "サーバーに送信せずにクリップをトリミングします。" + "description": "音声や動画を端末内で処理するシンプルなツールです", + "audioTitle": "音声変換", + "audio": "一般的な音声形式を端末内で変換します", + "videoTitle": "動画をトリミング", + "video": "サーバーへ送信せず、必要な区間を切り出します" } }, "why": { - "eyebrow": "ツールを安全にする理由", - "title": "よりシンプルなプライバシー モデル。", - "description": "Secure Tools は、コアのファイル処理ワークフローからサーバーを削除することで、信頼する必要があるものを最小限に抑えます。", + "eyebrow": "Secure Toolsを選ぶ理由", + "title": "シンプルで透明なプライバシー設計", + "description": "主要なファイル処理からサーバーを外し、信頼しなければならない対象を減らしています", "local": { - "title": "ローカル処理", - "body": "ファイルはデバイスのブラウザ内で処理されます。" + "title": "端末内処理", + "body": "ファイルはブラウザを使って、この端末上で処理されます" }, "accounts": { - "title": "アカウントがありません", - "body": "基本的なユーティリティは、登録や個人プロファイルなしで動作します。" + "title": "アカウント不要", + "body": "登録や個人プロフィールを作らなくても基本ツールを利用できます" }, "tracking": { "title": "追跡なし", - "body": "分析、行動追跡、広告ピクセルはありません。" + "body": "アクセス解析、行動トラッキング、広告用ピクセルは使用しません" }, "network": { - "title": "最小限のネットワーク依存性", - "body": "コア処理は外部ランタイム サービスに依存しません。" + "title": "ネットワーク依存を最小限に", + "body": "主要な処理は外部の実行サービスに依存しません" } }, "verify": { - "eyebrow": "プライバシーを確認する", - "title": "私たちの言葉を鵜呑みにしないでください。", - "description": "ネットワークアクティビティを自分で検査し、ファイルがデバイスから決して流出していないことを確認できます。", - "step1": "ブラウザの開発者ツールを開きます。", - "step2": "[ネットワーク]タブを選択します。", - "step3": "サンプル ファイルを使用して安全なツールを実行します。", - "step4": "ファイルがアップロードされていないことを確認してください。", - "source": "ソースを見る", - "architecture": "プライバシー アーキテクチャ" + "eyebrow": "プライバシーを確認", + "title": "自分で確認できます", + "description": "ブラウザのネットワーク通信を確認すれば、ファイルが外部へ送信されていないことを自分で確かめられます", + "step1": "ブラウザの開発者ツールを開きます", + "step2": "「ネットワーク」タブを開きます", + "step3": "サンプルファイルでSecure Toolsを実行します", + "step4": "ファイルをアップロードする通信がないことを確認します", + "source": "ソースコードを見る", + "architecture": "プライバシー設計" }, "openSource": { "eyebrow": "オープンソース", - "title": "設計によりオープンします。", - "description": "セキュア ツールは、単に信頼するのではなく、プライバシーの主張を検査できるように構築されています。", - "action": "ソースを参照する" + "title": "設計からオープンに", + "description": "Secure Toolsのプライバシー設計は、ソースコードを見て自分で確認できます", + "action": "ソースコードを見る" }, "privacy": { "eyebrow": "プライバシーモデル", - "title": "プライバシーはアーキテクチャ上の選択です。", - "intro": "Secure Tools は、ファイルの内容をブラウザ内でローカルに処理するように設計されています。このページでは現在の技術モデルについて説明しており、未完成の機能について約束するものではありません。", - "localTitle": "ローカルファイル処理", - "localBody": "サポートされているツールは、ブラウザーのメモリ内のファイルを読み取り、変換します。ファイルの内容は Secure Tools サーバーには送信されません。", - "collectionTitle": "アカウントも分析もありません", - "collectionBody": "Web ハブには、ユーザー アカウント、分析、行動追跡、広告、または追跡ピクセルがありません。", - "storageTitle": "優先ストレージ", - "storageBody": "localStorage は、このデバイスでの言語とテーマの選択を記憶するためにのみ使用されます。", + "title": "プライバシーは設計から", + "intro": "Secure Toolsは、ファイル内容をブラウザ上で端末内処理するよう設計されています。このページでは、未提供の機能ではなく現在の技術的な仕組みを説明します。", + "localTitle": "端末内でファイルを処理", + "localBody": "対応しているツールはブラウザのメモリ上でファイルを読み込み、変換します。ファイル内容がSecure Toolsのサーバーへ送信されることはありません。", + "collectionTitle": "アカウントもアクセス解析もなし", + "collectionBody": "ユーザーアカウント、アクセス解析、行動追跡、広告、トラッキングピクセルは使用しません。", + "storageTitle": "設定の保存", + "storageBody": "localStorageは、この端末で選んだ言語とテーマを記憶するためだけに使用します。", "linksTitle": "外部リンク", - "linksBody": "GitHub などの外部リンクをたどると、Secure Tools から離れ、そのサービス独自のポリシーによって管理されます。" + "linksBody": "GitHubなどの外部リンクを開くとSecure Toolsを離れ、以降はリンク先サービスのポリシーが適用されます。" }, "about": { - "eyebrow": "について", - "title": "小さな工具。検査可能な信頼。", - "intro": "Secure Tools は、ブラウザ内でローカルに実行するように構築された、プライバシーを最優先した日常的なファイル ユーティリティのコレクションです。", - "purposeTitle": "存在理由", - "purposeBody": "一般的なファイルタスクでは、個人文書を未知のサーバーにアップロードしたり、アカウントを作成したりする必要があってはなりません。", - "philosophyTitle": "ローカルファーストでオープンな", - "philosophyBody": "このプロジェクトは、ソースとネットワークの検査を通じて誰でも検証できる、静的で理解しやすいコードとプライバシー モデルを優先します。" + "eyebrow": "Secure Toolsについて", + "title": "小さなツール、確かめられる信頼", + "intro": "Secure Toolsは、日常のファイルをブラウザ上で端末内処理する、プライバシー重視のツール集です。", + "purposeTitle": "作った理由", + "purposeBody": "ちょっとしたファイル作業のために、個人文書を知らないサーバーへアップロードしたり、アカウントを作ったりする必要はないと考えています。", + "philosophyTitle": "ローカルファーストと透明性", + "philosophyBody": "ソースコードやネットワーク通信を誰でも確認できるよう、シンプルな静的構成と透明なプライバシー設計を大切にしています。" }, "splitPdf": { "eyebrow": "PDFツール", - "title": "PDFの分割", - "description": "選択したページを抽出するか、1 つの PDF を予測可能なローカル ファイルに分割します。", + "title": "PDFを分割", + "description": "必要なページを抽出したり、PDFを複数のローカルファイルに分割したりできます", "drop": { - "title": "PDF を 1 つ追加", - "description": "PDF をここにドロップするか、ピッカーを使用します。このデバイスではソースは読み取り専用です。", - "choose": "PDF を選択してください", - "localTitle": "このデバイス上で処理します。", - "localBody": "PDFが外部へ送信されることはありません。", + "title": "PDFを追加", + "description": "PDFをここにドロップするか、ファイルを選択してください。元のファイルはこの端末内でのみ読み取られます", + "choose": "PDFを選択", + "localTitle": "この端末で処理します", + "localBody": "PDFは外部へ送信されません", "privacyLink": "プライバシーの仕組み" }, "source": { - "title": "ソース PDF", - "empty": "PDF がまだ選択されていません。", - "meta": "{size} · {pages} ページ", - "remove": "ソース PDF を削除" + "title": "元のPDF", + "empty": "PDFが選択されていません", + "meta": "{size} · {pages}ページ", + "remove": "元のPDFを削除" }, "modes": { - "title": "スプリットモード", - "legend": "PDF の分割方法を選択します", + "title": "分割方法", + "legend": "PDFの分割方法を選択", "extract": { - "title": "ページ範囲の抽出", - "body": "入力されたページ順序どおりに 1 つの PDF を作成します。" + "title": "ページを指定して抽出", + "body": "入力した順番でページをまとめ、1つのPDFを作成します" }, "every": { - "title": "ページごとに分割", - "body": "ZIP アーカイブ内のページごとに 1 つの PDF を作成します。" + "title": "1ページずつ分割", + "body": "各ページを1つずつPDFにしてZIPファイルにまとめます" }, "interval": { - "title": "一定間隔", - "body": "固定ページ数でグループ化された PDF を作成します。" + "title": "指定ページ数ごとに分割", + "body": "指定したページ数ごとにPDFを作成します" } }, "range": { "label": "抽出するページ", - "help": "例: 1-3、5、2、4、7、または 5、1、3。重複は保存されます。" + "help": "例:1-3, 5, 2,4,7 または 5,1,3。重複したページもそのまま残ります" }, "interval": { - "label": "ファイルあたりのページ数", - "help": "最終ファイルには残りのページが含まれます。" + "label": "1ファイルあたりのページ数", + "help": "余ったページは最後のファイルに含まれます" }, "settings": { "title": "出力設定", - "filename": "出力ベース名", - "filenameHelp": "拡張子とソート可能なページ番号は自動的に追加されます。", - "autoDownload": "分割後自動ダウンロード", - "clearAfterSave": "保存後にソースファイル参照をクリアします", + "filename": "出力ファイルの基本名", + "filenameHelp": "拡張子と並べ替えやすいページ番号は自動で追加されます", + "autoDownload": "分割後に自動でダウンロード", + "clearAfterSave": "保存後に元のファイル情報を消去", "pdfFile": "PDFドキュメント", - "zipFile": "ZIP アーカイブ" + "zipFile": "ZIPファイル" }, "actions": { "split": "分割して保存", - "clear": "ソースをクリア" + "clear": "PDFを消去" }, "status": { - "reading": "PDF をローカルで読んでいます…", - "loaded": "PDF が準備完了: {pages} ページ。", - "cleared": "ソース PDF がクリアされました。", - "preparing": "ページを準備しています…", - "archive": "ZIP アーカイブを作成しています…", - "progress": "{total} のファイル {completed} を作成しています…", + "reading": "PDFを読み込み中…", + "loaded": "PDFの準備ができました:{pages}ページ", + "cleared": "PDFを消去しました", + "preparing": "ページを準備中…", + "archive": "ZIPファイルを作成中…", + "progress": "{total}個中{completed}個目のファイルを作成中…", "progressLabel": "PDF 分割の進行状況", - "saved": "{name} を {count} 出力ファイルとともに保存しました。", - "downloaded": "{name} を {count} 出力ファイルとともにダウンロードしました。", - "savedAndCleared": "分割結果が保存され、ソース参照がクリアされました。", - "saveCancelled": "保存がキャンセルされました。ファイルは書き込まれませんでした。" + "saved": "{count}個の出力ファイルを含む{name}を保存しました", + "downloaded": "{count}個の出力ファイルを含む{name}をダウンロードしました", + "savedAndCleared": "分割結果を保存し、元のファイル情報を消去しました", + "saveCancelled": "保存をキャンセルしました。ファイルは作成されていません" }, "errors": { - "oneFile": "一度に 1 つの PDF を選択します。", - "unsupported": "PDF ファイルを選択してください。 .pdf 拡張子は、ブラウザがファイル タイプを提供しない場合にのみ受け入れられます。", - "noFile": "分割する前に PDF を 1 つ追加します。", - "protected": "{name} は暗号化またはパスワードで保護されている可能性があり、分割できません。", - "unreadable": "{name} を読み取れませんでした。 PDF が破損しているか、サポートされていない可能性があります。", - "pdfLibrary": "ローカル PDF ライブラリをロードできませんでした。", - "archiveLibrary": "ローカル アーカイブ ライブラリをロードできませんでした。", - "generation": "PDF ページを作成できませんでした。小さいドキュメントを試してください。", - "archive": "ZIP アーカイブを作成できませんでした。", - "noPages": "PDF にはページが含まれていません。", - "rangeRequired": "少なくとも 1 ページまたは範囲を入力してください。", - "rangeInvalid": "ページ番号と昇順の範囲をカンマで区切って使用します。", - "rangeOutOfBounds": "すべてのページは 1 から {pages} まででなければなりません。", - "rangeReversed": "範囲は下位ページから上位ページまで続く必要があります。", - "intervalInvalid": "ファイルごとのページ数には 1 以上の整数を入力してください。", - "mode": "有効な分割モードを選択してください。", - "save": "保存場所を開けませんでした。もう一度試してください。" + "oneFile": "PDFは1回につき1つ選択してください", + "unsupported": "PDFファイルを選択してください。ブラウザからファイル形式を取得できない場合のみ、.pdf拡張子をPDFとして扱います", + "noFile": "分割する前にPDFを追加してください", + "protected": "{name}は暗号化またはパスワード保護されているため、分割できない可能性があります", + "unreadable": "{name}を読み込めません。PDFが破損しているか、対応していない可能性があります", + "pdfLibrary": "ローカルPDFライブラリを読み込めませんでした", + "archiveLibrary": "ローカル圧縮ライブラリを読み込めませんでした", + "generation": "PDFを作成できませんでした。より小さい文書で試してください", + "archive": "ZIPファイルを作成できませんでした", + "noPages": "PDFにページがありません", + "rangeRequired": "ページまたは範囲を1つ以上入力してください", + "rangeInvalid": "ページ番号と昇順の範囲をカンマ区切りで入力してください", + "rangeOutOfBounds": "ページ番号は1〜{pages}の範囲で指定してください", + "rangeReversed": "範囲は小さいページ番号から大きいページ番号の順で指定してください", + "intervalInvalid": "1ファイルあたりのページ数は1以上の整数で入力してください", + "mode": "分割方法を選択してください", + "save": "保存先を開けませんでした。もう一度試してください" } }, "mergePdf": { "eyebrow": "PDFツール", - "title": "PDF を結合", - "description": "PDF ファイルを配置し、各ページを 1 つのドキュメントに結合します。ファイルはこのデバイス上に残ります。", + "title": "PDFを結合", + "description": "PDFの順番を並べ替え、すべてのページを1つの文書にまとめます。ファイルはこの端末内で処理されます。", "drop": { - "title": "PDF を追加します", - "description": "PDF ファイルをここにドロップするか、ピッカーを使用します。キューの順序がドキュメントの順序になります。", - "choose": "PDF を選択してください", - "localTitle": "このデバイス上で処理します。", - "localBody": "PDFが外部へ送信されることはありません。", + "title": "PDFを追加", + "description": "PDFをここにドロップするか、ファイルを選択してください。リストの順番で結合します", + "choose": "PDFを選択", + "localTitle": "この端末で処理します", + "localBody": "PDFは外部へ送信されません", "privacyLink": "プライバシーの仕組み" }, "queue": { - "title": "PDF注文", + "title": "PDFの順番", "count": "PDF: {count}", - "empty": "PDFはまだ追加されていません。", - "meta": "{size} · {pages} ページ", + "empty": "PDFが追加されていません", + "meta": "{size} · {pages}ページ", "moveUp": "PDF を上に移動", "moveDown": "PDF を下に移動", "remove": "PDF を削除" }, "settings": { - "title": "マージ設定", + "title": "結合設定", "filename": "出力ファイル名", - "filenameHelp": ".pdf 拡張子が自動的に追加されます。", - "autoDownload": "マージ後に自動的にダウンロード", - "clearAfterSave": "保存後にファイル参照をクリアします", + "filenameHelp": ".pdf拡張子は自動で追加されます", + "autoDownload": "結合後に自動でダウンロード", + "clearAfterSave": "保存後にファイル情報を消去", "pdfFile": "PDFドキュメント" }, "actions": { - "merge": "PDF を結合して保存", - "add": "PDF をさらに追加", - "clear": "すべてクリア" + "merge": "PDFを結合して保存", + "add": "PDFを追加", + "clear": "すべて消去" }, "status": { - "validating": "PDF をローカルでチェックしています…", - "added": "{count} PDF ファイルを追加しました。", - "addedWithRejected": "{added} PDF ファイルを追加しました。 {rejected} のサポートされていないファイルまたは読み取り不可能なファイルがスキップされました。", - "removed": "PDF が削除されました。", - "reordered": "PDF注文が更新されました。", - "cleared": "PDF リストがクリアされました。", - "merging": "PDF をローカルで結合しています…", - "progress": "ファイル {completed} と {total} を結合しています…", + "validating": "PDFを確認中…", + "added": "PDFを{count}個追加しました", + "addedWithRejected": "PDFを{added}個追加し、対応していないか読み込めないファイル{rejected}個を除外しました", + "removed": "PDFを削除しました", + "reordered": "PDFの順番を変更しました", + "cleared": "PDFリストを消去しました", + "merging": "PDFを結合中…", + "progress": "{total}個中{completed}個目のPDFを結合中…", "progressLabel": "PDF 結合の進行状況", - "saved": "{pages} ページの PDF を保存しました。", - "downloaded": "{name} を {pages} ページとともにダウンロードしました。", - "savedAndCleared": "PDF が保存され、ファイル参照が消去されました。", - "saveCancelled": "保存がキャンセルされました。ファイルは書き込まれませんでした。" + "saved": "{pages}ページのPDFを保存しました", + "downloaded": "{pages}ページの{name}をダウンロードしました", + "savedAndCleared": "PDFを保存し、ファイル情報を消去しました", + "saveCancelled": "保存をキャンセルしました。ファイルは作成されていません" }, "errors": { - "unsupported": "PDF ファイルを選択してください。ブラウザーがファイルの種類を提供しない場合は、.pdf 拡張子が受け入れられます。", - "noFiles": "結合する前に少なくとも 1 つの PDF を追加してください。", - "encrypted": "{name} は暗号化またはパスワードで保護されているため、マージできません。", - "unreadable": "{name} を読み取れませんでした。 PDF が破損しているか、サポートされていない可能性があります。", - "library": "ローカル PDF ライブラリをロードできませんでした。", - "generation": "PDF を結合できませんでした。ファイルの数を減らすか、サイズを小さくしてみてください。", - "save": "保存場所を開けませんでした。もう一度試してください。" + "unsupported": "PDFファイルを選択してください。ブラウザからファイル形式を取得できない場合は.pdf拡張子をPDFとして扱います", + "noFiles": "結合する前にPDFを1つ以上追加してください", + "encrypted": "{name}は暗号化またはパスワード保護されているため結合できません", + "unreadable": "{name}を読み込めません。PDFが破損しているか、対応していない可能性があります", + "library": "ローカルPDFライブラリを読み込めませんでした", + "generation": "PDFを結合できませんでした。ファイルの数やサイズを減らして試してください", + "save": "保存先を開けませんでした。もう一度試してください" } }, "imageToPdf": { "eyebrow": "画像ツール", - "title": "画像を PDF に変換", - "description": "JPEG、PNG、WebP 画像を並べて 1 つの PDF として保存します。すべてがこのデバイス上で起こります。", + "title": "画像をPDFに変換", + "description": "JPEG、PNG、WebP画像を好きな順番に並べ、1つのPDFとして保存します。すべてこの端末上で処理されます。", "drop": { - "title": "画像を追加してください", - "description": "ここにファイルをドロップするか、ピッカーを使用します。 JPEG、PNG、WebP を混在させることができます。", - "choose": "画像を選択してください", - "localTitle": "このデバイス上で処理します。", - "localBody": "画像が外部へ送信されることはありません。", + "title": "画像を追加", + "description": "画像をここにドロップするか、ファイルを選択してください。JPEG、PNG、WebPを混在させられます", + "choose": "画像を選択", + "localTitle": "この端末で処理します", + "localBody": "画像は外部へ送信されません", "privacyLink": "プライバシーの仕組み" }, "queue": { - "title": "画像順", + "title": "画像の順番", "count": "画像: {count}", - "empty": "画像はまだ追加されていません。", + "empty": "画像が追加されていません", "previewAlt": "{name} のプレビュー", - "meta": "{size} · ページ {page}", - "moveUp": "画像を上に移動", - "moveDown": "画像を下に移動します", + "meta": "{size} · {page}ページ目", + "moveUp": "画像を上へ移動", + "moveDown": "画像を下へ移動", "remove": "画像を削除" }, "settings": { @@ -414,306 +414,306 @@ export const ja = { "filenameHelp": ".pdf 拡張子が自動的に追加されます。", "pageSize": "ページサイズ", "a4": "A4", - "letter": "手紙", - "imageSize": "試合イメージ", - "orientation": "方向", + "letter": "Letter", + "imageSize": "画像サイズに合わせる", + "orientation": "ページの向き", "auto": "自動", - "portrait": "ポートレート", - "landscape": "風景", - "fit": "画像フィット", - "contain": "画像全体を合わせる", - "cover": "ページを埋める", - "margin": "マージン(mm)", + "portrait": "縦", + "landscape": "横", + "fit": "画像の配置", + "contain": "画像全体を表示", + "cover": "ページ全体に表示", + "margin": "余白(mm)", "quality": "画質", - "qualityBest": "ベスト", - "qualityHigh": "高い", - "qualityBalanced": "バランス調整済み", - "qualitySmall": "小さいファイル", - "autoDownload": "生成後に自動的にダウンロード", - "clearAfterSave": "保存後に画像参照をクリアする", + "qualityBest": "最高", + "qualityHigh": "高", + "qualityBalanced": "標準", + "qualitySmall": "容量を抑える", + "autoDownload": "作成後に自動でダウンロード", + "clearAfterSave": "保存後に画像情報を消去", "pdfFile": "PDFドキュメント" }, "actions": { "generate": "PDFを作成", "add": "画像を追加", - "clear": "すべてクリア" + "clear": "すべて消去" }, "status": { - "added": "{count} 画像を追加しました。", - "addedWithRejected": "{added} がサポートする画像を追加しました。 {rejected} 個のサポートされていないファイルがスキップされました。", - "addedWithReasons": "{added} 画像を追加しました。拒否されました: {reasons}", + "added": "画像を{count}枚追加しました", + "addedWithRejected": "対応している画像を{added}枚追加し、対応していないファイル{rejected}個を除外しました", + "addedWithReasons": "画像を{added}枚追加しました。除外したファイル:{reasons}", "rejectedItem": "{name}: {reason}", - "removed": "画像が削除されました。", - "reordered": "画像の順番を更新しました。", - "cleared": "画像リストがクリアされました。", - "generating": "PDF をローカルで生成しています…", - "progress": "{total} のページ {completed} を処理中…", + "removed": "画像を削除しました", + "reordered": "画像の順番を変更しました", + "cleared": "画像リストを消去しました", + "generating": "PDFを作成中…", + "progress": "{total}ページ中{completed}ページを処理中…", "progressLabel": "PDF 生成の進行状況", - "saved": "{count} ページの PDF を保存しました。", - "downloaded": "{name} をダウンロードしました。", - "savedAndCleared": "PDF が保存され、画像参照が消去されました。", - "saveCancelled": "保存がキャンセルされました。ファイルは書き込まれませんでした。" + "saved": "{count}ページのPDFを保存しました", + "downloaded": "{name}をダウンロードしました", + "savedAndCleared": "PDFを保存し、画像情報を消去しました", + "saveCancelled": "保存をキャンセルしました。ファイルは作成されていません" }, "errors": { - "unsupported": "JPEG、PNG、または WebP 画像ファイルを選択します。", - "noFiles": "PDF を生成する前に、少なくとも 1 つの画像を追加します。", - "decode": "{name} を読み取れませんでした。ファイルが破損しているか、このブラウザでサポートされていない可能性があります。", - "fileTooLarge": "この画像はファイルあたり 50 MiB の制限を超えています。", - "queueFiles": "画像は100枚まで追加できます。", - "queueBytes": "画像キューが合計制限 500 MiB を超えています。", - "dimension": "この画像は 16,384 ピクセルの寸法制限を超えています。", - "pixels": "この画像は 50 メガピクセルの制限を超えています。", - "signature": "ファイルのコンテンツは有効な JPEG、PNG、または WebP 画像ではありません。", - "imageExport": "画像が大きすぎるか、PDF 用に準備できませんでした。", - "canvas": "このブラウザでは PDF 生成用の画像を準備できません。", - "library": "ローカル PDF ライブラリをロードできませんでした。", - "generation": "PDF を生成できませんでした。画像の数を減らすか、小さい画像を試してください。", - "save": "保存場所を開けませんでした。もう一度試してください。" + "unsupported": "JPEG、PNG、WebP画像を選択してください", + "noFiles": "PDFを作成する前に画像を1枚以上追加してください", + "decode": "{name}を読み込めません。ファイルが破損しているか、このブラウザで対応していない可能性があります", + "fileTooLarge": "画像1枚あたりの上限50 MiBを超えています", + "queueFiles": "画像は最大100枚まで追加できます", + "queueBytes": "画像全体の上限500 MiBを超えています", + "dimension": "画像の一辺が16,384ピクセルの上限を超えています", + "pixels": "画像が5,000万ピクセルの上限を超えています", + "signature": "ファイル内容が正しいJPEG、PNG、WebP画像ではありません", + "imageExport": "画像が大きすぎるか、PDF用に処理できませんでした", + "canvas": "このブラウザではPDF用の画像を準備できません", + "library": "ローカルPDFライブラリを読み込めませんでした", + "generation": "PDFを作成できませんでした。画像の数やサイズを減らして試してください", + "save": "保存先を開けませんでした。もう一度試してください" } }, "pdfMetadata": { - "eyebrow": "PDF プライバシー ツール", - "title": "PDF メタデータ インスペクターおよびクリーナー", - "description": "一般的なドキュメントのメタデータを確認し、PDF をアップロードせずにクリーンなコピーを作成します。", + "eyebrow": "PDF プライバシーツール", + "title": "PDFメタデータの確認・削除", + "description": "PDFをアップロードせず、一般的な文書メタデータを確認し、必要な項目を削除したコピーを作成します", "scope": { - "title": "サポート範囲", - "body": "このツールは、以下に示す標準の文書情報フィールドをカバーします。これは法医学的スクラバーではなく、XMP パケット、添付ファイル、注釈、隠しコンテンツ、またはあらゆる識別構造を削除するとは主張しません。", - "cleaning": "クリーニングでは、上記のサポートされているドキュメント メタデータのみが削除されます。" + "title": "対応範囲", + "body": "このツールが扱うのは、下に表示される標準的な文書情報だけです。完全なフォレンジック消去を行うものではなく、XMP、添付ファイル、注釈、非表示コンテンツなど、あらゆる識別情報を削除するものではありません。", + "cleaning": "削除できるのは、上に表示された対応メタデータ項目だけです" }, "drop": { - "title": "PDF を 1 つ追加", - "description": "PDF をここにドロップするか、ピッカーを使用します。別のファイルを選択すると、現在のソースが置き換えられます。", - "choose": "PDF を選択してください", - "localTitle": "このデバイス上で処理します。", - "localBody": "PDFが外部へ送信されることはありません。", + "title": "PDFを追加", + "description": "PDFをここにドロップするか、ファイルを選択してください。別のPDFを選ぶと現在のファイルが置き換わります", + "choose": "PDFを選択", + "localTitle": "この端末で処理します", + "localBody": "PDFは外部へ送信されません", "privacyLink": "プライバシーの仕組み" }, "source": { - "title": "ソース PDF", - "empty": "PDF がまだ選択されていません。", - "summary": "{name} · {size} · {pages} ページ · {fields} サポートされるフィールドが存在します" + "title": "元のPDF", + "empty": "PDFが選択されていません", + "summary": "{name} · {size} · {pages}ページ · 対応項目{fields}件" }, "inspector": { - "title": "ドキュメントのメタデータが検出されました", - "help": "削除するフィールドを選択します。作成者、製作者、作成者、および日付の値は、個人、デバイス、またはアプリケーションを識別する場合があります。", - "caption": "PDF で見つかったサポートされているメタデータ フィールド", + "title": "確認された文書メタデータ", + "help": "削除する項目を選択してください。作成者、使用アプリ、PDF生成ソフト、日時などから、個人・端末・アプリに関する情報がわかる場合があります。", + "caption": "PDF内で確認された対応メタデータ", "remove": "削除", - "field": "フィールド", - "value": "現在値", + "field": "項目", + "value": "現在の値", "notSet": "未設定", - "removeField": "{field} を削除", - "truncated": "表示が短縮されました。クリーニングでは、保存されている完全な値が引き続き使用されます。" + "removeField": "{field}を削除", + "truncated": "表示のみ短くしています。削除処理では保存されている値全体を使用します。" }, "fields": { "title": "タイトル", - "author": "著者", + "author": "作成者", "subject": "件名", "keywords": "キーワード", - "creator": "クリエイターアプリ", - "producer": "PDF プロデューサー", - "creationDate": "作成日", - "modificationDate": "更新日" + "creator": "作成アプリ", + "producer": "PDF生成ソフト", + "creationDate": "作成日時", + "modificationDate": "更新日時" }, "output": { - "title": "クリーンなコピーを作成する", - "description": "オリジナルのままです。保存されたコピーは再度開かれ、成功が報告される前にチェックされます。", + "title": "メタデータを削除したコピーを作成", + "description": "元のPDFは変更しません。保存するPDFをもう一度読み込み、実際に削除されたことを確認してから完了を表示します。", "filename": "出力ファイル名", "filenameHelp": ".pdf 拡張子が自動的に追加されます。", "pdfFile": "PDFドキュメント" }, "actions": { - "clearSource": "クリアPDF", - "selectAll": "存在するものをすべて選択", - "clearSelection": "選択をクリア", + "clearSource": "PDFを消去", + "selectAll": "すべて選択", + "clearSelection": "選択解除", "removeAll": "対応項目をすべて削除", - "clean": "PDF をクリーンアップして保存" + "clean": "削除して保存" }, "comparison": { - "title": "前後で検証済み", - "before": "前", - "after": "コピーを保存した後", - "cleared": "検証済み: {count} の選択されたフィールドがシリアル化されたコピーに存在しません。", - "retained": "警告: {count} 個の選択されたフィールドがシリアル化されたコピーに残ります。" + "title": "保存結果を確認", + "before": "変更前", + "after": "保存後", + "cleared": "確認済み:選択した{count}項目は保存後のPDFから削除されています", + "retained": "警告:選択した{count}項目が保存後のPDFに残っています" }, "status": { - "reading": "メタデータをローカルで読み取り中…", - "loaded": "{pages} ページ PDF 全体で、{count} がサポートするメタデータ フィールドが見つかりました。", - "noneFound": "この {pages} ページ PDF には、サポートされているドキュメント情報メタデータが見つかりませんでした。", - "cleared": "ソース PDF がクリアされました。", - "cleaning": "選択したメタデータを削除し、保存されたバイトを確認しています…", - "saved": "{name} を保存しました。検証済みの {count} フィールドが削除されました。", - "downloaded": "{name} をダウンロードしました。検証済みの {count} フィールドが削除されました。", - "partiallyCleaned": "{name} は保存されましたが、検証の結果、選択された {count} フィールドがまだ存在していることが判明しました。", - "cancelled": "保存がキャンセルされました。ファイルは書き込まれませんでした。" + "reading": "メタデータを読み込み中…", + "loaded": "{pages}ページのPDFから対応メタデータを{count}件確認しました", + "noneFound": "この{pages}ページのPDFには、対応している文書メタデータが見つかりませんでした", + "cleared": "PDFを消去しました", + "cleaning": "選択したメタデータを削除し、保存結果を確認中…", + "saved": "{name}を保存しました。{count}項目の削除を確認しました", + "downloaded": "{name}をダウンロードしました。{count}項目の削除を確認しました", + "partiallyCleaned": "{name}を保存しましたが、選択した{count}項目が残っていることを確認しました", + "cancelled": "保存をキャンセルしました。ファイルは作成されていません" }, "errors": { - "oneFile": "一度に 1 つの PDF を選択します。", - "unsupported": "PDF ファイルを選択してください。", - "protected": "この PDF はパスワードで保護または暗号化されているため、検査できません。", - "unreadable": "この PDF は破損しているか、読み取ることができません。", - "noSelection": "削除する現在のメタデータ フィールドを少なくとも 1 つ選択します。", - "pagesChanged": "クリーンアップされたコピーは、すべてのページの寸法と回転を保持していないため、保存されませんでした。", - "failure": "PDF メタデータを安全に処理できませんでした。", - "save": "保存場所を開けませんでした。もう一度試してください。" + "oneFile": "PDFは1回につき1つ選択してください", + "unsupported": "PDFファイルを選択してください", + "protected": "パスワード保護または暗号化されたPDFは確認できません", + "unreadable": "PDFが破損しているか、読み込めません", + "noSelection": "削除するメタデータ項目を1つ以上選択してください", + "pagesChanged": "処理後のPDFでページサイズまたは回転が保持されなかったため、保存を中止しました", + "failure": "PDFメタデータを安全に処理できませんでした", + "save": "保存先を開けませんでした。もう一度試してください" } }, "pdfToImages": { "eyebrow": "PDFツール", - "title": "PDF から画像へ", - "description": "ドキュメントをアップロードせずに、PDF ページを PNG、JPEG、または WebP ファイルに変換します。", + "title": "PDFを画像に変換", + "description": "PDFをアップロードせず、ページをPNG、JPEG、WebP画像に変換します", "drop": { - "title": "PDF を 1 つ追加", - "description": "PDF をここにドロップするか、ピッカーを使用します。新しい PDF を選択すると、現在のソースが安全に置き換えられます。", - "choose": "PDF を選択してください", - "localTitle": "このデバイス上で処理します。", - "localBody": "PDFが外部へ送信されることはありません。", + "title": "PDFを追加", + "description": "PDFをここにドロップするか、ファイルを選択してください。別のPDFを選ぶと現在のファイルが置き換わります", + "choose": "PDFを選択", + "localTitle": "この端末で処理します", + "localBody": "PDFは外部へ送信されません", "privacyLink": "プライバシーの仕組み" }, "source": { - "title": "ソース PDF", - "empty": "PDF がまだ選択されていません。", - "meta": "{size} · {pages} ページ", - "remove": "ソース PDF を削除" + "title": "元のPDF", + "empty": "PDFが選択されていません", + "meta": "{size} · {pages}ページ", + "remove": "元のPDFを削除" }, "pages": { "title": "変換するページ", - "legend": "変換するページを選択してください", + "legend": "変換するページを選択", "all": { - "title": "全ページ", - "body": "すべてのページをソース順に変換します。" + "title": "すべてのページ", + "body": "元の順番ですべてのページを変換します" }, "selected": { - "title": "選択したページ", - "body": "ページ番号とページ範囲は、入力した順序どおりに使用してください。" + "title": "指定したページ", + "body": "入力したページ番号と範囲を、その順番のまま変換します" }, - "rangeLabel": "ページ選択", - "rangeHelp": "例: 1-3、5、8-10 または 5、1、3。順序と重複は保持されます。" + "rangeLabel": "ページ指定", + "rangeHelp": "例:1-3,5,8-10 または 5,1,3。順番と重複はそのまま保持されます" }, "settings": { "title": "画像設定", - "filename": "出力ベース名", - "filenameHelp": "ページ番号とファイル拡張子は自動的に追加されます。", - "format": "画像フォーマット", + "filename": "出力ファイルの基本名", + "filenameHelp": "ページ番号と拡張子は自動で追加されます", + "format": "画像形式", "quality": "画質", - "qualityHelp": "JPEG と WebP で利用できます。", - "scale": "出力スケール", - "scaleHelp": "スケールが大きくなると、より多くのメモリが使用され、より大きなファイルが作成されます。", + "qualityHelp": "JPEGとWebPで設定できます", + "scale": "出力倍率", + "scaleHelp": "倍率を上げるほどメモリ使用量とファイルサイズが大きくなります", "imageFile": "画像ファイル", - "zipFile": "ZIP アーカイブ" + "zipFile": "ZIPファイル" }, "actions": { - "convert": "変換して保存", + "convert": "画像に変換", "cancel": "キャンセル", - "clear": "ソースをクリア" + "clear": "PDFを消去" }, "status": { - "reading": "PDF をローカルで読んでいます…", - "loaded": "PDF が準備完了: {pages} ページ。", - "rendering": "選択したページをローカルでレンダリングしています…", - "progress": "{total} のレンダリング出力 {completed} (ソース ページ {page})…", + "reading": "PDFを読み込み中…", + "loaded": "PDFの準備ができました:{pages}ページ", + "rendering": "選択したページを変換中…", + "progress": "{total}件中{completed}件目を処理中(元の{page}ページ)…", "progressLabel": "PDF 画像変換の進行状況", - "archive": "ZIP アーカイブを準備しています…", - "saving": "画像を準備しています…", - "saved": "{name} を {count} 画像とともに保存しました。", - "downloaded": "{name} と {count} 画像をダウンロードしました。", - "saveCancelled": "保存がキャンセルされました。ファイルは書き込まれませんでした。", - "cancelled": "変換がキャンセルされました。ファイルは書き込まれませんでした。", - "cleared": "ソース PDF がクリアされました。" + "archive": "ZIPファイルを作成中…", + "saving": "画像を準備中…", + "saved": "画像{count}枚を含む{name}を保存しました", + "downloaded": "画像{count}枚を含む{name}をダウンロードしました", + "saveCancelled": "保存をキャンセルしました。ファイルは作成されていません", + "cancelled": "変換をキャンセルしました。ファイルは作成されていません", + "cleared": "PDFを消去しました" }, "errors": { - "oneFile": "一度に 1 つの PDF を選択します。", - "unsupported": "PDF ファイルを選択してください。", - "noFile": "変換する前に PDF を 1 つ追加します。", - "protected": "この PDF はパスワードで保護または暗号化されているため、変換できません。", - "unreadable": "この PDF は破損しているか、読み取ることができません。", - "noPages": "この PDF にはページが含まれていません。", - "rangeRequired": "少なくとも 1 ページまたは範囲を入力してください。", - "rangeInvalid": "ページ番号と昇順の範囲をカンマで区切って使用します。", - "rangeOutOfBounds": "すべてのページは 1 から {pages} まででなければなりません。", - "rangeReversed": "範囲は下位ページから上位ページまで続く必要があります。", - "format": "PNG、JPEG、または WebP を選択します。", - "scale": "サポートされている出力スケールを選択します。", - "dimension": "レンダリングされたページは、16,384 ピクセルの寸法制限を超えます。", - "pixels": "レンダリングされたページは 50 メガピクセルの制限を超えます。", - "canvas": "このブラウザでは画像キャンバスを作成できません。", - "encoder": "このブラウザは、選択された画像形式をエンコードできませんでした。", - "render": "PDF ページを安全にレンダリングできませんでした。", - "archiveLibrary": "ローカル アーカイブ ライブラリをロードできませんでした。", - "archive": "ZIP アーカイブを作成できませんでした。", - "failure": "PDF を変換できませんでした。ページ数を減らすか、規模を小さくしてみてください。", - "save": "保存場所を開けませんでした。もう一度試してください。" + "oneFile": "PDFは1回につき1つ選択してください", + "unsupported": "PDFファイルを選択してください", + "noFile": "変換する前にPDFを追加してください", + "protected": "パスワード保護または暗号化されたPDFは変換できません", + "unreadable": "PDFが破損しているか、読み込めません", + "noPages": "PDFにページがありません", + "rangeRequired": "ページまたは範囲を1つ以上入力してください", + "rangeInvalid": "ページ番号と昇順の範囲をカンマ区切りで入力してください", + "rangeOutOfBounds": "ページ番号は1〜{pages}の範囲で指定してください", + "rangeReversed": "範囲は小さいページ番号から大きいページ番号の順で指定してください", + "format": "PNG、JPEG、WebPのいずれかを選択してください", + "scale": "対応している出力倍率を選択してください", + "dimension": "変換後のページが16,384ピクセルの上限を超えます", + "pixels": "変換後のページが5,000万ピクセルの上限を超えます", + "canvas": "このブラウザでは画像用キャンバスを作成できません", + "encoder": "このブラウザでは選択した画像形式に変換できません", + "render": "PDFページを安全に画像化できませんでした", + "archiveLibrary": "ローカル圧縮ライブラリを読み込めませんでした", + "archive": "ZIPファイルを作成できませんでした", + "failure": "PDFを変換できませんでした。ページ数を減らすか、倍率を下げて試してください", + "save": "保存先を開けませんでした。もう一度試してください" } }, "organizePdf": { "eyebrow": "PDFツール", - "title": "PDF を整理する", - "description": "元の PDF を変更せずに、ページの並べ替え、回転、または削除を行います。すべてがこのデバイス上に残ります。", + "title": "PDFを整理", + "description": "元のPDFを変更せず、ページの並べ替え・回転・削除ができます。すべてこの端末内で処理されます。", "drop": { - "title": "PDF を 1 つ追加", - "description": "PDF をここにドロップするか、ピッカーを使用します。", - "choose": "PDF を選択してください", - "localTitle": "このデバイス上で処理します。", - "localBody": "PDFが外部へ送信されることはありません。", + "title": "PDFを追加", + "description": "PDFをここにドロップするか、ファイルを選択してください", + "choose": "PDFを選択", + "localTitle": "この端末で処理します", + "localBody": "PDFは外部へ送信されません", "privacyLink": "プライバシーの仕組み" }, "workspace": { - "title": "ページワークスペース", - "empty": "PDF がまだ選択されていません。", - "summary": "{name} · 残り {pages} ページ · {size}", - "changed": "保存されていない変更", - "gridLabel": "PDFページ" + "title": "ページ編集", + "empty": "PDFが選択されていません", + "summary": "{name} · 残り{pages}ページ · {size}", + "changed": "未保存の変更あり", + "gridLabel": "PDFページ一覧" }, "page": { - "preview": "元のページ {page} のプレビュー", - "position": "位置 {position} · 元のページ {page}", - "moveEarlier": "ページ {page} を前に移動します", - "moveLater": "ページ {page} を後で移動します", - "rotateLeft": "ページ {page} を反時計回りに回転します", - "rotateRight": "ページ {page} を時計回りに回転します", - "remove": "ページ {page} を削除", + "preview": "元の{page}ページのプレビュー", + "position": "{position}番目 · 元の{page}ページ", + "moveEarlier": "{page}ページを前へ移動", + "moveLater": "{page}ページを後ろへ移動", + "rotateLeft": "{page}ページを左に90°回転", + "rotateRight": "{page}ページを右に90°回転", + "remove": "{page}ページを削除", "removeText": "削除" }, "output": { - "title": "PDFを保存", - "description": "元の PDF は変更されません。エクスポートは、上に示した順序でページをコピーします。", + "title": "整理したPDFを保存", + "description": "元のPDFは変更されません。上に表示されている順番でページをコピーして保存します", "filename": "出力ファイル名", - "filenameHelp": ".pdf 拡張子が自動的に追加されます。", + "filenameHelp": ".pdf拡張子は自動で追加されます", "pdfFile": "PDFドキュメント" }, "actions": { - "reset": "変更をリセットする", - "clear": "クリアPDF", + "reset": "変更をリセット", + "clear": "PDFを消去", "export": "PDFを保存" }, "status": { - "loading": "PDF をローカルにロードしています…", - "preparing": "{total} のページ {completed} を準備中…", + "loading": "PDFを読み込み中…", + "preparing": "{total}ページ中{completed}ページを準備中…", "progressLabel": "サムネイル作成進捗状況", - "ready": "PDF が準備完了: {pages} ページ。", - "moved": "ページ {page} が移動されました。", - "rotated": "ページ {page} が回転されました。", - "removed": "ページ {page} が削除されました。", - "reset": "元のページ順序と回転が復元されました。", - "cleared": "ソース PDF がクリアされました。", - "exporting": "整理された PDF をローカルで作成しています…", - "saved": "{name} を保存しました。", - "downloaded": "{name} をダウンロードしました。", - "cancelled": "保存がキャンセルされました。ファイルは書き込まれませんでした。" + "ready": "PDFの準備ができました:{pages}ページ", + "moved": "{page}ページを移動しました", + "rotated": "{page}ページを回転しました", + "removed": "{page}ページを削除しました", + "reset": "元のページ順と回転に戻しました", + "cleared": "PDFを消去しました", + "exporting": "整理したPDFを作成中…", + "saved": "{name}を保存しました", + "downloaded": "{name}をダウンロードしました", + "cancelled": "保存をキャンセルしました。ファイルは作成されていません" }, "errors": { - "oneFile": "一度に 1 つの PDF を選択します。", - "unsupported": "PDF ファイルを選択してください。", - "protected": "この PDF はパスワードで保護または暗号化されているため、整理できません。", - "unreadable": "この PDF は破損しているか、読み取ることができません。", - "noPages": "エクスポートする前に少なくとも 1 ページを保存してください。", - "failure": "PDF を安全にロードまたは処理できませんでした。", - "save": "保存場所を開けませんでした。もう一度試してください。" + "oneFile": "PDFは1回につき1つ選択してください", + "unsupported": "PDFファイルを選択してください", + "protected": "パスワード保護または暗号化されたPDFは整理できません", + "unreadable": "PDFが破損しているか、読み込めません", + "noPages": "保存するには1ページ以上残してください", + "failure": "PDFを安全に読み込み、処理できませんでした", + "save": "保存先を開けませんでした。もう一度試してください" } }, "notFound": { "eyebrow": "404", - "title": "このページはここではありません。", - "description": "住所が古いか、タイプミスされている可能性があります。", - "action": "帰宅" + "title": "ページが見つかりません", + "description": "URLが古いか、入力が間違っている可能性があります", + "action": "ホームへ戻る" } }; diff --git a/js/locales/ko.js b/js/locales/ko.js index ca4e104..a67b43f 100644 --- a/js/locales/ko.js +++ b/js/locales/ko.js @@ -17,34 +17,34 @@ export const ko = { mediaCategory: { title: "미디어 도구 — Secure Tools", description: "Secure Tools의 로컬 오디오 및 비디오 유틸리티입니다." }, }, common: { - skip: "본문으로 바로가기", brandTagline: "기기에서 바로 처리하는 개인정보 보호 도구", + skip: "본문으로 바로가기", brandTagline: "기기에서 바로 처리하는 프라이버시 중심 도구", nav: { tools: "도구", privacy: "개인정보 보호", source: "소스", about: "소개" }, - language: "언어", theme: "테마", themes: { system: "시스템", light: "라이트", dark: "다크" }, footerStatement: "파일은 이 기기 밖으로 전송되지 않습니다.", + language: "언어", theme: "테마", themes: { system: "시스템", light: "라이트", dark: "다크" }, footerStatement: "파일은 이 기기 밖으로 전송되지 않습니다", }, hero: { - eyebrow: "로컬 우선 파일 도구", title: "일상 파일을 더 안전하게 다루세요.", description: "파일을 이 기기에서 바로 변환하고 확인하세요. 업로드도, 계정도, 추적도 없습니다.", - primary: "도구 보기", secondary: "개인정보 보호 확인", trust: "기기 내 처리 · 오픈 소스 · 추적 없음", proofLabel: "기기 밖으로 전송되는 파일", proofValue: "없음", proofNote: "핵심 도구는 브라우저 메모리에서 파일을 처리합니다.", + eyebrow: "로컬 우선 파일 도구", title: "일상 파일을 더 안전하게 다루세요", description: "파일을 이 기기에서 바로 변환하고 확인하세요. 업로드도, 계정도, 추적도 없습니다", + primary: "도구 보기", secondary: "개인정보 보호 확인하기", trust: "기기 내 처리 · 오픈 소스 · 추적 없음", proofLabel: "기기 밖으로 전송되는 파일", proofValue: "없음", proofNote: "파일 처리는 브라우저 메모리에서 이루어집니다", }, tools: { - eyebrow: "도구", title: "파일 종류에 맞는 도구를 선택하세요.", description: "카테고리에서 필요한 도구를 찾아보세요. 지금 사용할 수 있는 기능을 명확히 표시합니다.", + eyebrow: "도구", title: "파일 종류에 맞는 도구를 선택하세요", description: "카테고리에서 필요한 도구를 찾아보세요. 지금 사용할 수 있는 기능은 명확히 표시됩니다", categories: { pdf: "PDF", image: "이미지", privacy: "개인정보 보호", scan: "스캔 및 OCR", media: "미디어" }, - categoryDescriptions: { pdf: "문서를 만들고 합치고 정리합니다.", image: "일상 이미지를 변환하고 준비합니다.", privacy: "숨겨진 파일 정보를 확인하고 정리합니다.", scan: "스캔을 유용한 문서로 바꿉니다.", media: "오디오와 비디오를 로컬에서 다룹니다." }, - browseCategory: "도구 보기 →", availableEyebrow: "바로 사용", availableTitle: "사용 가능한 도구", availableDescription: "파일을 외부로 보내지 않고 이 기기에서 처리합니다.", + categoryDescriptions: { pdf: "문서를 만들고 합치고 정리합니다", image: "이미지를 변환하고 다듬습니다", privacy: "숨겨진 파일 정보를 확인하고 정리합니다", scan: "스캔을 활용 가능한 문서로 바꿉니다", media: "오디오와 비디오를 기기에서 처리합니다" }, + browseCategory: "도구 보기 →", availableEyebrow: "바로 사용", availableTitle: "사용 가능한 도구", availableDescription: "파일을 외부로 보내지 않고 이 기기에서 처리합니다", imagesToPdf: "이미지를 PDF로", mergePdf: "PDF 합치기", splitPdf: "PDF 나누기", organizePdf: "PDF 정리", pdfToImages: "PDF를 이미지로", pdfMetadata: "PDF 메타데이터", imageConverter: "이미지 변환기", imageCompressor: "이미지 압축기", imageResizer: "이미지 크기 조절", metadataInspector: "메타데이터 확인", metadataCleaner: "메타데이터 제거", available: "사용 가능", comingSoon: "준비 중", }, categories: { eyebrow: "도구 카테고리", nav: { pdf: "PDF", image: "이미지", privacy: "개인정보 보호", scan: "스캔 및 OCR", media: "미디어" }, - localNote: "사용 가능한 도구는 브라우저 메모리에서 파일을 처리합니다. 준비 중인 기능은 따로 표시합니다.", - plannedNote: "이 카테고리는 준비 중입니다. 아직 사용할 수 없는 기능은 별도로 표시합니다.", + localNote: "사용 가능한 도구는 브라우저 메모리에서 파일을 처리합니다. 준비 중인 기능은 따로 표시합니다", + plannedNote: "이 카테고리는 준비 중입니다. 아직 사용할 수 없는 기능은 별도로 표시합니다", pdf: { title: "PDF 도구", description: "개인 파일을 업로드하지 않고 PDF 문서를 로컬에서 만들고 정리하세요.", imagesToPdf: "JPEG, PNG, WebP 이미지를 원하는 순서로 하나의 PDF로 만듭니다.", merge: "선택한 순서대로 PDF 파일을 합칩니다.", split: "선택한 페이지를 추출하거나 문서를 로컬 파일로 나눕니다.", rotateTitle: "PDF 회전", rotate: "문서 품질을 바꾸지 않고 페이지 방향을 바로잡습니다.", - organize: "PDF 하나의 페이지를 미리 보고 재정렬하고 회전하거나 제거합니다.", - toImages: "PDF 페이지를 입력 순서대로 PNG, JPEG 또는 WebP 이미지로 변환합니다.", - metadata: "일반 문서 메타데이터를 확인하고 검증된 정리 사본을 만듭니다.", + organize: "PDF 페이지를 미리 보고 재정렬하거나 회전·제거합니다", + toImages: "PDF 페이지를 지정한 순서대로 PNG, JPEG 또는 WebP 이미지로 변환합니다", + metadata: "일반 문서 메타데이터를 확인하고 정리한 사본을 만듭니다", }, image: { title: "이미지 도구", description: "일상 이미지 파일을 브라우저에서 비공개로 준비하세요.", @@ -64,26 +64,26 @@ export const ko = { }, }, why: { - eyebrow: "Secure Tools를 선택하는 이유", title: "단순하고 투명한 개인정보 보호.", description: "핵심 파일 처리에서 서버를 제외해 사용자가 신뢰해야 할 대상을 줄였습니다.", - local: { title: "기기 내 처리", body: "파일은 브라우저를 통해 이 기기에서 바로 처리됩니다." }, accounts: { title: "계정 불필요", body: "가입하거나 개인 프로필을 만들지 않아도 기본 도구를 사용할 수 있습니다." }, tracking: { title: "추적 없음", body: "분석 도구, 행동 추적기, 광고 픽셀을 사용하지 않습니다." }, network: { title: "최소한의 네트워크 의존", body: "핵심 파일 처리는 외부 실행 서비스에 의존하지 않습니다." }, + eyebrow: "왜 Secure Tools인가", title: "단순하고 투명한 개인정보 보호 설계", description: "핵심 파일 처리에서 서버를 제외해 사용자가 믿고 맡겨야 할 대상을 줄였습니다", + local: { title: "기기 내 처리", body: "파일은 브라우저에서 이 기기 안에서 바로 처리됩니다" }, accounts: { title: "계정 불필요", body: "가입하거나 개인 프로필을 만들지 않아도 기본 도구를 사용할 수 있습니다" }, tracking: { title: "추적 없음", body: "분석 도구, 행동 추적기, 광고 픽셀을 사용하지 않습니다" }, network: { title: "네트워크 의존 최소화", body: "핵심 파일 처리는 외부 실행 서비스에 의존하지 않습니다" }, }, verify: { - eyebrow: "개인정보 보호 확인", title: "직접 확인해 보세요.", description: "브라우저의 네트워크 활동을 살펴보면 파일이 외부로 전송되지 않는지 확인할 수 있습니다.", - step1: "브라우저 개발자 도구를 엽니다.", step2: "네트워크 탭을 선택합니다.", step3: "샘플 파일로 Secure Tools 기능을 실행합니다.", step4: "파일 업로드 요청이 없는지 확인합니다.", source: "소스 보기", architecture: "개인정보 보호 구조", + eyebrow: "개인정보 보호 확인", title: "직접 확인해 보세요", description: "브라우저의 네트워크 활동을 살펴보면 파일이 외부로 전송되지 않는지 직접 확인할 수 있습니다", + step1: "브라우저 개발자 도구를 엽니다.", step2: "네트워크 탭을 선택합니다.", step3: "샘플 파일로 Secure Tools 기능을 실행합니다.", step4: "파일 업로드 요청이 없는지 확인합니다.", source: "소스 보기", architecture: "개인정보 보호 구조 보기", }, - openSource: { eyebrow: "오픈 소스", title: "설계부터 투명하게.", description: "Secure Tools의 개인정보 보호 방식은 소스 코드에서 직접 확인할 수 있습니다.", action: "소스 보기" }, + openSource: { eyebrow: "오픈 소스", title: "설계부터 투명하게", description: "Secure Tools의 개인정보 보호 방식은 소스 코드에서 직접 확인할 수 있습니다", action: "소스 보기" }, privacy: { - eyebrow: "개인정보 보호 모델", title: "개인정보 보호는 설계에서 시작합니다.", intro: "Secure Tools는 파일을 브라우저에서 처리하도록 설계했습니다. 이 페이지에서는 아직 제공하지 않는 기능이 아니라 현재 적용된 기술 구조를 설명합니다.", + eyebrow: "개인정보 보호 모델", title: "개인정보 보호는 설계에서 시작합니다", intro: "Secure Tools는 파일을 브라우저에서 처리하도록 설계했습니다. 이 페이지에서는 아직 제공하지 않는 기능이 아니라 현재 적용된 기술 구조를 설명합니다.", localTitle: "기기 내 파일 처리", localBody: "지원되는 도구는 브라우저 메모리에서 파일을 읽고 변환합니다. 파일 내용은 Secure Tools 서버로 전송되지 않습니다.", collectionTitle: "계정과 분석 도구 없음", collectionBody: "사용자 계정, 이용 분석, 행동 추적, 광고, 추적 픽셀을 사용하지 않습니다.", storageTitle: "환경설정 저장", storageBody: "localStorage는 이 기기에서 선택한 언어와 테마를 기억하는 데만 사용합니다.", linksTitle: "외부 링크", linksBody: "GitHub 같은 외부 링크를 열면 Secure Tools를 벗어나며, 이후에는 해당 서비스의 정책이 적용됩니다.", }, about: { - eyebrow: "소개", title: "작은 도구, 직접 확인하는 신뢰.", intro: "Secure Tools는 일상 파일을 브라우저에서 바로 처리하는 개인정보 보호 중심 도구 모음입니다.", + eyebrow: "소개", title: "작은 도구, 직접 확인하는 신뢰", intro: "Secure Tools는 일상 파일을 브라우저에서 바로 처리하는 개인정보 보호 중심 도구 모음입니다", purposeTitle: "만든 이유", purposeBody: "간단한 파일 작업 때문에 개인 문서를 알 수 없는 서버에 업로드하거나 계정을 만들 필요는 없습니다.", philosophyTitle: "로컬 우선과 투명성", philosophyBody: "누구나 소스 코드와 네트워크 활동을 확인할 수 있도록 단순한 정적 구조와 투명한 개인정보 보호 모델을 지향합니다.", }, splitPdf: { eyebrow: "PDF 도구", title: "PDF 나누기", description: "선택한 페이지를 추출하거나 하나의 PDF를 예측 가능한 로컬 파일로 나누세요.", - drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요. 원본은 이 기기에서만 읽습니다.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다.", localBody: "PDF는 외부로 전송되지 않습니다.", privacyLink: "개인정보 보호 방식" }, - source: { title: "원본 PDF", empty: "아직 선택한 PDF가 없습니다.", meta: "{size} · {pages}페이지", remove: "원본 PDF 제거" }, + drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요. 원본은 이 기기에서만 읽습니다.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다", localBody: "PDF는 외부로 전송되지 않습니다", privacyLink: "개인정보 보호 방식" }, + source: { title: "원본 PDF", empty: "아직 선택한 PDF가 없습니다", meta: "{size} · {pages}페이지", remove: "원본 PDF 제거" }, modes: { title: "나누기 방식", legend: "PDF를 나눌 방식을 선택하세요", extract: { title: "페이지 범위 추출", body: "입력한 페이지 순서 그대로 하나의 PDF를 만듭니다." }, @@ -97,7 +97,7 @@ export const ko = { autoDownload: "분할 후 자동 다운로드", clearAfterSave: "저장 후 원본 목록 비우기", pdfFile: "PDF 문서", zipFile: "ZIP 압축 파일", }, - actions: { split: "분할하기", clear: "PDF 비우기" }, + actions: { split: "나누기", clear: "PDF 비우기" }, status: { reading: "PDF 읽는 중…", loaded: "PDF 준비 완료: {pages}페이지", cleared: "PDF를 비웠습니다.", preparing: "페이지 준비 중…", archive: "ZIP 파일 만드는 중…", progress: "{total}개 중 {completed}개 파일 만드는 중…", progressLabel: "PDF 분할 진행률", @@ -116,16 +116,16 @@ export const ko = { }, mergePdf: { eyebrow: "PDF 도구", title: "PDF 합치기", description: "PDF 파일의 순서를 정하고 모든 페이지를 하나의 문서로 합치세요. 파일은 이 기기에 머뭅니다.", - drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요. 목록 순서대로 합칩니다.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다.", localBody: "PDF는 외부로 전송되지 않습니다.", privacyLink: "개인정보 보호 방식" }, + drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요. 목록 순서대로 합칩니다.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다", localBody: "PDF는 외부로 전송되지 않습니다", privacyLink: "개인정보 보호 방식" }, queue: { - title: "PDF 순서", count: "PDF: {count}개", empty: "추가된 PDF가 없습니다.", + title: "PDF 순서", count: "PDF: {count}개", empty: "추가된 PDF가 없습니다", meta: "{size} · {pages}페이지", moveUp: "PDF를 위로 이동", moveDown: "PDF를 아래로 이동", remove: "PDF 제거", }, settings: { title: "합치기 설정", filename: "출력 파일명", filenameHelp: ".pdf 확장자는 자동으로 추가됩니다.", autoDownload: "병합 후 자동 다운로드", clearAfterSave: "저장 후 파일 목록 비우기", pdfFile: "PDF 문서", }, - actions: { merge: "병합하기", add: "PDF 추가", clear: "모두 비우기" }, + actions: { merge: "PDF 합치기", add: "PDF 추가", clear: "모두 비우기" }, status: { validating: "PDF 확인 중…", added: "PDF {count}개를 추가했습니다.", addedWithRejected: "PDF {added}개를 추가하고 지원하지 않거나 읽을 수 없는 파일 {rejected}개는 제외했습니다.", removed: "PDF를 제거했습니다.", reordered: "PDF 순서를 바꿨습니다.", cleared: "PDF 목록을 비웠습니다.", merging: "PDF 병합 중…", @@ -140,8 +140,8 @@ export const ko = { }, imageToPdf: { eyebrow: "이미지 도구", title: "이미지를 PDF로", description: "JPEG, PNG, WebP 이미지를 원하는 순서로 정리해 하나의 PDF로 저장하세요. 모든 처리는 이 기기에서 이루어집니다.", - drop: { title: "이미지 추가", description: "파일을 여기에 놓거나 파일 선택 버튼을 누르세요. JPEG, PNG, WebP를 함께 추가할 수 있습니다.", choose: "이미지 선택", localTitle: "이 기기에서 바로 처리합니다.", localBody: "이미지는 외부로 전송되지 않습니다.", privacyLink: "개인정보 보호 방식" }, - queue: { title: "이미지 순서", count: "이미지: {count}개", empty: "추가된 이미지가 없습니다.", previewAlt: "{name} 미리보기", meta: "{size} · {page}페이지", moveUp: "이미지를 위로 이동", moveDown: "이미지를 아래로 이동", remove: "이미지 제거" }, + drop: { title: "이미지 추가", description: "파일을 여기에 놓거나 파일 선택 버튼을 누르세요. JPEG, PNG, WebP를 함께 추가할 수 있습니다.", choose: "이미지 선택", localTitle: "이 기기에서 바로 처리합니다", localBody: "이미지는 외부로 전송되지 않습니다", privacyLink: "개인정보 보호 방식" }, + queue: { title: "이미지 순서", count: "이미지: {count}개", empty: "추가된 이미지가 없습니다", previewAlt: "{name} 미리보기", meta: "{size} · {page}페이지", moveUp: "이미지를 위로 이동", moveDown: "이미지를 아래로 이동", remove: "이미지 제거" }, settings: { title: "PDF 설정", filename: "출력 파일명", filenameHelp: ".pdf 확장자는 자동으로 추가됩니다.", pageSize: "페이지 크기", a4: "A4", letter: "Letter", imageSize: "이미지 크기에 맞춤", orientation: "페이지 방향", auto: "자동", portrait: "세로", landscape: "가로", fit: "이미지 맞춤", contain: "전체 이미지 표시", cover: "페이지 채우기", margin: "여백 (mm)", @@ -161,17 +161,17 @@ export const ko = { }, pdfMetadata: { eyebrow: "PDF 개인정보 보호 도구", title: "PDF 메타데이터 확인 및 제거", description: "PDF를 업로드하지 않고 일반 문서 메타데이터를 확인하고 정리된 사본을 만드세요.", - scope: { title: "지원 범위", body: "이 도구는 아래에 표시된 표준 문서 정보 필드를 처리합니다. 포렌식 삭제 도구가 아니며 XMP 패킷, 첨부 파일, 주석, 숨겨진 콘텐츠 또는 모든 식별 정보를 제거한다고 주장하지 않습니다.", cleaning: "정리는 위에 나열된 지원 문서 메타데이터만 제거합니다." }, - drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요. 새 파일을 선택하면 현재 PDF를 바꿉니다.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다.", localBody: "PDF는 외부로 전송되지 않습니다.", privacyLink: "개인정보 보호 방식" }, - source: { title: "원본 PDF", empty: "아직 선택한 PDF가 없습니다.", summary: "{name} · {size} · {pages}페이지 · 지원 필드 {fields}개 있음" }, + scope: { title: "지원 범위", body: "이 도구는 아래에 표시된 표준 문서 정보 필드만 처리합니다. XMP 패킷, 첨부 파일, 주석, 숨겨진 콘텐츠 등 PDF에 남을 수 있는 모든 식별 정보를 제거하는 포렌식 삭제 도구는 아닙니다.", cleaning: "정리할 수 있는 범위는 위에 표시된 지원 문서 메타데이터로 한정됩니다" }, + drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요. 새 파일을 선택하면 현재 PDF를 바꿉니다.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다", localBody: "PDF는 외부로 전송되지 않습니다", privacyLink: "개인정보 보호 방식" }, + source: { title: "원본 PDF", empty: "아직 선택한 PDF가 없습니다", summary: "{name} · {size} · {pages}페이지 · 지원 필드 {fields}개 있음" }, inspector: { - title: "감지된 문서 메타데이터", help: "제거할 필드를 선택하세요. 작성자, 생성 프로그램, PDF 생성기 및 날짜 값은 사람, 기기 또는 앱을 식별할 수 있습니다.", caption: "PDF에서 발견된 지원 메타데이터 필드", - remove: "제거", field: "필드", value: "현재 값", notSet: "설정되지 않음", removeField: "{field} 제거", truncated: "표시만 줄였습니다. 정리에는 저장된 전체 값을 사용합니다.", + title: "확인된 문서 메타데이터", help: "제거할 항목을 선택하세요. 작성자, 생성 프로그램, PDF 생성기, 날짜 정보는 사람이나 기기, 앱을 식별하는 단서가 될 수 있습니다.", caption: "PDF에서 확인된 지원 메타데이터 항목", + remove: "제거", field: "항목", value: "현재 값", notSet: "설정 안 됨", removeField: "{field} 제거", truncated: "화면에만 일부 표시합니다. 정리할 때는 저장된 전체 값을 사용합니다", }, - fields: { title: "제목", author: "작성자", subject: "주제", keywords: "키워드", creator: "생성 프로그램", producer: "PDF 생성기", creationDate: "생성 날짜", modificationDate: "수정 날짜" }, + fields: { title: "제목", author: "작성자", subject: "주제", keywords: "키워드", creator: "작성 프로그램", producer: "PDF 생성 프로그램", creationDate: "생성 날짜", modificationDate: "수정 날짜" }, output: { title: "정리된 사본 만들기", description: "원본은 변경되지 않습니다. 저장된 사본을 다시 열어 확인한 뒤 성공을 알립니다.", filename: "출력 파일명", filenameHelp: ".pdf 확장자는 자동으로 추가됩니다.", pdfFile: "PDF 문서" }, - actions: { clearSource: "PDF 비우기", selectAll: "모두 선택", clearSelection: "선택 해제", removeAll: "지원 항목 모두 제거", clean: "정리 후 저장" }, - comparison: { title: "검증된 전후 비교", before: "이전", after: "저장된 사본", cleared: "검증 완료: 선택한 필드 {count}개가 직렬화된 사본에 없습니다.", retained: "경고: 선택한 필드 {count}개가 직렬화된 사본에 남아 있습니다." }, + actions: { clearSource: "PDF 비우기", selectAll: "모두 선택", clearSelection: "선택 해제", removeAll: "지원 항목 모두 제거", clean: "정리하고 저장" }, + comparison: { title: "저장 결과 확인", before: "정리 전", after: "저장된 사본", cleared: "검증 완료: 선택한 필드 {count}개가 직렬화된 사본에 없습니다.", retained: "경고: 선택한 필드 {count}개가 직렬화된 사본에 남아 있습니다." }, status: { reading: "메타데이터를 로컬에서 읽는 중…", loaded: "{pages}페이지 PDF에서 지원 메타데이터 필드 {count}개를 찾았습니다.", noneFound: "이 {pages}페이지 PDF에서 지원되는 문서 정보 메타데이터를 찾지 못했습니다.", cleared: "원본 PDF를 비웠습니다.", cleaning: "선택한 메타데이터를 제거하고 저장된 바이트를 검증하는 중…", saved: "{name} 저장 완료, 필드 {count}개 제거를 검증했습니다.", downloaded: "{name} 다운로드 완료, 필드 {count}개 제거를 검증했습니다.", partiallyCleaned: "{name} 파일을 저장했지만 선택한 필드 {count}개가 남아 있음을 확인했습니다.", cancelled: "저장을 취소했습니다. 파일은 기록되지 않았습니다.", @@ -182,8 +182,8 @@ export const ko = { }, pdfToImages: { eyebrow: "PDF 도구", title: "PDF를 이미지로", description: "문서를 업로드하지 않고 PDF 페이지를 PNG, JPEG 또는 WebP 파일로 변환하세요.", - drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요. 새 파일을 선택하면 현재 PDF를 바꿉니다.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다.", localBody: "PDF는 외부로 전송되지 않습니다.", privacyLink: "개인정보 보호 방식" }, - source: { title: "원본 PDF", empty: "아직 선택한 PDF가 없습니다.", meta: "{size} · {pages}페이지", remove: "원본 PDF 제거" }, + drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요. 새 파일을 선택하면 현재 PDF를 바꿉니다.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다", localBody: "PDF는 외부로 전송되지 않습니다", privacyLink: "개인정보 보호 방식" }, + source: { title: "원본 PDF", empty: "아직 선택한 PDF가 없습니다", meta: "{size} · {pages}페이지", remove: "원본 PDF 제거" }, pages: { title: "변환할 페이지", legend: "변환할 페이지 선택", all: { title: "모든 페이지", body: "원본 순서대로 모든 페이지를 변환합니다." }, @@ -195,7 +195,7 @@ export const ko = { format: "이미지 형식", quality: "이미지 품질", qualityHelp: "JPEG와 WebP에서 사용할 수 있습니다.", scale: "출력 배율", scaleHelp: "배율이 클수록 메모리를 더 사용하고 파일 크기가 커집니다.", imageFile: "이미지 파일", zipFile: "ZIP 압축 파일", }, - actions: { convert: "변환하기", cancel: "취소", clear: "PDF 비우기" }, + actions: { convert: "이미지로 변환", cancel: "취소", clear: "PDF 비우기" }, status: { reading: "PDF 읽는 중…", loaded: "PDF 준비 완료: {pages}페이지", rendering: "선택한 페이지 변환 중…", progress: "{total}개 중 {completed}개 처리 중(원본 {page}페이지)…", progressLabel: "PDF 이미지 변환 진행률", @@ -212,16 +212,16 @@ export const ko = { }, organizePdf: { eyebrow: "PDF 도구", title: "PDF 정리", description: "원본 PDF를 변경하지 않고 페이지를 재정렬하고 회전하거나 제거하세요. 모든 처리는 이 기기에서 이루어집니다.", - drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다.", localBody: "PDF는 외부로 전송되지 않습니다.", privacyLink: "개인정보 보호 방식" }, - workspace: { title: "페이지 작업 공간", empty: "아직 선택한 PDF가 없습니다.", summary: "{name} · 남은 페이지 {pages}개 · {size}", changed: "저장하지 않은 변경", gridLabel: "PDF 페이지" }, + drop: { title: "PDF 추가", description: "PDF를 여기에 놓거나 파일 선택 버튼을 누르세요.", choose: "PDF 선택", localTitle: "이 기기에서 바로 처리합니다", localBody: "PDF는 외부로 전송되지 않습니다", privacyLink: "개인정보 보호 방식" }, + workspace: { title: "페이지 작업 공간", empty: "아직 선택한 PDF가 없습니다", summary: "{name} · 남은 페이지 {pages}개 · {size}", changed: "저장하지 않은 변경", gridLabel: "PDF 페이지" }, page: { preview: "원본 {page}페이지 미리보기", position: "{position}번째 · 원본 {page}페이지", moveEarlier: "{page}페이지를 앞으로 이동", moveLater: "{page}페이지를 뒤로 이동", rotateLeft: "{page}페이지를 반시계 방향으로 회전", rotateRight: "{page}페이지를 시계 방향으로 회전", remove: "{page}페이지 제거", removeText: "제거", }, - output: { title: "정리한 PDF 내보내기", description: "원본 PDF는 변경되지 않습니다. 위에 표시된 순서로 페이지를 복사해 내보냅니다.", filename: "출력 파일명", filenameHelp: ".pdf 확장자는 자동으로 추가됩니다.", pdfFile: "PDF 문서" }, - actions: { reset: "변경 초기화", clear: "PDF 비우기", export: "PDF 저장하기" }, + output: { title: "정리한 PDF 내보내기", description: "원본 PDF는 변경되지 않습니다. 위에 표시된 순서대로 페이지를 복사해 저장합니다", filename: "출력 파일명", filenameHelp: ".pdf 확장자는 자동으로 추가됩니다.", pdfFile: "PDF 문서" }, + actions: { reset: "변경 초기화", clear: "PDF 비우기", export: "PDF 저장" }, status: { loading: "PDF 읽는 중…", preparing: "{total}페이지 중 {completed}페이지 준비 중…", progressLabel: "미리보기 준비 진행률", ready: "PDF 준비 완료: {pages}페이지.", moved: "{page}페이지를 이동했습니다.", rotated: "{page}페이지를 회전했습니다.", removed: "{page}페이지를 제거했습니다.", reset: "원래 페이지 순서와 회전을 복원했습니다.", @@ -232,5 +232,5 @@ export const ko = { noPages: "내보내기 전에 페이지를 하나 이상 남겨 주세요.", failure: "PDF를 안전하게 불러오거나 처리하지 못했습니다.", save: "저장 위치를 열 수 없습니다. 다시 시도하세요.", }, }, - notFound: { eyebrow: "404", title: "이 페이지는 존재하지 않습니다.", description: "주소가 오래되었거나 잘못 입력되었을 수 있습니다.", action: "홈으로 돌아가기" }, + notFound: { eyebrow: "404", title: "이 페이지는 존재하지 않습니다", description: "주소가 오래되었거나 잘못 입력되었을 수 있습니다", action: "홈으로 돌아가기" }, }; From e6ef4335c87c51bd71d1bd7e4e7cc9e89fd1818e Mon Sep 17 00:00:00 2001 From: maruson08 Date: Thu, 20 Aug 2026 22:12:14 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=8E=80[Style]=20Remove=20duplicate=20?= =?UTF-8?q?homepage=20ready=20section?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/pages.css | 7 +------ index.html | 1 - js/locales/de.js | 3 --- js/locales/en.js | 3 +-- js/locales/es.js | 3 --- js/locales/fr.js | 3 --- js/locales/ja.js | 3 --- js/locales/ko.js | 2 +- tests/i18n-quality.test.mjs | 2 +- 9 files changed, 4 insertions(+), 23 deletions(-) diff --git a/css/pages.css b/css/pages.css index be1a609..1f680f8 100644 --- a/css/pages.css +++ b/css/pages.css @@ -7,7 +7,7 @@ .prose a { color: var(--accent); font-weight: 700; } .hero { display: grid; min-height: min(48rem, calc(100vh - 4.5rem)); grid-template-columns: minmax(0, 1.55fr) minmax(17rem, 0.65fr); align-items: center; gap: clamp(3rem, 8vw, 8rem); padding-block: clamp(5rem, 11vw, 9rem); } -.hero > *, .section-heading > *, .available-tools > *, .principles-layout > *, .verify-layout > *, .open-source-layout > * { min-width: 0; } +.hero > *, .section-heading > *, .principles-layout > *, .verify-layout > *, .open-source-layout > * { min-width: 0; } .hero h1 { max-width: 12ch; margin-bottom: var(--space-5); } .hero-description { max-width: 41rem; margin-bottom: var(--space-6); color: var(--text-secondary); font-size: clamp(1.05rem, 2vw, 1.25rem); } .hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); } @@ -28,10 +28,6 @@ .category-card__index { color: var(--accent); font-size: 0.75rem; font-weight: 750; } .category-card h3 { margin: auto 0 var(--space-2); font-size: clamp(1.4rem, 3vw, 1.85rem); } .category-card p { min-height: 3em; margin: 0 0 var(--space-4); color: var(--text-secondary); } -.available-tools { display: grid; grid-template-columns: minmax(14rem, 0.65fr) minmax(0, 1fr); gap: var(--space-6); align-items: end; margin-top: var(--space-7); padding-top: var(--space-7); border-top: 1px solid var(--border); } -.available-tools h3 { margin-bottom: var(--space-2); font-size: 1.5rem; } -.available-tools p { margin-bottom: 0; color: var(--text-secondary); } -.available-tools .tool-list { padding-inline: var(--space-5); } .tool-list { margin: 0; padding: 0; list-style: none; } .tool-list li + li { border-top: 1px solid var(--border); } .tool-row { display: flex; min-width: 0; min-height: 3.8rem; align-items: center; justify-content: space-between; gap: var(--space-4); padding-block: var(--space-3); text-decoration: none; } @@ -70,7 +66,6 @@ a.tool-row:hover { color: var(--accent); } .category-directory { grid-template-columns: repeat(2, minmax(0, 1fr)); } .category-card, .category-card:nth-child(4) { grid-column: auto; } .category-card:last-child { grid-column: span 2; } - .available-tools { grid-template-columns: 1fr; align-items: start; } } @media (max-width: 30rem) { diff --git a/index.html b/index.html index 5a16e8d..3f6d28c 100644 --- a/index.html +++ b/index.html @@ -87,7 +87,6 @@

Start with what you need to work on.

04

Scan & OCR

Turn scans into useful documents.

Browse category →
05

Media

Work with audio and video locally.

Browse category →
-

Ready to use

Available now

Production tools that keep file processing on your device.

diff --git a/js/locales/de.js b/js/locales/de.js index b04df8a..06a1379 100644 --- a/js/locales/de.js +++ b/js/locales/de.js @@ -109,9 +109,6 @@ export const de = { "media": "Lokal mit Audio und Video arbeiten." }, "browseCategory": "Kategorie öffnen →", - "availableEyebrow": "Sofort nutzbar", - "availableTitle": "Jetzt verfügbar", - "availableDescription": "Tools, die Dateien direkt auf Ihrem Gerät verarbeiten", "imagesToPdf": "Bilder in PDF", "mergePdf": "PDF zusammenführen", "splitPdf": "PDF teilen", diff --git a/js/locales/en.js b/js/locales/en.js index 175e6d9..fb6b13d 100644 --- a/js/locales/en.js +++ b/js/locales/en.js @@ -33,8 +33,7 @@ export const en = { description: "Browse focused categories, then choose a tool. Availability is always shown clearly.", categories: { pdf: "PDF", image: "Image", privacy: "Privacy", scan: "Scan & OCR", media: "Media" }, categoryDescriptions: { pdf: "Create, combine, and organize documents.", image: "Convert and prepare everyday images.", privacy: "Inspect and clean hidden file details.", scan: "Turn scans into useful documents.", media: "Work with audio and video locally." }, - browseCategory: "Browse category →", availableEyebrow: "Ready to use", availableTitle: "Available now", - availableDescription: "Production tools that keep file processing on your device.", + browseCategory: "Browse category →", imagesToPdf: "Images to PDF", mergePdf: "Merge PDF", splitPdf: "Split PDF", organizePdf: "Organize PDF", pdfToImages: "PDF to Images", pdfMetadata: "PDF Metadata", imageConverter: "Image Converter", imageCompressor: "Image Compressor", imageResizer: "Image Resizer", metadataInspector: "Metadata Inspector", metadataCleaner: "Metadata Cleaner", available: "Available", comingSoon: "Coming soon", diff --git a/js/locales/es.js b/js/locales/es.js index fbc53f9..18d0a77 100644 --- a/js/locales/es.js +++ b/js/locales/es.js @@ -109,9 +109,6 @@ export const es = { "media": "Trabaja con audio y video localmente." }, "browseCategory": "Ver categoría →", - "availableEyebrow": "Disponible ahora", - "availableTitle": "Disponible ahora", - "availableDescription": "Herramientas que procesan los archivos directamente en su dispositivo", "imagesToPdf": "Imágenes a PDF", "mergePdf": "Fusionar PDF", "splitPdf": "Dividir PDF", diff --git a/js/locales/fr.js b/js/locales/fr.js index b86c034..5153b44 100644 --- a/js/locales/fr.js +++ b/js/locales/fr.js @@ -109,9 +109,6 @@ export const fr = { "media": "Travaillez avec l'audio et la vidéo localement." }, "browseCategory": "Voir la catégorie →", - "availableEyebrow": "Disponible maintenant", - "availableTitle": "Disponible maintenant", - "availableDescription": "Outils qui traitent les fichiers directement sur votre appareil", "imagesToPdf": "Images au format PDF", "mergePdf": "Fusionner le PDF", "splitPdf": "Diviser le PDF", diff --git a/js/locales/ja.js b/js/locales/ja.js index 2d93aa4..c1be239 100644 --- a/js/locales/ja.js +++ b/js/locales/ja.js @@ -109,9 +109,6 @@ export const ja = { "media": "音声や動画を端末内で処理" }, "browseCategory": "ツールを見る →", - "availableEyebrow": "すぐ使えるツール", - "availableTitle": "利用可能", - "availableDescription": "ファイルを外部へ送らず、この端末上で処理します", "imagesToPdf": "画像をPDFに", "mergePdf": "PDFを結合", "splitPdf": "PDFを分割", diff --git a/js/locales/ko.js b/js/locales/ko.js index a67b43f..941a05f 100644 --- a/js/locales/ko.js +++ b/js/locales/ko.js @@ -29,7 +29,7 @@ export const ko = { eyebrow: "도구", title: "파일 종류에 맞는 도구를 선택하세요", description: "카테고리에서 필요한 도구를 찾아보세요. 지금 사용할 수 있는 기능은 명확히 표시됩니다", categories: { pdf: "PDF", image: "이미지", privacy: "개인정보 보호", scan: "스캔 및 OCR", media: "미디어" }, categoryDescriptions: { pdf: "문서를 만들고 합치고 정리합니다", image: "이미지를 변환하고 다듬습니다", privacy: "숨겨진 파일 정보를 확인하고 정리합니다", scan: "스캔을 활용 가능한 문서로 바꿉니다", media: "오디오와 비디오를 기기에서 처리합니다" }, - browseCategory: "도구 보기 →", availableEyebrow: "바로 사용", availableTitle: "사용 가능한 도구", availableDescription: "파일을 외부로 보내지 않고 이 기기에서 처리합니다", + browseCategory: "도구 보기 →", imagesToPdf: "이미지를 PDF로", mergePdf: "PDF 합치기", splitPdf: "PDF 나누기", organizePdf: "PDF 정리", pdfToImages: "PDF를 이미지로", pdfMetadata: "PDF 메타데이터", imageConverter: "이미지 변환기", imageCompressor: "이미지 압축기", imageResizer: "이미지 크기 조절", metadataInspector: "메타데이터 확인", metadataCleaner: "메타데이터 제거", available: "사용 가능", comingSoon: "준비 중", }, diff --git a/tests/i18n-quality.test.mjs b/tests/i18n-quality.test.mjs index 75da6cb..e4df15e 100644 --- a/tests/i18n-quality.test.mjs +++ b/tests/i18n-quality.test.mjs @@ -45,7 +45,7 @@ function placeholders(value) { function testCatalogParityAndQuality() { assert.deepEqual([...Object.keys(translations)], [...languageNames.keys()]); const english = flatten(translations.en); - assert.equal(english.size, 525); + assert.equal(english.size, 522); for (const [language, catalog] of Object.entries(translations)) { const flattened = flatten(catalog); From cd1fab612a10440b18b9be2670b1c8157ffc0e05 Mon Sep 17 00:00:00 2001 From: maruson08 Date: Thu, 20 Aug 2026 22:15:29 +0900 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=8E=80[Style]=20Refine=20multilingual?= =?UTF-8?q?=20typography=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/base.css | 48 +++++++++++++++++++++++++++++-------- css/components.css | 24 ++++++++++--------- css/pages.css | 12 +++++----- tests/i18n-quality.test.mjs | 14 +++++++---- tools/pdf/organize/tool.css | 2 +- tools/shared/tool.css | 2 +- 6 files changed, 69 insertions(+), 33 deletions(-) diff --git a/css/base.css b/css/base.css index c57dd48..e3f4b1b 100644 --- a/css/base.css +++ b/css/base.css @@ -1,6 +1,21 @@ :root { color-scheme: light; - --font-sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", "Hiragino Kaku Gothic ProN", "Yu Gothic UI", Meiryo, sans-serif; + --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.35rem; + --line-height-tight: 1; + --line-height-heading: 1.12; + --line-height-body: 1.65; + --line-height-control: 1.35; + --letter-spacing-heading: -0.035em; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --font-weight-heavy: 800; --bg-primary: #f7f7f5; --bg-secondary: #ffffff; --bg-subtle: #efefec; @@ -62,25 +77,38 @@ body { background: var(--bg-primary); color: var(--text-primary); font-family: var(--font-sans); - font-size: 1rem; - line-height: 1.65; + font-size: var(--font-size-base); + line-height: var(--line-height-body); text-rendering: optimizeLegibility; transition: background-color var(--transition-theme), color var(--transition-theme); overflow-wrap: break-word; word-break: normal; } -body, button, select { font: inherit; } -button, select { color: inherit; } +body, button, input, select, textarea { font: inherit; } +button, input, select, textarea { color: inherit; } a { color: inherit; text-decoration-thickness: 0.08em; text-underline-offset: 0.2em; } img, svg { display: block; max-width: 100%; } h1, h2, h3, p { margin-block-start: 0; } -h1, h2, h3 { line-height: 1.12; letter-spacing: -0.035em; } +h1, h2, h3 { line-height: var(--line-height-heading); letter-spacing: var(--letter-spacing-heading); } h1 { font-size: clamp(2.65rem, 8vw, 5.8rem); } h2 { font-size: clamp(2rem, 4vw, 3.25rem); } -html:lang(ko) body { word-break: keep-all; } -html:lang(ja) body { line-break: strict; } -h3 { font-size: 1.1rem; letter-spacing: -0.015em; } +h3 { font-size: var(--font-size-lg); letter-spacing: -0.015em; } + +html:lang(ko) { + --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", sans-serif; + --line-height-heading: 1.2; + --line-height-body: 1.7; + --letter-spacing-heading: -0.02em; +} +html:lang(ja) { + --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Hiragino Sans", "Yu Gothic UI", "Yu Gothic", Meiryo, "Noto Sans JP", sans-serif; + --line-height-heading: 1.2; + --line-height-body: 1.7; + --letter-spacing-heading: -0.02em; + line-break: strict; +} +html:lang(ko) :is(h1, h2, h3, .eyebrow, .hero-description, .category-card p, .category-tool p, .tool-intro > p:last-child, .drop-zone h2, .drop-zone p, .section-heading > p, .button, .control-label, .status) { word-break: keep-all; } ::selection { background: var(--accent-soft); } :focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; } @@ -88,7 +116,7 @@ h3 { font-size: 1.1rem; letter-spacing: -0.015em; } .reading-width { max-width: var(--reading-width); } .section { padding-block: var(--space-8); } .section--bordered { border-top: 1px solid var(--border); } -.eyebrow { margin-bottom: var(--space-4); color: var(--accent); font-size: 0.75rem; font-weight: 750; letter-spacing: 0.13em; text-transform: uppercase; } +.eyebrow { margin-bottom: var(--space-4); color: var(--accent); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); letter-spacing: 0.13em; line-height: var(--line-height-control); text-transform: uppercase; } .muted { color: var(--text-secondary); } .skip-link { diff --git a/css/components.css b/css/components.css index 380f713..5228560 100644 --- a/css/components.css +++ b/css/components.css @@ -1,5 +1,6 @@ .button { display: inline-flex; + min-width: 0; min-height: 2.75rem; align-items: center; justify-content: center; @@ -7,10 +8,11 @@ padding: 0.65rem 1rem; border: 1px solid transparent; border-radius: var(--radius-sm); - font-weight: 700; - line-height: 1.2; + font-weight: var(--font-weight-bold); + line-height: var(--line-height-control); max-width: 100%; text-align: center; + overflow-wrap: anywhere; text-decoration: none; cursor: pointer; transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); @@ -24,11 +26,13 @@ .control { min-height: 2.5rem; + min-width: 0; max-width: 7.5rem; padding: 0.4rem 1.8rem 0.4rem 0.65rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background-color: var(--bg-secondary); + line-height: var(--line-height-control); transition: border-color var(--transition-fast), background-color var(--transition-theme); } @@ -36,27 +40,28 @@ .control:hover:not(:disabled) { border-color: var(--border-strong); } .control:disabled { border-color: var(--border); background: var(--bg-subtle); color: var(--status-muted); cursor: not-allowed; } -.control-label { display: inline-flex; min-width: 0; align-items: center; gap: var(--space-2); color: var(--text-secondary); font-size: 0.75rem; font-weight: 650; } +.control-label { display: inline-flex; min-width: 0; align-items: center; gap: var(--space-2); color: var(--text-secondary); font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); line-height: var(--line-height-control); } .surface { border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--bg-secondary); box-shadow: var(--shadow-sm); } -.status { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--status-muted); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; white-space: nowrap; } +.status { display: inline-flex; max-width: 100%; align-items: center; gap: var(--space-2); color: var(--status-muted); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); letter-spacing: 0.04em; line-height: var(--line-height-control); text-align: center; text-transform: uppercase; } .status::before { width: 0.45rem; height: 0.45rem; border-radius: 50%; background: currentColor; content: ""; } .status--available { color: var(--accent); } -.text-link { font-weight: 700; text-decoration-color: var(--border-strong); transition: color var(--transition-fast), text-decoration-color var(--transition-fast); } +.text-link { font-weight: var(--font-weight-bold); text-decoration-color: var(--border-strong); transition: color var(--transition-fast), text-decoration-color var(--transition-fast); } .text-link:hover { color: var(--accent); text-decoration-color: currentColor; } .icon { width: 1.1rem; height: 1.1rem; flex: 0 0 auto; } .site-header { position: relative; z-index: 10; border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--bg-primary) 92%, transparent); } .header-inner { display: flex; min-height: 4.5rem; align-items: center; gap: var(--space-6); } -.brand { display: inline-flex; align-items: center; gap: var(--space-3); font-weight: 780; letter-spacing: -0.025em; text-decoration: none; white-space: nowrap; } +.brand { display: inline-flex; align-items: center; gap: var(--space-3); font-weight: var(--font-weight-bold); letter-spacing: -0.025em; text-decoration: none; white-space: nowrap; } .brand-mark { width: 0.75rem; height: 0.75rem; border: 3px solid var(--accent); border-radius: 50%; box-shadow: 0 0 0 4px var(--accent-soft); } .site-nav { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: var(--space-5); margin-right: auto; } -.site-nav a, .site-footer nav a { color: var(--text-secondary); font-size: 0.875rem; font-weight: 650; text-decoration: none; } +.site-nav a, .site-footer nav a { min-width: 0; color: var(--text-secondary); font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); line-height: var(--line-height-control); overflow-wrap: anywhere; text-decoration: none; } .site-nav a:hover, .site-footer nav a:hover { color: var(--text-primary); } +.header-inner > *, .header-controls > *, .control-label > span { min-width: 0; } .header-controls { display: flex; min-width: 0; flex: 0 1 auto; align-items: center; gap: var(--space-3); } .site-footer { padding-block: var(--space-7); border-top: 1px solid var(--border); } .footer-inner { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-6); } -.footer-inner p { margin: var(--space-3) 0 0; color: var(--text-secondary); font-size: 0.875rem; } +.footer-inner p { margin: var(--space-3) 0 0; color: var(--text-secondary); font-size: var(--font-size-sm); } .site-footer nav { display: flex; flex-wrap: wrap; gap: var(--space-5); } @media (max-width: 68rem) { @@ -73,6 +78,3 @@ .site-nav a { font-size: 0.8rem; } .footer-inner { flex-direction: column; } } - -html:lang(ko) .button, html:lang(ko) .control-label, html:lang(ko) .status { word-break: keep-all; } -html:lang(ja) .button, html:lang(ja) .control-label, html:lang(ja) .status { line-break: strict; } diff --git a/css/pages.css b/css/pages.css index 1f680f8..8911fa9 100644 --- a/css/pages.css +++ b/css/pages.css @@ -11,11 +11,11 @@ .hero h1 { max-width: 12ch; margin-bottom: var(--space-5); } .hero-description { max-width: 41rem; margin-bottom: var(--space-6); color: var(--text-secondary); font-size: clamp(1.05rem, 2vw, 1.25rem); } .hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); } -.trust-line { margin: var(--space-5) 0 0; color: var(--text-secondary); font-size: 0.85rem; font-weight: 650; } +.trust-line { margin: var(--space-5) 0 0; color: var(--text-secondary); font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); } .hero-proof { position: relative; padding: var(--space-6); overflow: hidden; } .proof-signal { display: block; width: 0.8rem; height: 0.8rem; margin-bottom: var(--space-7); border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 0.55rem var(--accent-soft); } -.proof-label { margin-bottom: var(--space-2); color: var(--text-secondary); font-size: 0.75rem; font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; } -.proof-value { margin-bottom: var(--space-4); font-size: clamp(2.5rem, 5vw, 4rem); font-weight: 780; letter-spacing: -0.06em; line-height: 1; } +.proof-label { margin-bottom: var(--space-2); color: var(--text-secondary); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); letter-spacing: 0.08em; text-transform: uppercase; } +.proof-value { margin-bottom: var(--space-4); font-size: clamp(2.5rem, 5vw, 4rem); font-weight: var(--font-weight-bold); letter-spacing: -0.06em; line-height: var(--line-height-tight); } .proof-note { margin: 0; color: var(--text-secondary); font-size: 0.9rem; } .section-heading { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(17rem, 0.65fr); gap: var(--space-7); align-items: end; margin-bottom: var(--space-7); } @@ -25,7 +25,7 @@ .category-card { display: flex; min-width: 0; min-height: 15rem; grid-column: span 2; flex-direction: column; align-items: flex-start; padding: var(--space-5); color: inherit; text-decoration: none; transition: border-color var(--transition-fast), transform var(--transition-fast); } .category-card:nth-child(4) { grid-column: 2 / span 2; } .category-card:hover { border-color: var(--border-strong); transform: translateY(-2px); } -.category-card__index { color: var(--accent); font-size: 0.75rem; font-weight: 750; } +.category-card__index { color: var(--accent); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); } .category-card h3 { margin: auto 0 var(--space-2); font-size: clamp(1.4rem, 3vw, 1.85rem); } .category-card p { min-height: 3em; margin: 0 0 var(--space-4); color: var(--text-secondary); } .tool-list { margin: 0; padding: 0; list-style: none; } @@ -39,7 +39,7 @@ a.tool-row:hover { color: var(--accent); } .section-intro p:last-child { max-width: 30rem; } .principle-list article { display: grid; grid-template-columns: 2rem 1fr; gap: var(--space-4); padding-block: var(--space-5); border-top: 1px solid var(--border); } .principle-list article:last-child { border-bottom: 1px solid var(--border); } -.principle-list article > span { color: var(--accent); font-size: 0.75rem; font-weight: 750; } +.principle-list article > span { color: var(--accent); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); } .principle-list h3 { margin-bottom: var(--space-2); } .principle-list p { margin: 0; color: var(--text-secondary); } @@ -51,7 +51,7 @@ a.tool-row:hover { color: var(--accent); } .verify-steps { margin: 0; padding: 0; list-style: none; counter-reset: verify; } .verify-steps li { display: grid; grid-template-columns: 2.4rem 1fr; gap: var(--space-4); align-items: center; padding-block: var(--space-4); border-top: 1px solid var(--border-strong); counter-increment: verify; } .verify-steps li:last-child { border-bottom: 1px solid var(--border-strong); } -.verify-steps li::before { display: grid; width: 2.25rem; height: 2.25rem; place-items: center; border: 1px solid var(--border-strong); border-radius: 50%; content: counter(verify); font-size: 0.75rem; font-weight: 750; } +.verify-steps li::before { display: grid; width: 2.25rem; height: 2.25rem; place-items: center; border: 1px solid var(--border-strong); border-radius: 50%; content: counter(verify); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); } .open-source-layout { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-7); align-items: end; } .open-source-layout h2 { margin-bottom: 0; } diff --git a/tests/i18n-quality.test.mjs b/tests/i18n-quality.test.mjs index e4df15e..26055b2 100644 --- a/tests/i18n-quality.test.mjs +++ b/tests/i18n-quality.test.mjs @@ -123,10 +123,16 @@ function testMultilingualLayoutFoundation() { .map((file) => fs.readFileSync(path.join(root, "css", file), "utf8")) .join("\n"); assert.doesNotMatch(css, /word-break:\s*break-all/); - assert.match(css, /Malgun Gothic/); - assert.match(css, /Hiragino Kaku Gothic ProN/); - assert.match(css, /html:lang\(ko\)[^{]*\{[^}]*word-break:\s*keep-all/); - assert.match(css, /html:lang\(ja\)[^{]*\{[^}]*line-break:\s*strict/); + assert.match(css, /--font-sans:\s*system-ui/); + assert.doesNotMatch(css, /\bInter\b|@font-face|fonts\.(?:googleapis|gstatic)\.com/); + assert.match(css, /html:lang\(ko\)\s*\{[^}]*Malgun Gothic/); + assert.match(css, /html:lang\(ja\)\s*\{[^}]*Hiragino Sans[^}]*Yu Gothic UI/); + assert.match(css, /html:lang\(ko\) :is\([^}]+word-break:\s*keep-all/); + assert.doesNotMatch(css, /html:lang\(ko\) body\s*\{[^}]*word-break:\s*keep-all/); + assert.match(css, /html:lang\(ja\)\s*\{[^}]*line-break:\s*strict/); + assert.match(css, /--line-height-heading:/); + assert.match(css, /--font-weight-bold:/); + assert.match(css, /body, button, input, select, textarea\s*\{\s*font:\s*inherit/); assert.match(css, /min-width:\s*0/); assert.match(css, /@media \(max-width:\s*68rem\)/); assert.match(css, /control\[data-language-select\][^{]*\{[^}]*width:\s*9rem/); diff --git a/tools/pdf/organize/tool.css b/tools/pdf/organize/tool.css index 4175dfc..c81c8ae 100644 --- a/tools/pdf/organize/tool.css +++ b/tools/pdf/organize/tool.css @@ -4,7 +4,7 @@ .organizer-toolbar__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); align-items: center; } .source-empty, .source-summary { margin: 0; color: var(--text-secondary); } .source-empty[hidden], .source-summary[hidden], .dirty-state[hidden] { display: none; } -.dirty-state { padding: .45rem .65rem; border: 1px solid var(--accent); border-radius: 999px; color: var(--accent); font-size: .75rem; font-weight: 750; } +.dirty-state { padding: .45rem .65rem; border: 1px solid var(--accent); border-radius: 999px; color: var(--accent); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); } .page-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: var(--space-4); margin: var(--space-5) 0 0; padding: 0; list-style: none; } .page-card { min-width: 0; padding: var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-primary); transition: border-color var(--transition-fast), background-color var(--transition-fast), opacity var(--transition-fast); } .page-card:focus-within { border-color: var(--accent); } diff --git a/tools/shared/tool.css b/tools/shared/tool.css index b16e9ab..7b48d37 100644 --- a/tools/shared/tool.css +++ b/tools/shared/tool.css @@ -58,7 +58,7 @@ .settings { position: sticky; top: var(--space-4); padding: var(--space-5); } .settings h2 { margin-bottom: var(--space-5); padding-bottom: var(--space-4); border-bottom: 1px solid var(--border); font-size: 1.35rem; } .field { display: grid; gap: var(--space-2); margin-bottom: var(--space-4); } -.field label { color: var(--text-secondary); font-size: 0.78rem; font-weight: 750; } +.field label { color: var(--text-secondary); font-size: 0.78rem; font-weight: var(--font-weight-bold); } .field input, .field select { width: 100%; min-height: 2.75rem; padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-primary); color: var(--text-primary); transition: border-color var(--transition-fast), background-color var(--transition-theme); } .field input:hover, .field select:hover { border-color: var(--border-strong); } .field-help { margin: 0; color: var(--text-secondary); font-size: 0.72rem; } From 8e6da52899ab55e380dd5ae4fca32491b1069ba0 Mon Sep 17 00:00:00 2001 From: maruson08 Date: Thu, 20 Aug 2026 22:17:34 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=8E=80[Style]=20Harden=20localized=20?= =?UTF-8?q?responsive=20layouts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/categories.css | 5 +++-- css/components.css | 4 ++-- css/pages.css | 11 +++-------- tools/pdf/metadata/tool.css | 5 +++-- tools/pdf/organize/tool.css | 5 +++-- tools/shared/tool.css | 15 ++++++++------- 6 files changed, 22 insertions(+), 23 deletions(-) diff --git a/css/categories.css b/css/categories.css index 43c3a00..29f8db0 100644 --- a/css/categories.css +++ b/css/categories.css @@ -5,14 +5,15 @@ .category-intro > div > p:last-child, .category-intro > p { max-width: 38rem; margin-bottom: 0; color: var(--text-secondary); } .category-nav { border-bottom: 1px solid var(--border); background: var(--bg-subtle); } .category-nav__inner { display: flex; gap: var(--space-2); padding-block: var(--space-3); overflow-x: auto; scrollbar-width: thin; } -.category-nav a { flex: 0 0 auto; padding: 0.45rem 0.75rem; border-radius: var(--radius-sm); color: var(--text-secondary); font-size: 0.82rem; font-weight: 700; text-decoration: none; } +.category-nav a { display: inline-flex; min-height: 2.75rem; flex: 0 0 auto; align-items: center; padding: 0.45rem 0.75rem; border-radius: var(--radius-sm); color: var(--text-secondary); font-size: 0.82rem; font-weight: var(--font-weight-bold); line-height: var(--line-height-control); text-decoration: none; } .category-nav a:hover, .category-nav a[aria-current="page"] { background: var(--bg-secondary); color: var(--text-primary); } .category-tool-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4); margin: 0; padding: 0; list-style: none; } .category-tool { display: flex; min-width: 0; min-height: 12rem; flex-direction: column; align-items: flex-start; padding: var(--space-5); color: inherit; text-decoration: none; } .category-tool__top { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: var(--space-4); margin-bottom: auto; } .category-tool h2 { margin: var(--space-5) 0 var(--space-2); font-size: clamp(1.3rem, 3vw, 1.75rem); } .category-tool > .status { margin-bottom: auto; } -.category-tool p { margin: 0; color: var(--text-secondary); } +.category-tool > * { min-width: 0; } +.category-tool p { margin: 0; color: var(--text-secondary); overflow-wrap: anywhere; } a.category-tool { transition: border-color var(--transition-fast), transform var(--transition-fast); } a.category-tool:hover { border-color: var(--border-strong); transform: translateY(-2px); } .category-tool__arrow { color: var(--accent); font-size: 1.35rem; } diff --git a/css/components.css b/css/components.css index 5228560..745d046 100644 --- a/css/components.css +++ b/css/components.css @@ -54,7 +54,7 @@ .brand { display: inline-flex; align-items: center; gap: var(--space-3); font-weight: var(--font-weight-bold); letter-spacing: -0.025em; text-decoration: none; white-space: nowrap; } .brand-mark { width: 0.75rem; height: 0.75rem; border: 3px solid var(--accent); border-radius: 50%; box-shadow: 0 0 0 4px var(--accent-soft); } .site-nav { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: var(--space-5); margin-right: auto; } -.site-nav a, .site-footer nav a { min-width: 0; color: var(--text-secondary); font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); line-height: var(--line-height-control); overflow-wrap: anywhere; text-decoration: none; } +.site-nav a, .site-footer nav a { display: inline-flex; min-width: 0; min-height: 2.75rem; align-items: center; color: var(--text-secondary); font-size: var(--font-size-sm); font-weight: var(--font-weight-semibold); line-height: var(--line-height-control); overflow-wrap: anywhere; text-decoration: none; } .site-nav a:hover, .site-footer nav a:hover { color: var(--text-primary); } .header-inner > *, .header-controls > *, .control-label > span { min-width: 0; } .header-controls { display: flex; min-width: 0; flex: 0 1 auto; align-items: center; gap: var(--space-3); } @@ -74,7 +74,7 @@ .header-controls { width: 100%; align-items: stretch; flex-direction: column; margin-left: 0; } .control-label { width: 100%; justify-content: space-between; } .control, .control[data-language-select] { width: min(100%, 12rem); max-width: none; } - .site-nav { justify-content: space-between; gap: var(--space-3); } + .site-nav { justify-content: flex-start; gap: var(--space-2) var(--space-4); } .site-nav a { font-size: 0.8rem; } .footer-inner { flex-direction: column; } } diff --git a/css/pages.css b/css/pages.css index 8911fa9..5ce5c90 100644 --- a/css/pages.css +++ b/css/pages.css @@ -26,14 +26,10 @@ .category-card:nth-child(4) { grid-column: 2 / span 2; } .category-card:hover { border-color: var(--border-strong); transform: translateY(-2px); } .category-card__index { color: var(--accent); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); } -.category-card h3 { margin: auto 0 var(--space-2); font-size: clamp(1.4rem, 3vw, 1.85rem); } +.category-card > * { min-width: 0; } +.category-card h3 { margin: var(--space-7) 0 var(--space-2); font-size: clamp(1.4rem, 3vw, 1.85rem); } .category-card p { min-height: 3em; margin: 0 0 var(--space-4); color: var(--text-secondary); } -.tool-list { margin: 0; padding: 0; list-style: none; } -.tool-list li + li { border-top: 1px solid var(--border); } -.tool-row { display: flex; min-width: 0; min-height: 3.8rem; align-items: center; justify-content: space-between; gap: var(--space-4); padding-block: var(--space-3); text-decoration: none; } -a.tool-row:hover { color: var(--accent); } - -.tool-row > :first-child { min-width: 0; overflow-wrap: anywhere; } +.category-card .text-link { margin-top: auto; } .principles-layout { display: grid; grid-template-columns: minmax(17rem, 0.7fr) minmax(0, 1fr); gap: clamp(3rem, 9vw, 9rem); } .section-intro h2 { max-width: 10ch; } .section-intro p:last-child { max-width: 30rem; } @@ -70,7 +66,6 @@ a.tool-row:hover { color: var(--accent); } @media (max-width: 30rem) { .hero-actions .button { width: 100%; } - .tool-row { align-items: flex-start; flex-direction: column; gap: var(--space-1); } .category-directory { grid-template-columns: 1fr; } .category-card, .category-card:last-child { min-height: 12rem; grid-column: auto; } } diff --git a/tools/pdf/metadata/tool.css b/tools/pdf/metadata/tool.css index d35b6d0..ebacd18 100644 --- a/tools/pdf/metadata/tool.css +++ b/tools/pdf/metadata/tool.css @@ -19,6 +19,7 @@ .metadata-comparison p { color: var(--text-secondary); } .metadata-toolbar, .inspector-heading, +.metadata-toolbar > *, .inspector-heading > *, .selection-actions > *, .comparison-grid > * { min-width: 0; } .selection-actions { flex-wrap: wrap; justify-content: flex-end; } .metadata-toolbar, .inspector-heading { display: flex; justify-content: space-between; gap: var(--space-4); } .selection-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); } @@ -27,7 +28,7 @@ .metadata-table { width: 100%; border-collapse: collapse; text-align: left; } .metadata-table th, .metadata-table td { padding: var(--space-3); border-bottom: 1px solid var(--border); vertical-align: top; } -.metadata-table thead th { color: var(--text-secondary); font-size: .8rem; } +.metadata-table thead th { color: var(--text-secondary); font-size: .8rem; overflow-wrap: anywhere; } .metadata-table tbody td:first-child { width: 5rem; text-align: center; } .metadata-table input { width: 1.25rem; height: 1.25rem; accent-color: var(--accent); } .metadata-table input:focus-visible { outline: .18rem solid var(--focus); outline-offset: .18rem; } @@ -39,7 +40,7 @@ .comparison-grid > div { padding: var(--space-4); border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-subtle); } .comparison-grid h3 { margin-top: 0; } .comparison-grid dl { margin-bottom: 0; } -.comparison-grid dt { margin-top: var(--space-3); font-weight: 700; } +.comparison-grid dt { margin-top: var(--space-3); font-weight: var(--font-weight-bold); overflow-wrap: anywhere; } .comparison-grid dd { margin: var(--space-1) 0 0; overflow-wrap: anywhere; white-space: pre-wrap; } .metadata-status { max-width: 60rem; } @media (prefers-reduced-motion: reduce) { diff --git a/tools/pdf/organize/tool.css b/tools/pdf/organize/tool.css index c81c8ae..390566e 100644 --- a/tools/pdf/organize/tool.css +++ b/tools/pdf/organize/tool.css @@ -1,6 +1,7 @@ .organizer { margin-top: var(--space-5); padding: var(--space-5); } .organizer-toolbar { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); padding-bottom: var(--space-4); border-bottom: 1px solid var(--border); } .organizer-toolbar h2 { margin: 0 0 var(--space-2); font-size: 1.35rem; } +.organizer-toolbar > *, .organizer-toolbar__actions > *, .page-label > * { min-width: 0; } .organizer-toolbar__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); align-items: center; } .source-empty, .source-summary { margin: 0; color: var(--text-secondary); } .source-empty[hidden], .source-summary[hidden], .dirty-state[hidden] { display: none; } @@ -13,11 +14,11 @@ .page-card[data-drop-target="true"] { border-color: var(--accent); background: var(--accent-soft); } .page-preview { display: grid; min-height: 12rem; place-items: center; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--preview-backdrop); } .page-preview canvas { display: block; width: min(100%, 12.5rem); max-height: 17rem; object-fit: contain; background: #fff; box-shadow: 0 1px 4px rgb(0 0 0 / .18); } -.page-label { display: flex; justify-content: space-between; gap: var(--space-2); margin: var(--space-3) 0; font-size: .78rem; } +.page-label { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-1) var(--space-2); margin: var(--space-3) 0; font-size: .78rem; } .page-label strong { overflow-wrap: anywhere; } .page-rotation { color: var(--text-secondary); } .page-actions { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-2); } -.page-action { min-height: 2.5rem; padding: .35rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-secondary); color: var(--text-primary); cursor: pointer; font-size: 1rem; } +.page-action { min-width: 0; min-height: 2.75rem; padding: .35rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-secondary); color: var(--text-primary); cursor: pointer; font-size: 1rem; line-height: var(--line-height-control); } .page-action:hover:not(:disabled) { border-color: var(--border-strong); background: var(--bg-subtle); } .page-action:disabled { border-color: var(--border); background: var(--bg-subtle); color: var(--status-muted); cursor: not-allowed; } .page-action--remove { grid-column: span 3; color: var(--tool-danger); } diff --git a/tools/shared/tool.css b/tools/shared/tool.css index 7b48d37..09257c2 100644 --- a/tools/shared/tool.css +++ b/tools/shared/tool.css @@ -7,6 +7,7 @@ .tool-intro > p:last-child { max-width: 43rem; margin-bottom: 0; color: var(--text-secondary); font-size: clamp(1rem, 2vw, 1.15rem); } .tool-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(18rem, 21rem); gap: var(--space-5); align-items: start; } +.tool-layout > *, .tool-workspace > *, .settings, .settings form, .field, .mode-option > span, .check-field > span, .tool-actions, .tool-status { min-width: 0; } .tool-workspace { display: grid; min-width: 0; gap: var(--space-5); } .drop-zone { position: relative; padding: clamp(2.5rem, 7vw, 4.75rem) var(--space-5); overflow: hidden; border: 1px dashed var(--border-strong); border-radius: var(--radius-lg); text-align: center; background: var(--bg-secondary); cursor: pointer; transition: border-color var(--transition-fast), background-color var(--transition-fast); } .drop-zone::before { display: grid; width: 3rem; height: 3rem; place-items: center; margin: 0 auto var(--space-4); border: 1px solid var(--border); border-radius: 50%; background: var(--bg-primary); color: var(--accent); content: "+"; font-size: 1.5rem; font-weight: 500; } @@ -22,8 +23,8 @@ .drop-zone .button { position: relative; pointer-events: none; } .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } .local-note { margin: var(--space-5) auto 0; padding-top: var(--space-4); border-top: 1px solid var(--border); font-size: 0.82rem; } -.local-note strong { color: var(--accent); } -.local-note a { position: relative; z-index: 2; color: var(--text-primary); font-weight: 700; } +.local-note strong { color: var(--accent); font-weight: var(--font-weight-bold); } +.local-note a { position: relative; z-index: 2; color: var(--text-primary); font-weight: var(--font-weight-bold); } .source-panel { padding: var(--space-5); } .source-panel__header { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-4); } @@ -37,9 +38,9 @@ .source-summary { margin: 0; color: var(--text-secondary); } .queue { min-width: 0; padding: var(--space-5); } -.queue-header { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-4); } +.queue-header { display: flex; min-width: 0; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-2) var(--space-4); margin-bottom: var(--space-4); } .queue-header h2 { margin: 0; font-size: 1.35rem; } -.queue-header span { color: var(--text-secondary); font-size: 0.8rem; font-weight: 700; } +.queue-header span { min-width: 0; color: var(--text-secondary); font-size: 0.8rem; font-weight: var(--font-weight-bold); overflow-wrap: anywhere; } .queue-empty { min-height: 11rem; display: grid; place-items: center; margin: 0; padding: var(--space-5); border: 1px dashed var(--border); border-radius: var(--radius-md); color: var(--text-secondary); text-align: center; } .file-list { display: grid; gap: var(--space-3); margin: 0; padding: 0; list-style: none; } .queue-empty[hidden] { display: none; } @@ -59,7 +60,7 @@ .settings h2 { margin-bottom: var(--space-5); padding-bottom: var(--space-4); border-bottom: 1px solid var(--border); font-size: 1.35rem; } .field { display: grid; gap: var(--space-2); margin-bottom: var(--space-4); } .field label { color: var(--text-secondary); font-size: 0.78rem; font-weight: var(--font-weight-bold); } -.field input, .field select { width: 100%; min-height: 2.75rem; padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-primary); color: var(--text-primary); transition: border-color var(--transition-fast), background-color var(--transition-theme); } +.field input, .field select { width: 100%; min-height: 2.75rem; padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-primary); color: var(--text-primary); line-height: var(--line-height-control); transition: border-color var(--transition-fast), background-color var(--transition-theme); } .field input:hover, .field select:hover { border-color: var(--border-strong); } .field-help { margin: 0; color: var(--text-secondary); font-size: 0.72rem; } .field-error { min-height: 1.25rem; margin: var(--space-2) 0 0; color: var(--tool-danger); font-size: 0.78rem; } @@ -68,7 +69,7 @@ .mode-option { display: flex; min-width: 0; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-primary); cursor: pointer; } .mode-option:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); } .mode-option input { flex: 0 0 auto; margin-top: 0.2rem; accent-color: var(--accent); } -.mode-option strong, .mode-option span { display: block; } +.mode-option strong, .mode-option span { display: block; overflow-wrap: anywhere; } .mode-option span { margin-top: var(--space-1); color: var(--text-secondary); font-size: 0.78rem; line-height: 1.4; } .mode-panel { max-width: 38rem; } .mode-panel[hidden] { display: none; } @@ -77,7 +78,7 @@ .tool-actions { display: grid; gap: var(--space-2); margin-top: var(--space-5); padding-top: var(--space-5); border-top: 1px solid var(--border); } .tool-actions .button { width: 100%; } .generation-progress { width: 100%; height: 0.55rem; margin-top: var(--space-4); accent-color: var(--accent); } -.tool-status { margin: var(--space-3) 0 0; padding: var(--space-3) var(--space-4); border: 1px solid var(--border); border-left-width: 0.25rem; border-radius: var(--radius-sm); background: var(--bg-subtle); color: var(--text-secondary); font-size: 0.82rem; line-height: 1.5; } +.tool-status { margin: var(--space-3) 0 0; padding: var(--space-3) var(--space-4); border: 1px solid var(--border); border-left-width: 0.25rem; border-radius: var(--radius-sm); background: var(--bg-subtle); color: var(--text-secondary); font-size: 0.82rem; line-height: 1.5; overflow-wrap: anywhere; } .tool-status:empty { display: none; } .tool-status[data-tone="error"] { border-color: var(--tool-danger); background: var(--tool-danger-soft); color: var(--tool-danger); } .tool-status[data-tone="warning"] { border-color: var(--tool-warning); background: var(--tool-warning-soft); color: var(--tool-warning); } From 31bbcd7f0c15c7567682e1d8601c33ab29135dc9 Mon Sep 17 00:00:00 2001 From: maruson08 Date: Thu, 20 Aug 2026 22:19:34 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9C=85[Test]=20Add=20homepage=20and=20i1?= =?UTF-8?q?8n=20layout=20regression=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/home-structure.test.mjs | 51 ++++++++++++++++++ tests/run-all.mjs | 2 + tests/typography-i18n-layout.test.mjs | 77 +++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 tests/home-structure.test.mjs create mode 100644 tests/typography-i18n-layout.test.mjs diff --git a/tests/home-structure.test.mjs b/tests/home-structure.test.mjs new file mode 100644 index 0000000..46aa485 --- /dev/null +++ b/tests/home-structure.test.mjs @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const read = (relative) => fs.readFileSync(path.join(root, relative), "utf8"); +const home = read("index.html"); + +function testCategoryFirstHomepage() { + assert.doesNotMatch(home, /available-tools|available-tools-title|tools\.available(?:Eyebrow|Title|Description)/); + assert.equal((home.match(/class="category-card surface"/g) || []).length, 5, "Homepage must retain five category entry points"); + const categoryHrefs = [...home.matchAll(/ match[1]); + assert.deepEqual(categoryHrefs, [ + "./tools/pdf/", + "./tools/image/", + "./tools/privacy/", + "./tools/scan/", + "./tools/media/", + ]); + + const flow = [ + 'class="hero container"', + 'id="tools"', + 'data-i18n="why.title"', + 'id="verify"', + 'data-i18n="openSource.title"', + ].map((marker) => home.indexOf(marker)); + assert.ok(flow.every((index) => index >= 0), "Homepage flow is missing a required section"); + assert.deepEqual([...flow].sort((a, b) => a - b), flow, "Homepage sections are out of order"); + assert.equal((home.match(/]*data-i18n="${key}"`), `${key} must remain a level-two section heading`); + } +} + +function testReadySectionCodeIsDeadFree() { + const retiredKeys = /availableEyebrow|availableTitle|availableDescription/; + for (const language of ["en", "ko", "ja", "es", "de", "fr"]) { + assert.doesNotMatch(read(`js/locales/${language}.js`), retiredKeys, `${language} retains a removed Ready-section key`); + } + for (const relative of ["css/pages.css", "css/components.css"]) { + const css = read(relative); + assert.doesNotMatch(css, /\.available-tools|\.tool-list|\.tool-row/, `${relative} retains Ready-section styles`); + } +} + +testCategoryFirstHomepage(); +testReadySectionCodeIsDeadFree(); + +console.log("Homepage category-first structure and retired Ready-section cleanup checks passed."); diff --git a/tests/run-all.mjs b/tests/run-all.mjs index 3a47482..af2ab41 100644 --- a/tests/run-all.mjs +++ b/tests/run-all.mjs @@ -29,6 +29,8 @@ for (const test of [ "tests/file-input-queue-state.test.mjs", "tests/pdf-split.test.mjs", "tests/security-hardening.test.mjs", + "tests/home-structure.test.mjs", + "tests/typography-i18n-layout.test.mjs", "tests/pdf-to-images.test.mjs", "tests/pdf-organizer.test.mjs", "tests/pdf-metadata.test.mjs", diff --git a/tests/typography-i18n-layout.test.mjs b/tests/typography-i18n-layout.test.mjs new file mode 100644 index 0000000..9871be8 --- /dev/null +++ b/tests/typography-i18n-layout.test.mjs @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const read = (relative) => fs.readFileSync(path.join(root, relative), "utf8"); + +function listFiles(directory, extension) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const target = path.join(directory, entry.name); + return entry.isDirectory() ? listFiles(target, extension) : target.endsWith(extension) ? [target] : []; + }); +} + +const cssFiles = [...listFiles(path.join(root, "css"), ".css"), ...listFiles(path.join(root, "tools"), ".css")]; +const css = cssFiles.map((file) => fs.readFileSync(file, "utf8")).join("\n"); +const base = read("css/base.css"); +const components = read("css/components.css"); +const categories = read("css/categories.css"); +const tools = read("tools/shared/tool.css"); +const metadata = read("tools/pdf/metadata/tool.css"); + +function testSystemFirstTypography() { + assert.match(base, /--font-sans:\s*system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif/); + for (const token of [ + "--font-size-xs", "--font-size-base", "--font-size-xl", "--line-height-heading", + "--line-height-body", "--line-height-control", "--font-weight-normal", "--font-weight-bold", + ]) assert.match(base, new RegExp(`${token}:`), `Missing typography token ${token}`); + assert.match(base, /html:lang\(ko\)\s*\{[^}]*Apple SD Gothic Neo[^}]*Malgun Gothic[^}]*Noto Sans KR/); + assert.match(base, /html:lang\(ja\)\s*\{[^}]*Hiragino Sans[^}]*Yu Gothic UI[^}]*Meiryo[^}]*Noto Sans JP/); + assert.doesNotMatch(css, /\bInter\b|@font-face|@import\s+url\([^)]*https?:|fonts\.(?:googleapis|gstatic)\.com/); + assert.doesNotMatch(css, /transform:\s*scale\(/, "Typography must not be normalized with scaling hacks"); + assert.doesNotMatch(css, /font-weight:\s*(?:650|750|780)\b/, "Shared UI must use supported system-font weights"); +} + +function testLocaleWrappingPolicy() { + assert.match(base, /html:lang\(ko\) :is\([^}]+word-break:\s*keep-all/); + assert.doesNotMatch(base, /html:lang\(ko\) body\s*\{[^}]*word-break:\s*keep-all/); + assert.equal((css.match(/word-break:\s*keep-all/g) || []).length, 1, "Korean keep-all must remain intentionally scoped"); + assert.match(base, /html:lang\(ja\)\s*\{[^}]*line-break:\s*strict/); + assert.doesNotMatch(css, /word-break:\s*break-all|white-space:\s*nowrap[^;}]*!important/); + + const nowrapRules = [...css.matchAll(/([^{}]+)\{[^{}]*white-space:\s*nowrap/g)].map((match) => match[1].trim()); + for (const selector of nowrapRules) { + assert.match(selector, /brand|visually-hidden|source-name|queue-name|metadata-table thead/, `Unexpected nowrap rule: ${selector}`); + } +} + +function testControlAndLongCopyLayout() { + assert.match(base, /body, button, input, select, textarea\s*\{\s*font:\s*inherit/); + assert.match(components, /\.button\s*\{[^}]*min-width:\s*0[^}]*min-height:\s*2\.75rem[^}]*line-height:\s*var\(--line-height-control\)[^}]*overflow-wrap:\s*anywhere/); + assert.match(components, /\.site-nav\s*\{[^}]*min-width:\s*0[^}]*flex-wrap:\s*wrap/); + assert.match(components, /\.header-inner > \*, \.header-controls > \*, \.control-label > span\s*\{\s*min-width:\s*0/); + assert.match(components, /@media \(max-width:\s*38rem\)[\s\S]*\.site-nav\s*\{[^}]*justify-content:\s*flex-start/); + assert.match(categories, /\.category-nav__inner\s*\{[^}]*overflow-x:\s*auto/); + assert.match(categories, /\.category-nav a\s*\{[^}]*min-height:\s*2\.75rem/); + assert.match(tools, /\.tool-layout > \*[^}]*min-width:\s*0/); + assert.match(tools, /\.mode-option strong, \.mode-option span\s*\{[^}]*overflow-wrap:\s*anywhere/); + assert.match(metadata, /\.metadata-value\s*\{[^}]*overflow-wrap:\s*anywhere[^}]*white-space:\s*pre-wrap/); + assert.match(base, /body\s*\{[^}]*min-width:\s*20rem/); +} + +function testNoRemoteFontResources() { + for (const file of listFiles(root, ".html")) { + const html = fs.readFileSync(file, "utf8"); + assert.doesNotMatch(html, /]+rel="stylesheet"[^>]+href="https?:/i, `${path.relative(root, file)} loads a remote stylesheet`); + assert.doesNotMatch(html, /]+src="https?:/i, `${path.relative(root, file)} loads a remote script`); + } +} + +testSystemFirstTypography(); +testLocaleWrappingPolicy(); +testControlAndLongCopyLayout(); +testNoRemoteFontResources(); + +console.log("System typography, CJK wrapping, long-copy layout, controls, and remote-font checks passed."); From 791db0f32a4a41491458872f0f60275e1949ed64 Mon Sep 17 00:00:00 2001 From: maruson08 Date: Thu, 20 Aug 2026 22:26:56 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=93=84[Docs]=20Document=20multilingua?= =?UTF-8?q?l=20typography=20audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++-- docs/i18n-copy-review.md | 14 +++++++++----- docs/ux-consistency-audit.md | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index accdb5d..0288b8b 100644 --- a/README.md +++ b/README.md @@ -96,16 +96,18 @@ The hub is a static site built with semantic HTML, CSS, and Vanilla JavaScript E │ └── pdf-lib/ └── tests/ ├── ci-foundation.test.mjs + ├── home-structure.test.mjs ├── image-to-pdf.test.mjs ├── pdf-merge-and-categories.test.mjs ├── pdf-split.test.mjs ├── pdf-metadata.test.mjs + ├── typography-i18n-layout.test.mjs └── run-all.mjs ``` `js/main.js` initializes shared theme and internationalization behavior. `js/theme.js` owns Light, Dark, and System selection, OS color-scheme observation, and persistence. `js/i18n.js` resolves all six supported languages from browser preferences, applies `data-i18n` bindings without reloading, updates metadata and ``, and gives persisted manual selection precedence. Repository links are centralized in `js/config.js`. -The concise [UX consistency audit](./docs/ux-consistency-audit.md) records the shared interaction patterns, resolved drift, and intentional tool-specific exceptions established before v1.0.0 hardening. +The concise [UX consistency audit](./docs/ux-consistency-audit.md) records the shared interaction patterns, category-first homepage follow-up, multilingual typography policy, resolved drift, and intentional tool-specific exceptions established before v1.0.0 hardening. The category hubs are: @@ -261,7 +263,7 @@ Run the complete local and CI validation entry point with: node tests/run-all.mjs ``` -It checks JavaScript syntax and runs Images to PDF, PDF Merge, PDF Split, PDF Organizer, PDF to Images, PDF Metadata, category route, six-language catalog parity, locale detection and persistence, selector and layout safeguards, static resource, privacy/network, security-hardening, ZIP, and CI workflow regression coverage. PDF fixtures are generated deterministically during tests; CI never processes real user files. +It checks JavaScript syntax and runs Images to PDF, PDF Merge, PDF Split, PDF Organizer, PDF to Images, PDF Metadata, category-first homepage, system typography, CJK wrapping, long-copy layout, six-language catalog parity and placeholders, locale detection and persistence, static resource, privacy/network, security-hardening, ZIP, and CI workflow regression coverage. PDF fixtures are generated deterministically during tests; CI never processes real user files. ## Production security controls diff --git a/docs/i18n-copy-review.md b/docs/i18n-copy-review.md index 89b35f7..be143e1 100644 --- a/docs/i18n-copy-review.md +++ b/docs/i18n-copy-review.md @@ -4,7 +4,7 @@ This record describes the structured source-level copy review completed for Spri ## Catalog controls -- [x] Every catalog contains the same 525 leaf keys as English. +- [x] Every catalog contains the same 522 leaf keys as English after retiring three homepage-only keys. - [x] Every interpolation placeholder matches the English source key. - [x] No catalog contains `TODO`, `TRANSLATE`, `TBD`, or `[English]` draft markers. - [x] Production HTML references resolve to catalog keys. @@ -12,7 +12,7 @@ This record describes the structured source-level copy review completed for Spri ## Korean review -All 525 Korean production strings were inspected by namespace against their English meaning and their binding context. The review covered: +All 522 Korean production strings were inspected by namespace against their English meaning and their binding context. The review covered: - [x] Global navigation, theme, language, and shared privacy controls - [x] Homepage hero, directory, principles, verification, and source sections @@ -50,12 +50,16 @@ Initial drafts for these four catalogs were machine-assisted during development, ## Layout and behavior review +The reviewed Korean, Japanese, Spanish, German, and French corrections supplied before Sprint 10.5 are stored directly in the runtime-imported locale modules. English remains the reference catalog and was not rewritten during the typography pass. + - [x] Native language names are used without flag-only identification. - [x] The shared selector contains exactly `en`, `ko`, `ja`, `es`, `de`, and `fr` on every production page. -- [x] Korean uses phrase-preserving line breaks and Japanese uses strict line-breaking behavior. -- [x] Latin, Hangul, and Japanese system-font fallbacks are present; no remote font was added. +- [x] Korean phrase-preserving breaks are scoped to headings, short descriptions, labels, badges, and controls; filenames and technical values remain free to wrap safely. +- [x] Japanese uses the browser's natural wrapping with strict line-breaking behavior and does not inherit Korean `keep-all`. +- [x] Latin languages share a system-first stack; Korean and Japanese add deliberate installed-font fallbacks. No font file, remote stylesheet, or runtime font request was added. +- [x] Shared heading, body, control, size, and weight tokens avoid unsupported intermediate weights that can synthesize inconsistently across system fonts. - [x] Flex and grid children that carry localized text can shrink, and the header wraps before controls collide. -- [x] Narrow-screen controls stack without truncating critical action text. +- [x] German and French long labels wrap or stack at narrow widths without global `nowrap`, `break-all`, or text-scaling workarounds. - [x] Changing languages re-renders dynamic tools without clearing their application state. - [x] The selected language persists and overrides browser detection. - [x] `` and localized metadata update on every language change. diff --git a/docs/ux-consistency-audit.md b/docs/ux-consistency-audit.md index cd6c766..5daa9ee 100644 --- a/docs/ux-consistency-audit.md +++ b/docs/ux-consistency-audit.md @@ -48,6 +48,22 @@ Sprint 10 audited the homepage, About, Privacy, all five category hubs, the PDF - Tools continue to save through the established picker/download fallback. A separate “Ready to save” panel would add a second action without improving the existing flow. - Category navigation may scroll horizontally on very narrow screens by design; the page body itself must not overflow. +## Sprint 10.5 homepage and typography follow-up + +- The duplicate **Ready to use / Available now** homepage block was removed. The homepage now flows from Hero to Tool Categories, Why Secure Tools, Verify Privacy, Open Source, and Footer, with category cards as its single tool-entry model. +- The three retired homepage-only locale keys and all associated DOM and CSS rules were deleted from every catalog and source surface. +- Typography is system-first. Latin languages use `system-ui`, Apple, BlinkMacSystemFont, and Segoe UI fallbacks; Korean adds Apple SD Gothic Neo, Malgun Gothic, and Noto Sans KR; Japanese adds Hiragino Sans, Yu Gothic UI/Yu Gothic, Meiryo, and Noto Sans JP. +- No external font, local webfont, font CDN, `@font-face`, framework, build dependency, or CSP exception was introduced. +- Shared typography tokens now define common size, line-height, letter-spacing, and supported weight steps. Buttons, inputs, selects, and textareas inherit the active locale stack and line height. +- Korean `keep-all` is limited to short editorial UI text. Filenames, metadata, URLs, and technical values retain safe overflow behavior. +- Japanese keeps natural wrapping with strict line-breaking behavior rather than inheriting the Korean policy. +- Header links, category tabs, cards, controls, settings, statuses, Metadata values, and Organizer actions can shrink, wrap, or stack for German and French long copy without arbitrary font reduction. +- Category-tab horizontal scrolling and white PDF preview canvases remain intentional exceptions. + +Structural tests cover the category-first homepage, dead-key removal, six-locale parity and placeholders, system font stacks, CJK wrapping scope, long-copy layout invariants, control inheritance, and remote-font absence. + +Browser automation remained unavailable because the Windows helper failed under the known sandbox ACL restriction. Automated DOM/CSS checks were strengthened, but human review at 320, 360, 390, 768, 1024, and 1280+ CSS pixels—including German/French at 320 and Korean/Japanese at 360—remains required before v1.0.0. + ## Validation boundary Automated checks cover architecture, semantics, resources, six-language keys, known overflow anti-patterns, CSP, and functional regressions. Browser automation remained subject to the known Windows sandbox ACL limitation during this Sprint. If visual control is unavailable, real human browser review at 360, 390, 768, 1024, and 1440 CSS pixels in Light/Dark themes remains required before v1.0.0 release approval.