feat(editor): translate InstallLibrariesModal + SerialMonitor (Editor block 8)

InstallLibrariesModal — auto-install prompt that fires when an
example needs libraries:
- Title, subtitle (with the "Installing X of Y" progress
  interpolation, the all-done success state, and the singular /
  plural prompt explaining the requirement count).
- Per-row status badges (pending / installing… / installed /
  error) plus the Wokwi-hosted-library tooltip.
- Footer buttons: Close / Skip / "Install All ({{count}})" with
  loading variant.

SerialMonitor — multi-board tabbed serial console:
- Empty-state when no board is on the canvas.
- Right-side tab controls: Autoscroll checkbox, Clear button +
  tooltip.
- The "(Open IoT Gateway)" inline link rendered next to detected
  AP IP addresses.
- Output-area placeholders for the running-but-no-data and
  before-start states.
- Send button + input placeholder (different copy for MicroPython
  REPL vs raw Serial input).
- The line-ending dropdown options (None / Newline / Carriage
  return / Both).

Hand-translated for all 9 locales. Hotkeys (Ctrl+C) and dropdown
values stay untranslated (constants the firmware reads).

Pending in Phase 3:
- Oscilloscope panel, custom-chip dialog, sensor control panel.
- ComponentPropertyDialog (per-component property forms).
- Admin / Profile / Project pages.
- Long-form docs prose (DocsPage 2715 lines, AboutPage long
  paragraphs).
- 15 SEO landing pages (intentionally English for keyword targeting).
This commit is contained in:
David Montero Crespo 2026-05-09 11:35:22 -03:00
parent c1b0398c0d
commit ea290d0c22
11 changed files with 360 additions and 32 deletions

View File

@ -1,4 +1,5 @@
import React, { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { installLibrary } from '../../services/libraryService';
import './InstallLibrariesModal.css';
@ -58,6 +59,7 @@ export const InstallLibrariesModal: React.FC<InstallLibrariesModalProps> = ({
onClose,
libraries,
}) => {
const { t } = useTranslation();
const [items, setItems] = useState<LibItem[]>(() =>
libraries.map((spec) => {
const { name, version } = parseLibSpec(spec);
@ -134,7 +136,7 @@ export const InstallLibrariesModal: React.FC<InstallLibrariesModalProps> = ({
<path d="m3.3 7 8.7 5 8.7-5" />
<path d="M12 22V12" />
</svg>
<span>REQUIRED LIBRARIES</span>
<span>{t('editor.installLibs.title')}</span>
</div>
<button className="ilib-close-btn" onClick={onClose} disabled={running}>
<svg
@ -157,15 +159,15 @@ export const InstallLibrariesModal: React.FC<InstallLibrariesModalProps> = ({
{running ? (
<span className="ilib-subtitle-installing">
<Spinner size={13} />
Installing {doneCount + 1} of {items.length}
{t('editor.installLibs.installingProgress', {
done: doneCount + 1,
total: items.length,
})}
</span>
) : allDone ? (
<span className="ilib-subtitle-done">All libraries installed successfully</span>
<span className="ilib-subtitle-done">{t('editor.installLibs.allDone')}</span>
) : (
<span>
This project requires {items.length} {items.length === 1 ? 'library' : 'libraries'}.
Install them to compile correctly.
</span>
<span>{t('editor.installLibs.required', { count: items.length })}</span>
)}
</div>
@ -181,19 +183,19 @@ export const InstallLibrariesModal: React.FC<InstallLibrariesModalProps> = ({
<span className="ilib-version">v{item.version}</span>
)}
{isWokwiLib && (
<span className="ilib-badge ilib-badge--wokwi" title="Wokwi-hosted library">
<span className="ilib-badge ilib-badge--wokwi" title={t('editor.installLibs.wokwiHosted')}>
wokwi
</span>
)}
</span>
<span className="ilib-item-status">
{item.status === 'pending' && (
<span className="ilib-badge ilib-badge--pending">pending</span>
<span className="ilib-badge ilib-badge--pending">{t('editor.installLibs.pending')}</span>
)}
{item.status === 'installing' && (
<span className="ilib-badge ilib-badge--installing">
<Spinner size={12} />
installing
{t('editor.installLibs.installing')}
</span>
)}
{item.status === 'done' && (
@ -210,7 +212,7 @@ export const InstallLibrariesModal: React.FC<InstallLibrariesModalProps> = ({
>
<polyline points="20 6 9 17 4 12" />
</svg>
installed
{t('editor.installLibs.installed')}
</span>
)}
{item.status === 'error' && (
@ -227,7 +229,7 @@ export const InstallLibrariesModal: React.FC<InstallLibrariesModalProps> = ({
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
error
{t('editor.installLibs.errorStatus')}
</span>
)}
</span>
@ -240,12 +242,12 @@ export const InstallLibrariesModal: React.FC<InstallLibrariesModalProps> = ({
<div className="ilib-footer">
{allDone ? (
<button className="ilib-btn ilib-btn--primary" onClick={onClose}>
Close
{t('editor.installLibs.close')}
</button>
) : (
<>
<button className="ilib-btn ilib-btn--ghost" onClick={onClose} disabled={running}>
Skip
{t('editor.installLibs.skip')}
</button>
<button
className="ilib-btn ilib-btn--primary"
@ -255,10 +257,10 @@ export const InstallLibrariesModal: React.FC<InstallLibrariesModalProps> = ({
{running ? (
<>
<Spinner size={14} />
Installing
{t('editor.installLibs.installingShort')}
</>
) : (
`Install All (${pendingCount})`
t('editor.installLibs.installAll', { count: pendingCount })
)}
</button>
</>

View File

@ -4,6 +4,7 @@
*/
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useSimulatorStore } from '../../store/useSimulatorStore';
import { getTabSessionId } from '../../simulation/Esp32Bridge';
import type { BoardKind } from '../../types/board';
@ -71,6 +72,7 @@ const BOARD_COLOR: Partial<Record<string, string>> = {
};
export const SerialMonitor: React.FC = () => {
const { t } = useTranslation();
const boards = useSimulatorStore((s) => s.boards);
const activeBoardId = useSimulatorStore((s) => s.activeBoardId);
const serialWriteToBoard = useSimulatorStore((s) => s.serialWriteToBoard);
@ -176,9 +178,9 @@ export const SerialMonitor: React.FC = () => {
return (
<div style={styles.container}>
<div style={styles.header}>
<span style={styles.title}>Serial Monitor</span>
<span style={styles.title}>{t('editor.serial.title')}</span>
</div>
<pre style={styles.output}>Add a board to start the serial monitor.</pre>
<pre style={styles.output}>{t('editor.serial.addBoard')}</pre>
</div>
);
}
@ -231,14 +233,14 @@ export const SerialMonitor: React.FC = () => {
onChange={(e) => setAutoscroll(e.target.checked)}
style={styles.checkbox}
/>
Autoscroll
{t('editor.serial.autoscroll')}
</label>
<button
onClick={() => resolvedTabId && clearBoardSerialOutput(resolvedTabId)}
style={styles.clearBtn}
title="Clear output for this board"
title={t('editor.serial.clearTitle')}
>
Clear
{t('editor.serial.clear')}
</button>
</div>
</div>
@ -284,9 +286,9 @@ export const SerialMonitor: React.FC = () => {
fontWeight: 'bold',
cursor: 'pointer',
}}
title="Click to open through IoT Gateway"
title={t('editor.serial.iotGatewayTitle')}
>
{m[0]} (Open IoT Gateway )
{m[0]} ({t('editor.serial.openIotGateway')} )
</a>,
);
lastIdx = end;
@ -297,8 +299,8 @@ export const SerialMonitor: React.FC = () => {
return text;
})()
: activeBoard?.running
? 'Waiting for serial data...\n'
: 'Start simulation to see serial output.\n'}
? t('editor.serial.waitingData') + '\n'
: t('editor.serial.startSim') + '\n'}
</pre>
{/* Input row */}
@ -310,8 +312,8 @@ export const SerialMonitor: React.FC = () => {
onKeyDown={handleKeyDown}
placeholder={
isMicroPython
? 'Type Python expression... (Ctrl+C to interrupt)'
: 'Type message to send...'
? t('editor.serial.placeholderPython')
: t('editor.serial.placeholderText')
}
style={styles.input}
disabled={!activeBoard?.running}
@ -321,13 +323,13 @@ export const SerialMonitor: React.FC = () => {
onChange={(e) => setLineEnding(e.target.value as typeof lineEnding)}
style={styles.select}
>
<option value="none">No line ending</option>
<option value="nl">Newline</option>
<option value="cr">Carriage return</option>
<option value="both">Both NL &amp; CR</option>
<option value="none">{t('editor.serial.lineEnd.none')}</option>
<option value="nl">{t('editor.serial.lineEnd.nl')}</option>
<option value="cr">{t('editor.serial.lineEnd.cr')}</option>
<option value="both">{t('editor.serial.lineEnd.both')}</option>
</select>
<button onClick={handleSend} disabled={!activeBoard?.running} style={styles.sendBtn}>
Send
{t('editor.serial.send')}
</button>
</div>
</div>

View File

@ -333,6 +333,42 @@
"loadingInstalled": "Installierte Bibliotheken werden geladen...",
"noneInstalled": "Noch keine Bibliotheken installiert.",
"useSearchTab": "Nutze den Tab Suchen, um Bibliotheken zu installieren."
},
"installLibs": {
"title": "ERFORDERLICHE BIBLIOTHEKEN",
"installingProgress": "Installiere {{done}} von {{total}}…",
"allDone": "Alle Bibliotheken erfolgreich installiert",
"required_one": "Dieses Projekt benötigt {{count}} Bibliothek. Installiere sie, um korrekt zu kompilieren.",
"required_other": "Dieses Projekt benötigt {{count}} Bibliotheken. Installiere sie, um korrekt zu kompilieren.",
"wokwiHosted": "Von Wokwi gehostete Bibliothek",
"pending": "ausstehend",
"installing": "installiere…",
"installed": "installiert",
"errorStatus": "Fehler",
"close": "Schließen",
"skip": "Überspringen",
"installAll": "Alle installieren ({{count}})",
"installingShort": "Installiere…"
},
"serial": {
"title": "Serial Monitor",
"addBoard": "Füge ein Board hinzu, um den Serial Monitor zu starten.",
"autoscroll": "Auto-Scroll",
"clearTitle": "Ausgabe dieses Boards löschen",
"clear": "Löschen",
"send": "Senden",
"iotGatewayTitle": "Klicken, um über IoT Gateway zu öffnen",
"openIotGateway": "IoT Gateway öffnen",
"waitingData": "Warte auf serielle Daten...",
"startSim": "Starte die Simulation, um serielle Ausgabe zu sehen.",
"placeholderText": "Nachricht zum Senden eingeben...",
"placeholderPython": "Python-Ausdruck eingeben... (Strg+C zum Abbrechen)",
"lineEnd": {
"none": "Kein Zeilenende",
"nl": "Zeilenumbruch",
"cr": "Wagenrücklauf",
"both": "NL und CR"
}
}
}
}

View File

@ -333,6 +333,42 @@
"loadingInstalled": "Loading installed libraries...",
"noneInstalled": "No libraries installed yet.",
"useSearchTab": "Use the Search tab to install libraries."
},
"installLibs": {
"title": "REQUIRED LIBRARIES",
"installingProgress": "Installing {{done}} of {{total}}…",
"allDone": "All libraries installed successfully",
"required_one": "This project requires {{count}} library. Install it to compile correctly.",
"required_other": "This project requires {{count}} libraries. Install them to compile correctly.",
"wokwiHosted": "Wokwi-hosted library",
"pending": "pending",
"installing": "installing…",
"installed": "installed",
"errorStatus": "error",
"close": "Close",
"skip": "Skip",
"installAll": "Install All ({{count}})",
"installingShort": "Installing…"
},
"serial": {
"title": "Serial Monitor",
"addBoard": "Add a board to start the serial monitor.",
"autoscroll": "Autoscroll",
"clearTitle": "Clear output for this board",
"clear": "Clear",
"send": "Send",
"iotGatewayTitle": "Click to open through IoT Gateway",
"openIotGateway": "Open IoT Gateway",
"waitingData": "Waiting for serial data...",
"startSim": "Start simulation to see serial output.",
"placeholderText": "Type message to send...",
"placeholderPython": "Type Python expression... (Ctrl+C to interrupt)",
"lineEnd": {
"none": "No line ending",
"nl": "Newline",
"cr": "Carriage return",
"both": "Both NL & CR"
}
}
}
}

View File

@ -333,6 +333,42 @@
"loadingInstalled": "Cargando librerías instaladas...",
"noneInstalled": "Aún no hay librerías instaladas.",
"useSearchTab": "Usa la pestaña Buscar para instalar librerías."
},
"installLibs": {
"title": "LIBRERÍAS REQUERIDAS",
"installingProgress": "Instalando {{done}} de {{total}}…",
"allDone": "Todas las librerías se instalaron correctamente",
"required_one": "Este proyecto requiere {{count}} librería. Instálala para compilar correctamente.",
"required_other": "Este proyecto requiere {{count}} librerías. Instálalas para compilar correctamente.",
"wokwiHosted": "Librería alojada en Wokwi",
"pending": "pendiente",
"installing": "instalando…",
"installed": "instalada",
"errorStatus": "error",
"close": "Cerrar",
"skip": "Omitir",
"installAll": "Instalar todas ({{count}})",
"installingShort": "Instalando…"
},
"serial": {
"title": "Monitor serie",
"addBoard": "Añade una placa para iniciar el monitor serie.",
"autoscroll": "Autoscroll",
"clearTitle": "Limpiar la salida de esta placa",
"clear": "Limpiar",
"send": "Enviar",
"iotGatewayTitle": "Haz clic para abrir a través de IoT Gateway",
"openIotGateway": "Abrir IoT Gateway",
"waitingData": "Esperando datos serie...",
"startSim": "Inicia la simulación para ver la salida serie.",
"placeholderText": "Escribe un mensaje para enviar...",
"placeholderPython": "Escribe expresión Python... (Ctrl+C para interrumpir)",
"lineEnd": {
"none": "Sin terminador de línea",
"nl": "Nueva línea",
"cr": "Retorno de carro",
"both": "Ambos NL y CR"
}
}
}
}

View File

@ -333,6 +333,42 @@
"loadingInstalled": "Chargement des bibliothèques installées...",
"noneInstalled": "Aucune bibliothèque installée pour le moment.",
"useSearchTab": "Utilisez l'onglet Rechercher pour installer des bibliothèques."
},
"installLibs": {
"title": "BIBLIOTHÈQUES REQUISES",
"installingProgress": "Installation {{done}} sur {{total}}…",
"allDone": "Toutes les bibliothèques ont été installées avec succès",
"required_one": "Ce projet nécessite {{count}} bibliothèque. Installez-la pour compiler correctement.",
"required_other": "Ce projet nécessite {{count}} bibliothèques. Installez-les pour compiler correctement.",
"wokwiHosted": "Bibliothèque hébergée par Wokwi",
"pending": "en attente",
"installing": "installation…",
"installed": "installée",
"errorStatus": "erreur",
"close": "Fermer",
"skip": "Ignorer",
"installAll": "Tout installer ({{count}})",
"installingShort": "Installation…"
},
"serial": {
"title": "Moniteur série",
"addBoard": "Ajoutez une carte pour démarrer le moniteur série.",
"autoscroll": "Défilement auto",
"clearTitle": "Effacer la sortie de cette carte",
"clear": "Effacer",
"send": "Envoyer",
"iotGatewayTitle": "Cliquez pour ouvrir via IoT Gateway",
"openIotGateway": "Ouvrir IoT Gateway",
"waitingData": "En attente de données série...",
"startSim": "Démarrez la simulation pour voir la sortie série.",
"placeholderText": "Tapez un message à envoyer...",
"placeholderPython": "Tapez une expression Python... (Ctrl+C pour interrompre)",
"lineEnd": {
"none": "Aucune fin de ligne",
"nl": "Nouvelle ligne",
"cr": "Retour chariot",
"both": "NL et CR"
}
}
}
}

View File

@ -333,6 +333,42 @@
"loadingInstalled": "Caricamento librerie installate...",
"noneInstalled": "Nessuna libreria ancora installata.",
"useSearchTab": "Usa la scheda Cerca per installare librerie."
},
"installLibs": {
"title": "LIBRERIE RICHIESTE",
"installingProgress": "Installazione {{done}} di {{total}}…",
"allDone": "Tutte le librerie sono state installate correttamente",
"required_one": "Questo progetto richiede {{count}} libreria. Installala per compilare correttamente.",
"required_other": "Questo progetto richiede {{count}} librerie. Installale per compilare correttamente.",
"wokwiHosted": "Libreria ospitata da Wokwi",
"pending": "in attesa",
"installing": "installazione…",
"installed": "installata",
"errorStatus": "errore",
"close": "Chiudi",
"skip": "Salta",
"installAll": "Installa tutte ({{count}})",
"installingShort": "Installazione…"
},
"serial": {
"title": "Monitor seriale",
"addBoard": "Aggiungi una scheda per avviare il monitor seriale.",
"autoscroll": "Autoscroll",
"clearTitle": "Cancella l'output di questa scheda",
"clear": "Cancella",
"send": "Invia",
"iotGatewayTitle": "Clic per aprire tramite IoT Gateway",
"openIotGateway": "Apri IoT Gateway",
"waitingData": "In attesa di dati seriali...",
"startSim": "Avvia la simulazione per vedere l'output seriale.",
"placeholderText": "Scrivi un messaggio da inviare...",
"placeholderPython": "Scrivi espressione Python... (Ctrl+C per interrompere)",
"lineEnd": {
"none": "Nessun fine riga",
"nl": "Newline",
"cr": "Ritorno carrello",
"both": "Entrambi NL e CR"
}
}
}
}

View File

@ -329,6 +329,41 @@
"loadingInstalled": "インストール済みライブラリを読み込み中...",
"noneInstalled": "まだライブラリはインストールされていません。",
"useSearchTab": "ライブラリをインストールするには「検索」タブを使用してください。"
},
"installLibs": {
"title": "必要なライブラリ",
"installingProgress": "インストール中 {{done}}/{{total}}…",
"allDone": "すべてのライブラリが正常にインストールされました",
"required_other": "このプロジェクトには {{count}} 個のライブラリが必要です。正しくコンパイルするためにインストールしてください。",
"wokwiHosted": "Wokwi 提供ライブラリ",
"pending": "待機中",
"installing": "インストール中…",
"installed": "インストール済み",
"errorStatus": "エラー",
"close": "閉じる",
"skip": "スキップ",
"installAll": "すべてインストール({{count}}",
"installingShort": "インストール中…"
},
"serial": {
"title": "シリアルモニタ",
"addBoard": "シリアルモニタを開始するにはボードを追加してください。",
"autoscroll": "自動スクロール",
"clearTitle": "このボードの出力をクリア",
"clear": "クリア",
"send": "送信",
"iotGatewayTitle": "クリックで IoT Gateway 経由で開く",
"openIotGateway": "IoT Gateway を開く",
"waitingData": "シリアルデータを待機中...",
"startSim": "シミュレーションを開始するとシリアル出力が表示されます。",
"placeholderText": "送信するメッセージを入力...",
"placeholderPython": "Python 式を入力...Ctrl+C で中断)",
"lineEnd": {
"none": "改行なし",
"nl": "LF改行",
"cr": "CR復帰",
"both": "NL と CR"
}
}
}
}

View File

@ -333,6 +333,42 @@
"loadingInstalled": "Carregando bibliotecas instaladas...",
"noneInstalled": "Nenhuma biblioteca instalada ainda.",
"useSearchTab": "Use a aba Buscar para instalar bibliotecas."
},
"installLibs": {
"title": "BIBLIOTECAS NECESSÁRIAS",
"installingProgress": "Instalando {{done}} de {{total}}…",
"allDone": "Todas as bibliotecas foram instaladas com sucesso",
"required_one": "Este projeto requer {{count}} biblioteca. Instale para compilar corretamente.",
"required_other": "Este projeto requer {{count}} bibliotecas. Instale-as para compilar corretamente.",
"wokwiHosted": "Biblioteca hospedada na Wokwi",
"pending": "pendente",
"installing": "instalando…",
"installed": "instalada",
"errorStatus": "erro",
"close": "Fechar",
"skip": "Pular",
"installAll": "Instalar todas ({{count}})",
"installingShort": "Instalando…"
},
"serial": {
"title": "Monitor serial",
"addBoard": "Adicione uma placa para iniciar o monitor serial.",
"autoscroll": "Autoscroll",
"clearTitle": "Limpar a saída desta placa",
"clear": "Limpar",
"send": "Enviar",
"iotGatewayTitle": "Clique para abrir via IoT Gateway",
"openIotGateway": "Abrir IoT Gateway",
"waitingData": "Aguardando dados serial...",
"startSim": "Inicie a simulação para ver a saída serial.",
"placeholderText": "Digite uma mensagem para enviar...",
"placeholderPython": "Digite expressão Python... (Ctrl+C para interromper)",
"lineEnd": {
"none": "Sem terminador de linha",
"nl": "Nova linha",
"cr": "Retorno de carro",
"both": "Ambos NL e CR"
}
}
}
}

View File

@ -341,6 +341,44 @@
"loadingInstalled": "Загрузка установленных библиотек...",
"noneInstalled": "Пока нет установленных библиотек.",
"useSearchTab": "Используйте вкладку Поиск, чтобы установить библиотеки."
},
"installLibs": {
"title": "ТРЕБУЕМЫЕ БИБЛИОТЕКИ",
"installingProgress": "Установка {{done}} из {{total}}…",
"allDone": "Все библиотеки успешно установлены",
"required_one": "Этому проекту нужна {{count}} библиотека. Установите её, чтобы корректно скомпилировать.",
"required_few": "Этому проекту нужны {{count}} библиотеки. Установите их, чтобы корректно скомпилировать.",
"required_many": "Этому проекту нужны {{count}} библиотек. Установите их, чтобы корректно скомпилировать.",
"required_other": "Этому проекту нужны {{count}} библиотек. Установите их, чтобы корректно скомпилировать.",
"wokwiHosted": "Библиотека на серверах Wokwi",
"pending": "в очереди",
"installing": "устанавливается…",
"installed": "установлена",
"errorStatus": "ошибка",
"close": "Закрыть",
"skip": "Пропустить",
"installAll": "Установить всё ({{count}})",
"installingShort": "Установка…"
},
"serial": {
"title": "Serial Monitor",
"addBoard": "Добавьте плату, чтобы запустить Serial Monitor.",
"autoscroll": "Автопрокрутка",
"clearTitle": "Очистить вывод для этой платы",
"clear": "Очистить",
"send": "Отправить",
"iotGatewayTitle": "Нажмите, чтобы открыть через IoT Gateway",
"openIotGateway": "Открыть IoT Gateway",
"waitingData": "Ожидание serial-данных...",
"startSim": "Запустите симуляцию, чтобы увидеть serial-вывод.",
"placeholderText": "Введите сообщение для отправки...",
"placeholderPython": "Введите Python-выражение... (Ctrl+C — прерывание)",
"lineEnd": {
"none": "Без перевода строки",
"nl": "Newline",
"cr": "Carriage return",
"both": "NL и CR"
}
}
}
}

View File

@ -329,6 +329,41 @@
"loadingInstalled": "正在加载已安装的库...",
"noneInstalled": "尚未安装任何库。",
"useSearchTab": "请在「搜索」标签中安装库。"
},
"installLibs": {
"title": "所需的库",
"installingProgress": "正在安装 {{done}}/{{total}}…",
"allDone": "所有库已成功安装",
"required_other": "本项目需要 {{count}} 个库。请安装以便正确编译。",
"wokwiHosted": "由 Wokwi 托管的库",
"pending": "待安装",
"installing": "正在安装…",
"installed": "已安装",
"errorStatus": "错误",
"close": "关闭",
"skip": "跳过",
"installAll": "全部安装({{count}}",
"installingShort": "正在安装…"
},
"serial": {
"title": "串口监视器",
"addBoard": "请先添加开发板以启动串口监视器。",
"autoscroll": "自动滚动",
"clearTitle": "清空此开发板的输出",
"clear": "清空",
"send": "发送",
"iotGatewayTitle": "点击通过 IoT Gateway 打开",
"openIotGateway": "打开 IoT Gateway",
"waitingData": "等待串口数据...",
"startSim": "启动仿真以查看串口输出。",
"placeholderText": "输入要发送的消息...",
"placeholderPython": "输入 Python 表达式...(按 Ctrl+C 中断)",
"lineEnd": {
"none": "无行结束符",
"nl": "换行符",
"cr": "回车符",
"both": "换行 + 回车"
}
}
}
}