From 0d1dca3e0ae13efc2747348fbee873f676210822 Mon Sep 17 00:00:00 2001 From: Yury Pekishev Date: Thu, 30 Oct 2025 14:40:59 +0300 Subject: [PATCH 1/3] translateRuToEnWithPunctuation --- web/src/pages/desktop/menu/keyboard/paste.tsx | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/web/src/pages/desktop/menu/keyboard/paste.tsx b/web/src/pages/desktop/menu/keyboard/paste.tsx index 382d66e..a109c07 100644 --- a/web/src/pages/desktop/menu/keyboard/paste.tsx +++ b/web/src/pages/desktop/menu/keyboard/paste.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useRef, useState } from 'react'; +import { ChangeEvent, useRef, useState } from 'react'; import { Button, Input, Modal, type InputRef } from 'antd'; import clsx from 'clsx'; import { useSetAtom } from 'jotai'; @@ -24,15 +24,55 @@ export const Paste = () => { function onChange(e: ChangeEvent) { const value = e.target.value; - setStatus(isASCII(value) ? '' : 'error'); + setStatus(isAsciiOrRU(value) ? '' : 'error'); setInputValue(value); } + // Extended RU → EN translation including punctuation (applies only if Cyrillic is present) + function translateRuToEnWithPunctuation(value: string): string { + const hasCyrillic = /[А-Яа-яЁё]/.test(value); + + const letterMap: Record = { + 'ё': '`', + 'й': 'q', 'ц': 'w', 'у': 'e', 'к': 'r', 'е': 't', 'н': 'y', 'г': 'u', 'ш': 'i', + 'щ': 'o', 'з': 'p', 'х': '[', 'ъ': ']', + 'ф': 'a', 'ы': 's', 'в': 'd', 'а': 'f', 'п': 'g', 'р': 'h', 'о': 'j', 'л': 'k', + 'д': 'l', 'ж': ';', 'э': '\'', + 'я': 'z', 'ч': 'x', 'с': 'c', 'м': 'v', 'и': 'b', 'т': 'n', 'ь': 'm', 'б': ',', 'ю': '.', + }; + + const punctuationMap: Record = { + '"': '@', + '№': '#', + ';': '$', + ':': '^', + '?': '&', + 'Ё': '~', + '/': '|', + '.': '/', + ',': '?', + }; + + return Array.from(value).map((ch) => { + const lower = ch.toLowerCase(); + if (letterMap[lower]) { + const translated = letterMap[lower]; + return ch === lower ? translated : translated.toUpperCase(); + } + if (hasCyrillic && punctuationMap[ch]) { + return punctuationMap[ch]; + } + return ch; + }).join(''); + } + function submit() { if (isLoading) return; setIsLoading(true); - paste(inputValue) + const translated = translateRuToEnWithPunctuation(inputValue); + + paste(translated) .then((rsp) => { if (rsp.code !== 0) { setErrMsg(rsp.msg); @@ -57,11 +97,20 @@ export const Paste = () => { setIsKeyboardEnable(!open); } - function isASCII(value: string) { - for (let i = 0; i < value.length; i++) { - if (value.charCodeAt(i) > 127) { - return false; + function isAsciiOrRU(value: string) { + // Allow ASCII and Russian Cyrillic letters + for (const ch of value) { + const code = ch.codePointAt(0) ?? 0; + if (code <= 0x7F) continue; // ASCII + if ( + (code >= 0x0410 && code <= 0x042F) || // (А-Я) + (code >= 0x0430 && code <= 0x044F) || // (а-я) + code === 0x0401 || // (Ё) + code === 0x0451 // (ё) + ) { + continue; } + return false; } return true; } From 1cc31c30a34bd6baa382fa05b8dc2e3090034896 Mon Sep 17 00:00:00 2001 From: Yury Pekishev Date: Tue, 9 Dec 2025 00:56:52 +0300 Subject: [PATCH 2/3] revert invisible change --- web/src/pages/desktop/menu/keyboard/paste.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/desktop/menu/keyboard/paste.tsx b/web/src/pages/desktop/menu/keyboard/paste.tsx index d6e99e2..354197d 100644 --- a/web/src/pages/desktop/menu/keyboard/paste.tsx +++ b/web/src/pages/desktop/menu/keyboard/paste.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent, useEffect, useRef, useState } from 'react'; +import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { Button, Divider, Input, Modal, Select } from 'antd'; import type { InputRef } from 'antd'; import clsx from 'clsx'; From 6b783677e59334b177b96d84cce2dc72ea95716d Mon Sep 17 00:00:00 2001 From: Yury Pekishev Date: Tue, 9 Dec 2025 01:31:02 +0300 Subject: [PATCH 3/3] Use Dropdown with languages at paste Fix translation --- server/service/hid/paste.go | 16 +++---- web/src/i18n/locales/de.ts | 3 +- web/src/i18n/locales/en.ts | 3 +- web/src/i18n/locales/ru.ts | 5 +- web/src/pages/desktop/menu/keyboard/paste.tsx | 47 +++++++++++-------- 5 files changed, 44 insertions(+), 30 deletions(-) diff --git a/server/service/hid/paste.go b/server/service/hid/paste.go index 1f8e701..587e1b0 100644 --- a/server/service/hid/paste.go +++ b/server/service/hid/paste.go @@ -20,25 +20,25 @@ type PasteReq struct { } func LangueSwitch(base map[rune]Char, lang string) map[rune]Char { - // wenn kein lang angegeben → Standardmap zurück + // if no language is specified → return base map if lang == "" { return base } - // immer Kopie erstellen + // always create a copy of the base map m := copyMap(base) switch lang { case "de": - // Y tauschen + // swap Y m['y'] = Char{0, 29} m['Y'] = Char{2, 29} - // Z tauschen + // swap Z m['z'] = Char{0, 28} m['Z'] = Char{2, 28} - // deutsche Sonderzeichen hinzufügen oder remappen + // add German special characters or remap them m['\u00E4'] = Char{0, 52} // ä m['\u00C4'] = Char{2, 52} // Ä m['\u00F6'] = Char{0, 51} // ö @@ -47,8 +47,8 @@ func LangueSwitch(base map[rune]Char, lang string) map[rune]Char { m['\u00DC'] = Char{2, 47} // Ü m['\u00DF'] = Char{0, 45} // ß - //Tauschen - m['^'] = Char{0, 53} // muss doppelt sein + // swap special characters + m['^'] = Char{0, 53} // must be double m['/'] = Char{2, 36} // Shift + 7 m['('] = Char{2, 37} // Shift + 8 m['&'] = Char{2, 35} // Shift + 6 @@ -75,7 +75,7 @@ func LangueSwitch(base map[rune]Char, lang string) map[rune]Char { m['-'] = Char{0, 56} // Shift + - m['_'] = Char{2, 56} // Shift + - - //neu + // new special characters m['\u00B4'] = Char{0, 46} // ´ m['\u00B0'] = Char{2, 53} // ° m['\u00A7'] = Char{2, 32} // § diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index 5658f42..5afeec6 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -74,7 +74,8 @@ const de = { virtual: 'Tastatur', ctrlaltdel: 'Ctrl+Alt+Del', dropdownEnglish: 'Englisch', - dropdownGerman: 'Deutsch' + dropdownGerman: 'Deutsch', + dropdownRussian: 'Russisch' }, mouse: { title: 'Maus', diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index b1fc6f5..67d5077 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -79,7 +79,8 @@ const en = { 'Clipboard permission denied. Please allow clipboard access in your browser.', clipboardReadError: 'Failed to read clipboard', dropdownEnglish: 'English', - dropdownGerman: 'German' + dropdownGerman: 'German', + dropdownRussian: 'Russian' }, mouse: { title: 'Mouse', diff --git a/web/src/i18n/locales/ru.ts b/web/src/i18n/locales/ru.ts index 9eb3b77..da88138 100644 --- a/web/src/i18n/locales/ru.ts +++ b/web/src/i18n/locales/ru.ts @@ -73,7 +73,10 @@ const ru = { placeholder: 'Текст для ввода', submit: 'Вставить', virtual: 'Клавиатура', - ctrlaltdel: 'Ctrl+Alt+Del' + ctrlaltdel: 'Ctrl+Alt+Del', + dropdownEnglish: 'Английский', + dropdownGerman: 'Немецкий', + dropdownRussian: 'Русский' }, mouse: { title: 'Мышь', diff --git a/web/src/pages/desktop/menu/keyboard/paste.tsx b/web/src/pages/desktop/menu/keyboard/paste.tsx index 354197d..29b17d7 100644 --- a/web/src/pages/desktop/menu/keyboard/paste.tsx +++ b/web/src/pages/desktop/menu/keyboard/paste.tsx @@ -28,7 +28,8 @@ export const Paste = () => { const languages = [ { value: 'en', label: t('keyboard.dropdownEnglish') }, - { value: 'de', label: t('keyboard.dropdownGerman') } + { value: 'de', label: t('keyboard.dropdownGerman') }, + { value: 'ru', label: t('keyboard.dropdownRussian') } ]; useEffect(() => { @@ -37,7 +38,7 @@ export const Paste = () => { function onChange(e: ChangeEvent) { const value = e.target.value; - setStatus(isAsciiOrRU(value) ? '' : 'error'); + setStatus(isValidForLanguage(value, langue) ? '' : 'error'); setInputValue(value); } @@ -62,10 +63,8 @@ export const Paste = () => { } } - // Extended RU → EN translation including punctuation (applies only if Cyrillic is present) + // Extended RU → EN translation including punctuation function translateRuToEnWithPunctuation(value: string): string { - const hasCyrillic = /[А-Яа-яЁё]/.test(value); - const letterMap: Record = { 'ё': '`', 'й': 'q', 'ц': 'w', 'у': 'e', 'к': 'r', 'е': 't', 'н': 'y', 'г': 'u', 'ш': 'i', @@ -93,7 +92,7 @@ export const Paste = () => { const translated = letterMap[lower]; return ch === lower ? translated : translated.toUpperCase(); } - if (hasCyrillic && punctuationMap[ch]) { + if (punctuationMap[ch]) { return punctuationMap[ch]; } return ch; @@ -104,9 +103,9 @@ export const Paste = () => { if (isLoading || !inputValue) return; setIsLoading(true); - const translated = translateRuToEnWithPunctuation(inputValue); + const textToSend = langue === 'ru' ? translateRuToEnWithPunctuation(inputValue) : inputValue; - paste(translated, langue) + paste(textToSend, langue) .then((rsp) => { if (rsp.code !== 0) { setErrMsg(rsp.msg); @@ -131,20 +130,30 @@ export const Paste = () => { setIsKeyboardEnable(!open); } - function isAsciiOrRU(value: string) { - // Allow ASCII and Russian Cyrillic letters + function isValidForLanguage(value: string, selectedLangue: string) { + const isRussian = selectedLangue === 'ru'; + for (const ch of value) { const code = ch.codePointAt(0) ?? 0; - if (code <= 0x7F) continue; // ASCII - if ( - (code >= 0x0410 && code <= 0x042F) || // (А-Я) - (code >= 0x0430 && code <= 0x044F) || // (а-я) - code === 0x0401 || // (Ё) - code === 0x0451 // (ё) - ) { - continue; + + if (isRussian) { + // For Russian language, allow Cyrillic letters and special characters + // that can be typed on Russian keyboard (but not English letters) + if ( + (code >= 0x0410 && code <= 0x042F) || // (А-Я) + (code >= 0x0430 && code <= 0x044F) || // (а-я) + code === 0x0401 || // (Ё) + code === 0x0451 || // (ё) + (code >= 0x20 && code <= 0x7E && !(code >= 0x41 && code <= 0x5A) && !(code >= 0x61 && code <= 0x7A)) // Special chars, digits, space, but not English letters + ) { + continue; + } + return false; + } else { + // For English/German, only allow ASCII + if (code <= 0x7F) continue; + return false; } - return false; } return true; }