diff --git a/build.sh b/build.sh index 9eac6181..b40b47c5 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,13 @@ set -e -project_dir="$(realpath "${BASH_SOURCE%/*}")" +project_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +# Create empty build directory to prevent npm EEXIST error with build.sh +mkdir -p "${project_dir}/build" + +# Use SQLite for tests to avoid MySQL dependency +export DATABASE_CLIENT=sqlite rm -fr "${project_dir}/node_modules" npm --prefix "${project_dir}" install diff --git a/packages/keybr-color/lib/convert-xyz.test.ts b/packages/keybr-color/lib/convert-xyz.test.ts index 893b7dd2..4ec02252 100644 --- a/packages/keybr-color/lib/convert-xyz.test.ts +++ b/packages/keybr-color/lib/convert-xyz.test.ts @@ -102,12 +102,20 @@ test("rgb / oklch", () => { h: 0.08120522299896633, alpha: 0.5, }); - like(oklchToRgb(new OklchColor(0.6279553639214313, 0.25768330380536064, 0.08120522299896633, 0.5)), { - r: 0.9999999999999997, - g: 4.304625232653958e-15, - b: 0, - alpha: 0.5, - }); + const result = oklchToRgb(new OklchColor(0.6279553639214313, 0.25768330380536064, 0.08120522299896633, 0.5)); + // Use approximate comparison for floating-point values + if (Math.abs(result.r - 0.9999999999999997) > 1e-14) { + throw new Error(`r value ${result.r} is not close enough to expected`); + } + if (Math.abs(result.g) > 1e-14) { + throw new Error(`g value ${result.g} is not close enough to zero`); + } + if (Math.abs(result.b) > 1e-14) { + throw new Error(`b value ${result.b} is not close enough to zero`); + } + if (Math.abs(result.alpha - 0.5) > 1e-14) { + throw new Error(`alpha value ${result.alpha} is not close enough to 0.5`); + } like(rgbToOklch(new RgbColor(1, 1, 1, 0.5)), { l: 1, diff --git a/packages/keybr-intl/translations/af.json b/packages/keybr-intl/translations/af.json index a79ebe93..0711d7eb 100644 --- a/packages/keybr-intl/translations/af.json +++ b/packages/keybr-intl/translations/af.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Genereer tiklesse uit die teks van ’n boek. Alle sleutels is by verstek ingesluit. Hierdie modus is vir die voordele.", "lessonType.code.description": "Oefen leestekens wat spesifiek is vir ’n programmeertaalsintaksis.", "lessonType.customText.description": "Genereer tiklesse uit die woorde van jou eie persoonlike teks. Alle sleutels is normaalweg ingesluit. Hierdie modus is vir die kenners.", + "lessonType.customText.fromUrl": "Pasgemaakte teks is vanaf die URL gelaai.", + "lessonType.customText.fromUrl.description": "Hierdie teks sal slegs vir die huidige sessie gebruik word en sal nie in jou instellings gestoor word nie.", "lessonType.guided.description": "Genereer tiklesse met ewekansige woorde deur die fonetiese reëls van jou taal te gebruik. Die sleutelstel word dinamies uitgebrei op grond van jou prestasie. Hierdie modus is vir beginners.", "lessonType.numbers.description": "Oefen slegs getalle.", "lessonType.syntax.description": "Genereer lesse wat ooreenstem met die gespesifiseerde programmeertaalsintaksis.", diff --git a/packages/keybr-intl/translations/ar.json b/packages/keybr-intl/translations/ar.json index f1f48ab1..4463c046 100644 --- a/packages/keybr-intl/translations/ar.json +++ b/packages/keybr-intl/translations/ar.json @@ -66,6 +66,8 @@ "lessonType.books.description": "إنشاء دروس الطباعة من نص كتاب. يتم تضمين جميع المفاتيح بشكل افتراضي. هذا الوضع مخصص للمحترفين.", "lessonType.code.description": "التدرب على رموز علامات الترقيم الخاصة ببناء جمل لغة برمجة.", "lessonType.customText.description": "أنشئ دروسًا في الكتابة من كلمات نصك المخصص. يتم تضمين جميع المفاتيح بشكل افتراضي. هذا الوضع مخصص للمحترفين.", + "lessonType.customText.fromUrl": "تم تحميل نص مخصص من URL.", + "lessonType.customText.fromUrl.description": "سيتم استخدام هذا النص فقط للجلسة الحالية ولن يتم حفظه في إعداداتك.", "lessonType.guided.description": "إنشاء دروس كتابة باستخدام كلمات عشوائية باستخدام قواعد صوتيات لغتك. يتم توسيع مجموعة المفاتيح ديناميكيًا بناءً على أدائك. هذا الوضع للمبتدئين.", "lessonType.numbers.description": "التدرب على الأرقام فقط.", "lessonType.syntax.description": "إنشاء دروس تشبه بناء جمل لغة البرمجة المحددة.", diff --git a/packages/keybr-intl/translations/bg.json b/packages/keybr-intl/translations/bg.json index 9f87168e..2cb1ed4a 100644 --- a/packages/keybr-intl/translations/bg.json +++ b/packages/keybr-intl/translations/bg.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Генерирайте уроци по машинопис от текста на книга. Всички клавиши са включени по подразбиране. Този режим е за професионалисти.", "lessonType.code.description": "Практикувайте пунктуационни знаци, които са специфични за синтаксис на език за програмиране.", "lessonType.customText.description": "Генерирайте уроци по писане от думите на вашия персонализиран текст. Всички клавиши са включени по подразбиране. Този режим е за професионалистите.", + "lessonType.customText.fromUrl": "Персонализираният текст беше зареден от URL.", + "lessonType.customText.fromUrl.description": "Текстът ще бъде използван само за текущата сесия и няма да бъде запазен в настройките ви.", "lessonType.guided.description": "Генерирайте уроци по писане с произволни думи, като използвате фонетичните правила на вашия език. Наборът клавиши се разширява динамично въз основа на вашето представяне. Този режим е за начинаещи.", "lessonType.numbers.description": "Упражнявайте само числа.", "lessonType.syntax.description": "Генерирайте уроци, които наподобяват посочения синтаксис на езика за програмиране.", diff --git a/packages/keybr-intl/translations/bn.json b/packages/keybr-intl/translations/bn.json index d0097c3b..0456246b 100644 --- a/packages/keybr-intl/translations/bn.json +++ b/packages/keybr-intl/translations/bn.json @@ -66,6 +66,8 @@ "lessonType.books.description": "একটি বইয়ের পাঠ্য থেকে টাইপিং পাঠ তৈরি করুন। ডিফল্টরূপে সমস্ত কী অন্তর্ভুক্ত করা হয়। এই মোড পেশাদারদের জন্য।", "lessonType.code.description": "প্রোগ্রামিং ভাষার সিনট্যাক্সের জন্য নির্দিষ্ট বিরামচিহ্ন অক্ষরগুলি অনুশীলন করুন।", "lessonType.customText.description": "আপনার নিজস্ব কাস্টম টেক্সটের শব্দ থেকে টাইপিং পাঠ তৈরি করুন। সমস্ত কী ডিফল্টরূপে অন্তর্ভুক্ত করা হয়। এই মোডটি পেশাদারদের জন্য।", + "lessonType.customText.fromUrl": "কাস্টম টেক্সট URL থেকে লোড করা হয়েছে।", + "lessonType.customText.fromUrl.description": "এই টেক্সটটি শুধুমাত্র বর্তমান সেশনের জন্য ব্যবহৃত হবে এবং আপনার সেটিংসে সংরক্ষিত হবে না।", "lessonType.guided.description": "আপনার ভাষার ধ্বনিতাত্ত্বিক নিয়ম ব্যবহার করে এলোমেলো শব্দ দিয়ে টাইপিং পাঠ তৈরি করুন। আপনার পারফরম্যান্সের উপর ভিত্তি করে কী সেট গতিশীলভাবে প্রসারিত হয়। এই মোডটি নতুনদের জন্য।", "lessonType.numbers.description": "শুধুমাত্র সংখ্যা অনুশীলন করুন।", "lessonType.syntax.description": "নির্দিষ্ট প্রোগ্রামিং ভাষার সিনট্যাক্সের অনুরূপ পাঠ তৈরি করুন।", diff --git a/packages/keybr-intl/translations/ca.json b/packages/keybr-intl/translations/ca.json index dc0a98f9..4a0d55de 100644 --- a/packages/keybr-intl/translations/ca.json +++ b/packages/keybr-intl/translations/ca.json @@ -64,6 +64,8 @@ "lesson.indicator.notCalibrated": "Tecla amb un nivell de confiança desconegut. Encara no l’has premut.", "lesson.indicator.notIncluded": "Una tecla que encara no ha estat inclosa en les lliçons.", "lessonType.customText.description": "Genera lliçons de mecanografia a partir de les paraules del teu text personalitzat. Totes les tecles estan incloses per defecte. Aquest mode és per als professionals.", + "lessonType.customText.fromUrl": "El text personalitzat es va carregar des de la URL.", + "lessonType.customText.fromUrl.description": "Aquest text només s’utilitzarà per a la sessió actual i no es desarà a la vostra configuració.", "lessonType.guided.description": "Genera lliçons de mecanografia amb paraules aleatòries usant les normes fonètiques del teu idioma. El conjunt de tecles s’expandirà dinàmicament basant-se en el teu rendiment. Aquest mode és per als novells.", "lessonType.numbers.description": "Practica només nombres.", "lessonType.wordList.description": "Genera lliçons de mecanografia a partir de la llista de les paraules més comunes del teu idioma. Totes les tecles estan incloses per defecte. Aquest mode és per als professionals.", diff --git a/packages/keybr-intl/translations/cs.json b/packages/keybr-intl/translations/cs.json index cdc57572..ff86d731 100644 --- a/packages/keybr-intl/translations/cs.json +++ b/packages/keybr-intl/translations/cs.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Generuje lekce psaní z textů knížek. V základu jsou zahrnuty všechny znaky. Tento mód je pro profesionály.", "lessonType.code.description": "Procvičovat interpunkci specifickou pro syntax programovacích jazyků.", "lessonType.customText.description": "Lekce psaní na klávesnici se generují ze slov vlastního textu. Ve výchozím nastavení jsou zahrnuty všechny klávesy. Tento režim je určen pro zkušené uživatele.", + "lessonType.customText.fromUrl": "Vlastní text byl načten z URL.", + "lessonType.customText.fromUrl.description": "Tento text bude použit pouze pro aktuální relaci a nebude uložen do vašich nastavení.", "lessonType.guided.description": "Lekce psaní s náhodnými slovy s použitím fonetických pravidel vašeho jazyka. Sada kláves se dynamicky rozšiřuje na základě vašeho pokroku. Tento režim je určen pro začátečníky.", "lessonType.numbers.description": "Trénovat pouze číslice.", "lessonType.syntax.description": "Generovat cvičení připomínající syntax specifikovaného programovacího jazyka.", diff --git a/packages/keybr-intl/translations/da.json b/packages/keybr-intl/translations/da.json index 736ac19d..e3d9f0d9 100644 --- a/packages/keybr-intl/translations/da.json +++ b/packages/keybr-intl/translations/da.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Generer skrive lektioner fra tekster fra bøger. All knapper er inkluderet som standard. Denne tilstand er for de dygtige.", "lessonType.code.description": "Øv tegnsætningstegn, der er specifikke for et programmeringssprogssyntaks.", "lessonType.customText.description": "Generer skrivelektioner ud fra ordene i din egen tilpassede tekst. Alle taster er inkluderet som standard. Denne tilstand er for de professionelle.", + "lessonType.customText.fromUrl": "Brugerdefineret tekst blev indlæst fra URL.", + "lessonType.customText.fromUrl.description": "Denne tekst vil kun blive brugt til den nuværende session og vil ikke blive gemt i dine indstillinger.", "lessonType.guided.description": "Generer skrivelektioner med tilfældige ord ved at bruge de fonetiske regler for dit sprog. tastesættet udvides dynamisk baseret på din præstation. Denne tilstand er for begyndere.", "lessonType.numbers.description": "Øv kun tal.", "lessonType.syntax.description": "Generer lektioner, der genskaber det angivne programmeringssprogssyntaks.", diff --git a/packages/keybr-intl/translations/de.json b/packages/keybr-intl/translations/de.json index a6c7dc0a..1b855511 100644 --- a/packages/keybr-intl/translations/de.json +++ b/packages/keybr-intl/translations/de.json @@ -337,5 +337,7 @@ "t_ws_Bar_whitespace": "Blankes Leerzeichen", "t_ws_Bullet_whitespace": "Aufzählungsleerzeichen", "t_ws_No_whitespace": "Kein Leerzeichen", - "weekDayNames": "Mo|Di|Mi|Do|Fr|Sa|So" + "weekDayNames": "Mo|Di|Mi|Do|Fr|Sa|So", + "lessonType.customText.fromUrl": "Benutzerdefinierter Text wurde aus URL geladen.", + "lessonType.customText.fromUrl.description": "Dieser Text wird nur für die aktuelle Sitzung verwendet und nicht in Ihren Einstellungen gespeichert." } diff --git a/packages/keybr-intl/translations/el.json b/packages/keybr-intl/translations/el.json index 22506a4e..a9217b14 100644 --- a/packages/keybr-intl/translations/el.json +++ b/packages/keybr-intl/translations/el.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Δημιουργήστε μαθήματα πληκτρολόγησης από το κείμενο ενός βιβλίου. Όλα τα κλειδιά περιλαμβάνονται από προεπιλογή. Αυτή η λειτουργία είναι για τους έμπειρους χρήστες.", "lessonType.code.description": "Εξασκηθείτε στους χαρακτήρες στίξης που είναι συγκεκριμένοι για μια γλώσσα προγραμματισμού.", "lessonType.customText.description": "Δημιουργήστε μαθήματα πληκτρολόγησης από τις λέξεις του δικού σας προσαρμοσμένου κειμένου. Όλα τα πλήκτρα περιλαμβάνονται από προεπιλογή. Αυτή η λειτουργία είναι για τους επαγγελματίες.", + "lessonType.customText.fromUrl": "Το προσαρμοσμένο κείμενο φορτώθηκε από το URL.", + "lessonType.customText.fromUrl.description": "Αυτό το κείμενο θα χρησιμοποιηθεί μόνο για την τρέχουσα συνεδρία και δεν θα αποθηκευτεί στις ρυθμίσεις σας.", "lessonType.guided.description": "Δημιουργούνται μαθήματα πληκτρολόγησης με τυχαίες λέξεις χρησιμοποιώντας τους φωνητικούς κανόνες της γλώσσας σας. Το σετ των πλήκτρων επεκτείνεται δυναμικά με βάση την απόδοσή σας. Αυτή η λειτουργία είναι για αρχάριους.", "lessonType.numbers.description": "Μόνο αριθμοί για εξάσκηση.", "lessonType.syntax.description": "Δημιουργήστε μαθήματα που μοιάζουν με τη σύνταξη της καθορισμένης γλώσσας προγραμματισμού.", diff --git a/packages/keybr-intl/translations/en.json b/packages/keybr-intl/translations/en.json index d7811bbb..ff58cd62 100644 --- a/packages/keybr-intl/translations/en.json +++ b/packages/keybr-intl/translations/en.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Generate typing lessons from the text of a book. All keys are included by default. This mode is for the pros.", "lessonType.code.description": "Practice punctuation characters that are specific to a programming language syntax.", "lessonType.customText.description": "Generate typing lessons from the words of your own custom text. All keys are included by default. This mode is for the pros.", + "lessonType.customText.fromUrl": "Custom text was loaded from URL.", + "lessonType.customText.fromUrl.description": "This text will only be used for the current session and will not be saved to your settings.", "lessonType.guided.description": "Generate typing lessons with random words using the phonetic rules of your language. The key set is expanded dynamically based on your performance. This mode is for the beginners.", "lessonType.numbers.description": "Practice numbers only.", "lessonType.syntax.description": "Generate lessons that resemble the specified programming language syntax.", diff --git a/packages/keybr-intl/translations/eo.json b/packages/keybr-intl/translations/eo.json index 69b47346..bad28b13 100644 --- a/packages/keybr-intl/translations/eo.json +++ b/packages/keybr-intl/translations/eo.json @@ -45,6 +45,8 @@ "learningRate.remainingLessons": "Proksimume {remainingLessons} restantaj lesonoj por malŝlosi ĉi tiun literon ({certainty} certeco).", "learningRate.unknown": "Bezonas pli datumaro por komputi la restantaj lesonoj por malŝlosi ĉi tiun literon.", "lesson.indicator.focused": "Klavo kun pliiĝita ofteco. Vi bezonas la plej grandan tempon por trovi ĉi tiun klavon, do la algoritmo inkluzivatigis en ĉiu produktitajn vortojn.", + "lessonType.customText.fromUrl": "Personaligita teksto estis ŝargita de URL.", + "lessonType.customText.fromUrl.description": "Ĉi tiu teksto nur estos uzata por la nuna sesio kaj ne estos konservita en viaj agordoj.", "t_Account": "Konta", "t_Account_details": "Konta Detaloj", "t_Account_name": "Konta | {name}", diff --git a/packages/keybr-intl/translations/es.json b/packages/keybr-intl/translations/es.json index 82e65431..1d45f6c5 100644 --- a/packages/keybr-intl/translations/es.json +++ b/packages/keybr-intl/translations/es.json @@ -339,5 +339,7 @@ "t_ws_Bar_whitespace": "Guión bajo", "t_ws_Bullet_whitespace": "Bullet", "t_ws_No_whitespace": "Sin espacios en blanco", - "weekDayNames": "Lu|Ma|Mi|Ju|Vi|Sa|Do" + "weekDayNames": "Lu|Ma|Mi|Ju|Vi|Sa|Do", + "lessonType.customText.fromUrl": "Texto personalizado cargado desde la URL.", + "lessonType.customText.fromUrl.description": "Este texto solo se utilizará para la sesión actual y no se guardará en su configuración." } diff --git a/packages/keybr-intl/translations/et.json b/packages/keybr-intl/translations/et.json index 5624b8ed..7c27c787 100644 --- a/packages/keybr-intl/translations/et.json +++ b/packages/keybr-intl/translations/et.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Genereeri trükkimisõppetunde raamatu tekstist. Kõik klahvid on vaikimisi sissearvestatud. See režiim on mõeldud professionaalidele.", "lessonType.code.description": "Harjuta kirjavahemärke, mis on programmeerimiskeele süntaksile spetsiifilised", "lessonType.customText.description": "Loo tippimise harjutusi enda kohandatud teksti sõnadest. Kõik klahvid on vaikimisi lisatud. See režiim on mõeldud professionaalidele.", + "lessonType.customText.fromUrl": "Kohandatud tekst laaditi URL-ilt.", + "lessonType.customText.fromUrl.description": "See tekst kasutatakse ainult praeguses seansis ja ei salvestata teie seadistustesse.", "lessonType.guided.description": "Loo juhuslike sõnadega tippimisharjutusi, kasutades oma emakeele foneetilisi reegleid. Klahvikomplekt laieneb dünaamiliselt su soorituse põhjal. See režiim on mõeldud algajatele.", "lessonType.numbers.description": "Ainult harjutuse numbrid.", "lessonType.syntax.description": "Genereeri õppetunde, mis meenutavad märgitud programmeerimiskeele süntaksit.", diff --git a/packages/keybr-intl/translations/fa.json b/packages/keybr-intl/translations/fa.json index 9352b12b..4aa03ef2 100644 --- a/packages/keybr-intl/translations/fa.json +++ b/packages/keybr-intl/translations/fa.json @@ -62,6 +62,8 @@ "lesson.indicator.notCalibrated": "یک کلید تنظیم نشده با سطح اطمینان نامعلوم. شما هنوز این کلید را فشار نداده‌اید.", "lesson.indicator.notIncluded": "یک کلبد که هنور در درس‌هایتان افزوده نشده است.", "lessonType.customText.description": "درس‌های تایپ را از کلمات متن سفارشی خود ایجاد کنید. به طور پیش‌فرض همه کلیدها گنجانده شده‌اند. این حالت برای حرفه‌ای‌ها است.", + "lessonType.customText.fromUrl": "متن سفارشی از URL بارگذاری شد.", + "lessonType.customText.fromUrl.description": "این متن فقط برای جلسه جاری استفاده خواهد شد و در تنظیمات شما ذخیره نخواهد شد.", "lessonType.guided.description": "تولید درس‌های تایپ با کلمات تصادفی با استفاده از قوانین آوایی زبان شما. مجموعه کلید‌ها بر اساس عملکرد شما به شکل پویا گسترش می‌یابد. این حالت برای افراد مبتدی است.", "lessonType.numbers.description": "فقط شماره‌ها را تمرین کنید.", "lessonType.syntax.description": "درس‌هایی تولید کن که نمونه‌ی سینتکس زبان برنامه‌نویسی موردنظر باشند.", diff --git a/packages/keybr-intl/translations/fo.json b/packages/keybr-intl/translations/fo.json index c8f4b076..ca9bc604 100644 --- a/packages/keybr-intl/translations/fo.json +++ b/packages/keybr-intl/translations/fo.json @@ -18,6 +18,8 @@ "help.rule1.title": "Algoritman byrjar við teir byrjanar bókstavirnir", "help.rule2.title": "Tú lærir byrjanar bókstavirnir", "help.rule3.title": "Algoritman leggur fleiri bókstavir afturat", + "lessonType.customText.fromUrl": "Egin tekstur varð lastaður frá URL.", + "lessonType.customText.fromUrl.description": "Hesin teksturin verður bert brúktur til hesa núverandi sessión og verður ikki varðveitt í tínum innstillingum.", "t_Account_details": "Brúkaraupplýsingar", "t_Account_name": "Brúkari {navn}", "t_Anonymize_me": "Ger meg dulnevndan", diff --git a/packages/keybr-intl/translations/fr.json b/packages/keybr-intl/translations/fr.json index 390a8020..16a0e649 100644 --- a/packages/keybr-intl/translations/fr.json +++ b/packages/keybr-intl/translations/fr.json @@ -331,5 +331,7 @@ "t_ws_Bar_whitespace": "Tiret bas", "t_ws_Bullet_whitespace": "Point médian", "t_ws_No_whitespace": "Espace blanc", - "weekDayNames": "Lu|Ma|Me|Je|Ve|Sa|Di" + "weekDayNames": "Lu|Ma|Me|Je|Ve|Sa|Di", + "lessonType.customText.fromUrl": "Texte personnalisé chargé depuis la URL.", + "lessonType.customText.fromUrl.description": "Ce texte ne sera utilisé que pour la session actuelle et ne sera pas enregistré dans vos paramètres." } diff --git a/packages/keybr-intl/translations/ga.json b/packages/keybr-intl/translations/ga.json index 4f3007dd..09ba03c8 100644 --- a/packages/keybr-intl/translations/ga.json +++ b/packages/keybr-intl/translations/ga.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Ceachtanna clóscríobh a chruthú ó théacs leabhair. Tá na heochracha go léir san áireamh de réir réamhshocraithe. Tá an modh seo le haghaidh na buntáistí.", "lessonType.code.description": "Cleachtaigh carachtair poncaíochta a bhaineann go sonrach le comhréir teanga ríomhchlárúcháin.", "lessonType.customText.description": "Gin ceachtanna clóscríofa ó fhocail do théacs saincheaptha féin. Tá na heochracha go léir san áireamh de réir réamhshocraithe. Tá an modh seo le haghaidh na buntáistí.", + "lessonType.customText.fromUrl": "Lódáiltear téacs saincheaptha ó URL.", + "lessonType.customText.fromUrl.description": "Ní bheidh an téacs seo ach á úsáid don seisiún reatha agus ní shábhálfar é i do shocruithe.", "lessonType.guided.description": "Gin ceachtanna clóscríofa le focail randamacha ag úsáid rialacha foghraíochta do theanga. Tá an eochair-thacar leathnaithe go dinimiciúil bunaithe ar do fheidhmíocht. Tá an modh seo do thosaitheoirí.", "lessonType.numbers.description": "Uimhreacha a chleachtadh amháin.", "lessonType.syntax.description": "Gin ceachtanna atá cosúil le comhréir sonraithe na teanga ríomhchlárúcháin.", diff --git a/packages/keybr-intl/translations/he.json b/packages/keybr-intl/translations/he.json index a5ae7980..c2d2c49f 100644 --- a/packages/keybr-intl/translations/he.json +++ b/packages/keybr-intl/translations/he.json @@ -66,6 +66,8 @@ "lessonType.books.description": "יצר שיעור כתיבה מטקסט של ספר. כל המקשים מאופשרים באופן ברירת מחדל. מצב זה למקצוענים.", "lessonType.code.description": "תרגל סימני פיסוק ספציפיים לתחביר של שפת תכנות.", "lessonType.customText.description": "הפק שיעורי הקלדה מהמילים של הטקסט המותאם אישית שלך. כל המפתחות כלולים כברירת מחדל. מצב זה מיועד למקצוענים.", + "lessonType.customText.fromUrl": "טקסט מותאם אישית נטען מ-URL.", + "lessonType.customText.fromUrl.description": "טקסט זה ישמש רק עבור המושב הנוכחי ולא יישמר בהגדרות שלך.", "lessonType.guided.description": "צור שיעורי הקלדה עם מילים אקראיות תוך שימוש בחוקים הפונטיים של השפה שלך. ערכת המפתחות מורחבת באופן דינמי על סמך הביצועים שלך. מצב זה מיועד למתחילים.", "lessonType.numbers.description": "התאמן על מספרים בלבד.", "lessonType.syntax.description": "הפק שיעורים הדומים לתחביר שפת התכנות שצוין.", diff --git a/packages/keybr-intl/translations/hr.json b/packages/keybr-intl/translations/hr.json index c3b736a3..94a1a571 100644 --- a/packages/keybr-intl/translations/hr.json +++ b/packages/keybr-intl/translations/hr.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Generirajte lekcije tipkanja iz teksta knjige. Sve tipke su uključene prema zadanim postavkama. Ovaj način je za profesionalce.", "lessonType.code.description": "Vježbajte interpunkcijske znakove specifične za sintaksu programskog jezika.", "lessonType.customText.description": "Generirajte lekcije tipkanja iz riječi vlastitog prilagođenog teksta. Sve su tipke uključene prema zadanim postavkama. Ovaj način rada je za profesionalce.", + "lessonType.customText.fromUrl": "Prilagođeni tekst je učitan s URL-a.", + "lessonType.customText.fromUrl.description": "Ovaj tekst će se koristiti samo za trenutnu sesiju i neće se spremiti u vaše postavke.", "lessonType.guided.description": "Generirajte lekcije tipkanja slučajnim riječima koristeći fonetska pravila vašeg jezika. Skup ključeva dinamički se proširuje na temelju vaših performansi. Ovaj način rada je za početnike.", "lessonType.numbers.description": "Vježbajte samo brojeve.", "lessonType.syntax.description": "Generirajte lekcije koje obuhvaćaju navedenu sintaksu programskog jezika.", diff --git a/packages/keybr-intl/translations/hu.json b/packages/keybr-intl/translations/hu.json index f5d6f6c5..523aa866 100644 --- a/packages/keybr-intl/translations/hu.json +++ b/packages/keybr-intl/translations/hu.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Generálj gépelési leckét egy könyv szövegéből. Minden billentyűt tartalmaz alapból. Ez a mód a profiknak van.", "lessonType.code.description": "Gyakorolja a programozási nyelv szintaxisára jellemző írásjeleket.", "lessonType.customText.description": "Generáljon gépelési leckéket a saját egyedi szövegéből. Az összes billentyű alapértelmezés szerint be van véve a betűhalmazba. Ez a mód a profiknak szól.", + "lessonType.customText.fromUrl": "Egyedi szöveg lett betöltve az URL-ről.", + "lessonType.customText.fromUrl.description": "Ez a szöveg csak az aktuális munkamenet során lesz használva, és nem lesz elmentve a beállításaidba.", "lessonType.guided.description": "Generáljon gépírási leckéket véletlenszerű szavakkal, a nyelvi hangtörvények alkalmazásával. A billentyűkészlet dinamikusan bővül a teljesítményének megfelelően. Ez a mód a kezdőknek szól.", "lessonType.numbers.description": "Gyakorlojon csak számokat.", "lessonType.syntax.description": "Generáljon leckéket amik hasonlítanak a megjelölt programozási nyelv szintaxisára.", diff --git a/packages/keybr-intl/translations/id.json b/packages/keybr-intl/translations/id.json index aa76a879..9ff1d704 100644 --- a/packages/keybr-intl/translations/id.json +++ b/packages/keybr-intl/translations/id.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Buat pelajaran ngetik dari Text Buku. Semua Tombol akan termasuk secara bawaan. Mode ini hanya untuk Pro.", "lessonType.code.description": "Latih karakter tanda baca yang khusus untuk sintaks bahasa pemrograman.", "lessonType.customText.description": "Hasilkan pelajaran dari kata-kata pada teks anda. Semua tombol termasuk secara default. Mode ini untuk profesional.", + "lessonType.customText.fromUrl": "Teks kustom dimuat dari URL.", + "lessonType.customText.fromUrl.description": "Teks ini hanya akan digunakan untuk sesi saat ini dan tidak akan disimpan dalam pengaturan Anda.", "lessonType.guided.description": "Hasilkan pelajaran dengan kata-kata acak menggunakan aturan fonetik bahasa anda. Variasi tombol diperluas berdasarkan kinerja anda. Mode ini untuk pemula.", "lessonType.numbers.description": "Latihan angka saja.", "lessonType.syntax.description": "Menghasilkan pelajaran yang menyerupai sintaks bahasa pemrograman yang ditentukan.", diff --git a/packages/keybr-intl/translations/is.json b/packages/keybr-intl/translations/is.json index 08c114bb..a4d1769e 100644 --- a/packages/keybr-intl/translations/is.json +++ b/packages/keybr-intl/translations/is.json @@ -57,6 +57,8 @@ "lesson.indicator.focused": "Takki með aukinni tíðni. Það tekur þig mestan tíma að finna þennan takka svo algrímið velur að hafa hann með í hverju mynduðu orði.", "lesson.indicator.forced": "Takki sem var handvirkt bætt við æfingarnar.", "lesson.indicator.notIncluded": "Takki sem er ekki enn með í æfingunum.", + "lessonType.customText.fromUrl": "Sérsniðið texti var hlaðið frá URL.", + "lessonType.customText.fromUrl.description": "Þessi texti verður aðeins notaður fyrir þessa núverandi lotu og verður ekki vistaður í stillingunum þínum.", "t_Account_details": "Aðgangsupplýsingar", "t_Account_name": "Aðgangur | {name}", "t_Anonymize_me": "Nafnleyndu mig", diff --git a/packages/keybr-intl/translations/it.json b/packages/keybr-intl/translations/it.json index e2f314ff..74c68974 100644 --- a/packages/keybr-intl/translations/it.json +++ b/packages/keybr-intl/translations/it.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Genera lezioni di dattilografia dal testo di un libro. Tutti i tasti sono inclusi per impostazione predefinita. Questa modalità è per i professionisti.", "lessonType.code.description": "Esercitarsi con i caratteri di punteggiatura specifici della sintassi di un linguaggio di programmazione.", "lessonType.customText.description": "Genera lezioni di digitazione dalle parole del tuo testo personalizzato. Tutti i tasti sono inclusi in modo predefinito. Questa modalità è per i professionisti.", + "lessonType.customText.fromUrl": "Il testo personalizzato è stato caricato da URL.", + "lessonType.customText.fromUrl.description": "Questo testo sarà utilizzato solo per la sessione corrente e non sarà salvato nelle tue impostazioni.", "lessonType.guided.description": "Genera lezioni di digitazione con parole casuali utilizzando le regole fonetiche della tua lingua. Il set di tasti si espande dinamicamente in base alle tue prestazioni. Questa modalità è per i principianti.", "lessonType.numbers.description": "Solo numeri di pratica.", "lessonType.syntax.description": "Generare lezioni che riconciliano la sintassi del linguaggio di programmazione specificata.", diff --git a/packages/keybr-intl/translations/ja.json b/packages/keybr-intl/translations/ja.json index 561fd13b..fef1c1b1 100644 --- a/packages/keybr-intl/translations/ja.json +++ b/packages/keybr-intl/translations/ja.json @@ -66,6 +66,8 @@ "lessonType.books.description": "書籍のテキストからタイピングレッスンを生成します。デフォルトですべてのキーが含まれています。このモードは熟練者向けです。", "lessonType.code.description": "プログラミング言語の構文に特有の句読点文字を練習します。", "lessonType.customText.description": "独自のテキストからタイピングレッスンを生成します。デフォルトではすべてのキーが含まれています。このモードは熟練者向けです。", + "lessonType.customText.fromUrl": "カスタムテキストがURLから読み込まれました。", + "lessonType.customText.fromUrl.description": "このテキストは現在のセッションでのみ使用され、設定に保存されることはありません。", "lessonType.guided.description": "あなたの母国語の音韻規則に基づいて、ランダムな単語を使ったタイピングレッスンを生成します。キーセットはあなたのパフォーマンスに基づいて動的に拡張されます。このモードは初心者向けです。", "lessonType.numbers.description": "数字のみを練習します。", "lessonType.syntax.description": "指定されたプログラミング言語の構文に似たレッスンを生成します。", diff --git a/packages/keybr-intl/translations/ko.json b/packages/keybr-intl/translations/ko.json index f327e783..3850b7f8 100644 --- a/packages/keybr-intl/translations/ko.json +++ b/packages/keybr-intl/translations/ko.json @@ -38,6 +38,8 @@ "learningRate.unknown": "이 글자를 잠금 해제하기까지 걸리는 시간을 계산하기 위한 데이터가 부족함.", "lessonType.books.description": "책의 텍스트에서 타자 연습 자료를 생성합니다. 모든 글자가 포함되어 있습니다. 전문가를 위한 모드입니다.", "lessonType.customText.description": "직접 타자 연습에 사용할 단어를 추가해 보세요. 기본 설정에 모든 키가 포함되어 있어요. 전문가를 위한 설정이에요.", + "lessonType.customText.fromUrl": "사용자 정의 텍스트가 URL에서 로드되었습니다.", + "lessonType.customText.fromUrl.description": "이 텍스트는 현재 세션에서만 사용되며 귀하의 설정에 저장되지 않습니다.", "lessonType.numbers.description": "숫자만 연습하기.", "metric.score.description": "직전 연습에서의 점수예요. 점수는 빠르고 정확하게 타이핑 할 수록 올라갑니다.", "metric.speed.description": "이전 연습에서 타자 속도", diff --git a/packages/keybr-intl/translations/lt.json b/packages/keybr-intl/translations/lt.json index 8258a834..1589c1ff 100644 --- a/packages/keybr-intl/translations/lt.json +++ b/packages/keybr-intl/translations/lt.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Sukurkite spausdinimo pamokas iš knygos teksto. Pagal numatytuosius nustatymus įtraukti visi klavišai. Šis režimas skirtas profesionalams.", "lessonType.code.description": "Praktikuoti skyrybos ženklus kurie yra specifiški programavimo kalbos sintaksei.", "lessonType.customText.description": "Sugeneruokite spausdinimo pamokas iš savo pasirinktinio teksto žodžių. Visi klavišai įtraukti pagal numatytuosius nustatymus. Šis režimas skirtas profesionalams.", + "lessonType.customText.fromUrl": "Pasirinktinis tekstas buvo įkeltas iš URL.", + "lessonType.customText.fromUrl.description": "Šis tekstas bus naudojamas tik šiai sesijai ir nebus išsaugotas jūsų nustatymuose.", "lessonType.guided.description": "Sugeneruokite spausdinimo pamokas su atsitiktiniais žodžiais, naudojant jūsų kalbos fonetikos taisykles. Klavišų rinkinys plečiamas dinamiškai pagal jūsų našumą. Šis režimas skirtas pradedantiesiems.", "lessonType.syntax.description": "Generuoti pamokas kurios atspindi nurodytos programavimo kalbos sintaksę.", "lessonType.wordList.description": "Generuokite spausdinimo pamokas iš jūsų kalbos dažniausių žodžių sąrašo. Visi klavišai įtraukti pagal numatytuosius nustatymus. Šis režimas skirtas profesionalams.", diff --git a/packages/keybr-intl/translations/mn.json b/packages/keybr-intl/translations/mn.json index b22ebb73..03cddedc 100644 --- a/packages/keybr-intl/translations/mn.json +++ b/packages/keybr-intl/translations/mn.json @@ -9,6 +9,8 @@ "account.emailState.sendingText": "Нэвтрэх линкийг дараах мэйл руу явуулж байна {email}... Түр хүлээнэ үү.", "account.freeAccount.description": "

МанайПремиум аккаунт худалдаж авснаар нэмэлт функцүүдыг нээж сурталчилгаагүй шивэх боломжыг нээгээрэй. Премиум аккаунтын давуу талууд:

Нэг л удаа худалдан авснаар бүх насаараа ашиглах боломжтой. Олон удаа төлбөр авахгүй.

", "account.premiumAccount.description": "

Премиум аккаунт худалдаж авсанд баярлалаа! Та нэмэлт функцүүдээ ашиглан сурталчилгаагүй бичилт хийж таалан соёрхоно уу.

", + "lessonType.customText.fromUrl": "URL-аас захиалгат текст ачааллагдсан.", + "lessonType.customText.fromUrl.description": "Энэ текст нь зөвхөн одоогийн сессийн үед ашиглагдах бөгөөд таны тохиргоонд хадгалагдахгүй.", "t_Account_details": "Аккаунтын дэлгэрэнгүй", "t_Account_name": "Аккаунт | {name}", "t_Anonymous_User": "Нэргүй хэрэглэгч", diff --git a/packages/keybr-intl/translations/nb.json b/packages/keybr-intl/translations/nb.json index 3c09b19a..b7c870a1 100644 --- a/packages/keybr-intl/translations/nb.json +++ b/packages/keybr-intl/translations/nb.json @@ -60,6 +60,8 @@ "lesson.indicator.focused": "En tast med økt frekvens. Det tar deg lengst tid å finne denne tasten, så algoritmen valgte å inkludere den i hvert eneste genererte ord.", "lesson.indicator.forced": "En tast som ble manuelt inkludert i leksjonene.", "lesson.indicator.notIncluded": "En tast som hittil ikke var inkludert i leksjonene.", + "lessonType.customText.fromUrl": "Egendefinert tekst ble lastet fra URL.", + "lessonType.customText.fromUrl.description": "Denne teksten vil kun bli brukt for den nåværende sesjonen og vil ikke bli lagret i innstillingene dine.", "m_tour01": "

Lær å skriv raskere

Denne web-applikasjonen hjelper deg med å lære touch-metoden som betyr å skrive med muskelminnet uten å se på tastaturet for å finne tastene. Det kan forbedre skrivehastigheten og nøyaktigheten din betraktelig . Det motsatte er let-og-trykk, en metode hvor du ser på tastaturet istedenfor skjermen, og kun bruker pekefingerne.

Dette er en kort veiledning som forklarer hvordan denne applikasjonen virker.

Du kan bruke venstre og høyre piltast for å navigere gjennom veiledningen.

", "m_tour09": "

Dette er poengsummen i abstrakte poeng og differansen fra gjennomsnittet.

Poengsummen er beregnet ut ifra din skrivehastighet, antall feil og nåværende antall bokstaver i bruk. Formelen er utviklet på en slik måte at den belønner skrivehastighet og straffer for antall feil. Man kan ikke oppnå en høy poengsum med å skrive raskt samtidig som man gjør mange feil.

Brukerne med høyest poengsum er ført opp på poengtavlen.

", "metric.accuracy.description": "Prosentandelen av tegn skrevet uten feil i forrige leksjon.", diff --git a/packages/keybr-intl/translations/ne.json b/packages/keybr-intl/translations/ne.json index 04635c67..8f5e0f2a 100644 --- a/packages/keybr-intl/translations/ne.json +++ b/packages/keybr-intl/translations/ne.json @@ -66,6 +66,8 @@ "lessonType.books.description": "पुस्तकको पाठबाट टाइपिङ पाठहरू उत्पन्न गर्नुहोस्। सबै कुञ्जीहरू पूर्वनिर्धारित रूपमा समावेश छन्। यो मोड पेशेवरहरूको लागि हो।", "lessonType.code.description": "प्रोग्रामिङ भाषा सिन्ट्याक्सको लागि विशिष्ट विराम चिह्न वर्णहरू अभ्यास गर्नुहोस्।", "lessonType.customText.description": "तपाईंको आफ्नै अनुकूल पाठका शब्दहरूबाट टाइपिङ पाठहरू उत्पन्न गर्नुहोस्। सबै कुञ्जीहरू पूर्वनिर्धारित रूपमा समावेश छन्। यो मोड पेशेवरहरूको लागि हो।", + "lessonType.customText.fromUrl": "कस्टम पाठ URL बाट लोड गरिएको थियो।", + "lessonType.customText.fromUrl.description": "यो पाठ केवल वर्तमान सत्रको लागि प्रयोग गरिनेछ र तपाईंको सेटिङमा सुरक्षित गरिने छैन।", "lessonType.guided.description": "तपाईंको भाषाको फोनेटिक नियमहरू प्रयोग गरेर अनियमित शब्दहरूसँग टाइपिङ पाठहरू उत्पन्न गर्नुहोस्। तपाईँको कार्यसम्पादनको आधारमा कुञ्जी सेटलाई गतिशील रूपमा विस्तार गरिएको छ। यो मोड शुरुआतीहरूको लागि हो।", "lessonType.numbers.description": "अभ्यास संख्या मात्र।", "lessonType.syntax.description": "निर्दिष्ट प्रोग्रामिङ भाषा सिन्ट्याक्ससँग मिल्ने पाठहरू उत्पन्न गर्नुहोस्।", diff --git a/packages/keybr-intl/translations/nl.json b/packages/keybr-intl/translations/nl.json index 7ec04d12..e6f37503 100644 --- a/packages/keybr-intl/translations/nl.json +++ b/packages/keybr-intl/translations/nl.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Genereer typelessen van de tekst van een boek. Alle toetsen zijn standaard inbegrepen. Deze modus is voor gevorderden.", "lessonType.code.description": "Oefen leestekens die specifiek zijn voor de syntaxis van een programmeertaal.", "lessonType.customText.description": "Genereer typelessen uit de woorden van je eigen gegeven tekst. Alle toetsen zijn standaard inbegrepen. Deze modus is voor de professionals.", + "lessonType.customText.fromUrl": "Aangepaste tekst is geladen van URL.", + "lessonType.customText.fromUrl.description": "Deze tekst zal alleen voor de huidige sessie worden gebruikt en zal niet worden opgeslagen in uw instellingen.", "lessonType.guided.description": "Genereer typelessen met willekeurige woorden volgens de fonetische regels van je taal. De toetsen worden dynamisch uitgebreid op basis van je prestaties. Deze modus is voor beginners.", "lessonType.numbers.description": "Oefen alleen met cijfers.", "lessonType.syntax.description": "Genereer lessen die lijken op de opgegeven syntaxis van de programmeertaal.", diff --git a/packages/keybr-intl/translations/pl.json b/packages/keybr-intl/translations/pl.json index 7f709875..0a99c93f 100644 --- a/packages/keybr-intl/translations/pl.json +++ b/packages/keybr-intl/translations/pl.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Generuj lekcje pisania na podstawie tekstu książki. Wszystkie klawisze są domyślnie uwzględnione. Ten tryb jest dla profesjonalistów.", "lessonType.code.description": "Ćwicz znaki interpunkcyjne, które są specyficzne dla składni języka programowania.", "lessonType.customText.description": "Wygeneruj lekcje pisania na podstawie słów własnego, niestandardowego tekstu podanego tutaj. Wszystkie klawisze są domyślnie włączone. Ten tryb jest dla profesjonalistów.", + "lessonType.customText.fromUrl": "Niestandardowy tekst został załadowany z URL.", + "lessonType.customText.fromUrl.description": "Ten tekst będzie używany tylko w bieżącej sesji i nie zostanie zapisany w Twoich ustawieniach.", "lessonType.guided.description": "Ten tryb będzie automatycznie generował lekcje pisania na klawiaturze z wykorzystaniem losowych słów oraz reguł fonetycznych twojego języka. Podstawowy zestaw jest powiększany dynamicznie w oparciu o twoje wyniki. Ten tryb jest przeznaczony dla początkujących.", "lessonType.numbers.description": "Ćwicz tylko liczby.", "lessonType.syntax.description": "Generuje lekcje, które przypominają określoną składnię języka programowania.", diff --git a/packages/keybr-intl/translations/pt-br.json b/packages/keybr-intl/translations/pt-br.json index 5da6c2c1..96daf8d3 100644 --- a/packages/keybr-intl/translations/pt-br.json +++ b/packages/keybr-intl/translations/pt-br.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Gere lições de digitação partindo do texto de um livro. Todas as teclas estão incluídas por default. Este modo é para os pros.", "lessonType.code.description": "Pratique caracteres de pontuação específicos da sintaxe de uma linguagem de programação.", "lessonType.customText.description": "Gere lições de digitação a partir das palavras do seu próprio texto personalizado. Todas as teclas estão incluídas por padrão. Este modo é para os profissionais.", + "lessonType.customText.fromUrl": "Texto personalizado foi carregado da URL.", + "lessonType.customText.fromUrl.description": "Este texto será usado apenas para a sessão atual e não será salvo nas suas configurações.", "lessonType.guided.description": "Gere aulas de digitação com palavras aleatórias usando as regras fonéticas do seu idioma. O conjunto de teclas é expandido dinamicamente com base no seu desempenho. Este modo é para iniciantes.", "lessonType.numbers.description": "Praticar somente numeros.", "lessonType.syntax.description": "Gere lições que se assemelham à sintaxe da linguagem de programação especificada.", diff --git a/packages/keybr-intl/translations/pt-pt.json b/packages/keybr-intl/translations/pt-pt.json index 2bf60159..e9e2bab4 100644 --- a/packages/keybr-intl/translations/pt-pt.json +++ b/packages/keybr-intl/translations/pt-pt.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Gera lições de dactilografia a partir do texto de um livro. Todas as teclas estão incluídas por predefinição. Este modo é para profissionais.", "lessonType.code.description": "Practicar Caracteres de Pontuação que são específicos à sintaxe de uma linguagem de programação.", "lessonType.customText.description": "Gerar lições de escrita das palavras do seu texto customizado. Todas as teclas incluídas por defeito. Este modo é para pros.", + "lessonType.customText.fromUrl": "Texto personalizado foi carregado a partir da URL.", + "lessonType.customText.fromUrl.description": "Este texto será utilizado apenas para a sessão atual e não será guardado nas suas definições.", "lessonType.guided.description": "Gerar lições de escrita com palavras aleatórias utilizando as regras fonéticas da sua lingua. O conjunto de teclas é expandido dinamicamente baseado na sua performance. Este modo é para iniciantes.", "lessonType.numbers.description": "Praticar números apenas.", "lessonType.syntax.description": "Gerar lições que aparentem a sintaxe de uma linguagem de programação específica.", diff --git a/packages/keybr-intl/translations/ro.json b/packages/keybr-intl/translations/ro.json index f673b3b7..fa7b1525 100644 --- a/packages/keybr-intl/translations/ro.json +++ b/packages/keybr-intl/translations/ro.json @@ -51,6 +51,8 @@ "lessonType.books.description": "Generează lecții de tastare din textul unei cărți. Toate tastele sunt incluse implicit. Acest mod este pentru profesioniști.", "lessonType.code.description": "Exersați caracterele de punctuație care sunt specifice sintaxei unui limbaj de programare.", "lessonType.customText.description": "Generează lecții de tastare din cuvintele propriului tău text personalizat. Toate tastele sunt incluse în mod implicit. Acest mod este pentru profesioniști.", + "lessonType.customText.fromUrl": "Textul personalizat a fost încărcat de la URL.", + "lessonType.customText.fromUrl.description": "Acest text va fi folosit doar pentru sesiunea curentă și nu va fi salvat în setările tale.", "lessonType.guided.description": "Generează lecții de tastare cu cuvinte aleatorii folosind regulile fonetice ale limbii tale. Setul de taste este extins dinamic pe baza performanței tale. Acest mod este pentru începători.", "lessonType.numbers.description": "Exersați doar numere.", "lessonType.syntax.description": "Generează lecții care seamănă cu sintaxa limbajului de programare specificat.", diff --git a/packages/keybr-intl/translations/ru.json b/packages/keybr-intl/translations/ru.json index 692b6a88..e94110b8 100644 --- a/packages/keybr-intl/translations/ru.json +++ b/packages/keybr-intl/translations/ru.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Создайте уроки набора текста из текста книги. Все клавиши включены по умолчанию. Этот режим предназначен для профессионалов.", "lessonType.code.description": "Практиковать знаки пунктуации, специфичные для выбранного языка кода.", "lessonType.customText.description": "Генерировать уроки используя слова, взятые из вашего собственного текста. Все буквы включены по умолчанию. Этот режим для профессионалов.", + "lessonType.customText.fromUrl": "Пользовательский текст был загружен с URL.", + "lessonType.customText.fromUrl.description": "Этот текст будет использоваться только в текущей сессии и не будет сохранен в ваших настройках.", "lessonType.guided.description": "Генерировать уроки со случайными (несуществующими) словами используя фонетические правила вашего языка. Набор букв, из которого генерируются слова, изменяется динамически в зависимости от ваших успехов. Этот режим для новичков.", "lessonType.numbers.description": "Практика одних лишь чисел.", "lessonType.syntax.description": "Генерировать уроки, соответствующие синтаксису выбранного языка кода.", diff --git a/packages/keybr-intl/translations/sk.json b/packages/keybr-intl/translations/sk.json index 565bd955..199fae9c 100644 --- a/packages/keybr-intl/translations/sk.json +++ b/packages/keybr-intl/translations/sk.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Vytvoriť lekcie z textu knihy. Východzie nastavenie zahŕňa všetky klávesy. Tento režim je pre profíkov.", "lessonType.code.description": "Cvičte interpunkčné znaky, ktoré sú špecifické pre syntax programovacieho jazyka.", "lessonType.customText.description": "Generovať lekcie písania zo slov z vášho vlastného textu. Všetky klávesy sú automaticky zaradené. Tento režim je pre profíkov.", + "lessonType.customText.fromUrl": "Vlastný text bol načítaný z URL.", + "lessonType.customText.fromUrl.description": "Tento text bude použitý iba pre aktuálnu reláciu a nebude uložený do vašich nastavení.", "lessonType.guided.description": "Generovať lekcie písania zo slov, používajúc fonetické pravidlá vášho jazyka. Sada kláves je rozšírená v závislosti na vašom výkone. Tento režim je pre začiatočníkov.", "lessonType.numbers.description": "Cvičiť iba čísla.", "lessonType.syntax.description": "Vytvoriť lekcie, ktoré pripomínajú syntax programovacieho jazyka.", diff --git a/packages/keybr-intl/translations/sl.json b/packages/keybr-intl/translations/sl.json index be32e38f..90322816 100644 --- a/packages/keybr-intl/translations/sl.json +++ b/packages/keybr-intl/translations/sl.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Generiraj vaje za tipkanje iz besedila knjige. Kot privzeto so vključene vse tipke. Ta način je za proje.", "lessonType.code.description": "Vadite znake za ločila, ki so specifične za skladnjo nekega programskega jezika.", "lessonType.customText.description": "Ustvarite vaje za tipkanje iz besed lastnega besedila. Kot privzeto so vključene vse tipke. Ta način je za proje.", + "lessonType.customText.fromUrl": "Po meri besedilo je bilo naloženo iz URL.", + "lessonType.customText.fromUrl.description": "Ta besedilo bo uporabljeno samo za trenutno sejo in ne bo shranjeno v vaše nastavitve.", "lessonType.guided.description": "Ustvarite vaje za tipkanje z naključnimi besedami glede na fonetična pravila vašega jezika. Seznam tipk se razširi avtomatsko glede na vašo zmogljivost. Ta način je za začetnike.", "lessonType.numbers.description": "Vadite le števila.", "lessonType.syntax.description": "Generiraj vaje, ki posnemajo skladnjo izbranega programskega jezika.", diff --git a/packages/keybr-intl/translations/sq.json b/packages/keybr-intl/translations/sq.json index 1b626fe7..29d76e61 100644 --- a/packages/keybr-intl/translations/sq.json +++ b/packages/keybr-intl/translations/sq.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Gjeneroni mësime shtypjeje nga teksti i një libri. Të gjitha tastet përfshihen si parazgjedhje. Ky modalitet është për profesionistët.", "lessonType.code.description": "Praktikoni karaktere pikësimi që janë specifike për sintaksën e një gjuhe programimi.", "lessonType.customText.description": "Gjeneroni mësime shtypjeje nga fjalët e tekstit tuaj të personalizuar. Të gjithë tastet përfshihen si parazgjedhje. Kjo mënyrë është për profesionistët.", + "lessonType.customText.fromUrl": "Teksti i personalizuar u ngarkua nga URL.", + "lessonType.customText.fromUrl.description": "Ky tekst do të përdoret vetëm për sesionin aktual dhe nuk do të ruhet në cilësimet tuaja.", "lessonType.guided.description": "Gjeneroni mësime shtypjeje me fjalë të rastësishme duke përdorur rregullat fonetike të gjuhës suaj. Grupi i tasteve zgjerohet në mënyrë dinamike bazuar në performancën tuaj. Kjo mënyrë është për fillestarët.", "lessonType.numbers.description": "Praktikoni vetëm numrat.", "lessonType.syntax.description": "Gjeneroni mësime që i ngjajnë sintaksës së specifikuar të gjuhës së programimit.", diff --git a/packages/keybr-intl/translations/sv.json b/packages/keybr-intl/translations/sv.json index 7462f719..4e24959b 100644 --- a/packages/keybr-intl/translations/sv.json +++ b/packages/keybr-intl/translations/sv.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Skapa skrivlektioner från texten i en bok. Alla nycklar ingår som standard. Detta läge är för proffsen.", "lessonType.code.description": "Träna på skiljetecken som är specifika för ett programmeringsspråks syntax.", "lessonType.customText.description": "Skapa lektioner med ord från din egna valda text. Alla tangenter inkluderas som standard. Detta läge är mest givande för proffs.", + "lessonType.customText.fromUrl": "Anpassad text laddades från URL.", + "lessonType.customText.fromUrl.description": "Denna text kommer endast att användas för den aktuella sessionen och kommer inte att sparas i dina inställningar.", "lessonType.guided.description": "Skapa lektioner med slumpade ord med hjälp av fonetiska regler från ditt språk. Gruppen tangenter ökar dynamiskt efter prestationer. Detta läge är mest givande för nybörjare.", "lessonType.numbers.description": "Träna endast nummer.", "lessonType.syntax.description": "Generera lektioner som liknar det angivna programmeringsspråkets syntax.", diff --git a/packages/keybr-intl/translations/th.json b/packages/keybr-intl/translations/th.json index 178ee791..c5777d4b 100644 --- a/packages/keybr-intl/translations/th.json +++ b/packages/keybr-intl/translations/th.json @@ -66,6 +66,8 @@ "lessonType.books.description": "สร้างแบบฝึกพิมพ์จากข้อความของหนังสือ โดยค่าเริ่มต้นจะรวมทุกปุ่มไว้ โหมดนี้เหมาะสำหรับผู้เชี่ยวชาญ", "lessonType.code.description": "ฝึกฝนการพิมพ์อักขระวรรคตอนที่เป็นเฉพาะตามไวยากรณ์ของภาษาการเขียนโปรแกรม", "lessonType.customText.description": "สร้างแบบฝึกพิมพ์จากคำในข้อความที่คุณกำหนดเอง ทุกปุ่มจะถูกรวมโดยอัตโนมัติ โหมดนี้เหมาะสำหรับผู้เชี่ยวชาญ", + "lessonType.customText.fromUrl": "ข้อความที่กำหนดเองถูกโหลดจาก URL.", + "lessonType.customText.fromUrl.description": "ข้อความนี้จะถูกใช้เฉพาะในเซสชันปัจจุบันและจะไม่ถูกบันทึกในการตั้งค่าของคุณ", "lessonType.guided.description": "สร้างแบบฝึกพิมพ์ด้วยคำสุ่มที่ใช้กฎการออกเสียงของภาษาของคุณ เซตปุ่มจะขยายออกอย่างต่อเนื่องตามผลการปฏิบัติของคุณ โหมดนี้เหมาะสำหรับผู้เริ่มต้น", "lessonType.numbers.description": "ฝึกพิมพ์ตัวเลขเท่านั้น", "lessonType.syntax.description": "สร้างบทเรียนที่มีลักษณะคล้ายกับไวยากรณ์ของภาษาการเขียนโปรแกรมที่ระบุ", diff --git a/packages/keybr-intl/translations/tr.json b/packages/keybr-intl/translations/tr.json index 1cddc7e7..0c3b8998 100644 --- a/packages/keybr-intl/translations/tr.json +++ b/packages/keybr-intl/translations/tr.json @@ -45,6 +45,8 @@ "learningRate.alreadyUnlocked": "Bu harf zaten aktif.", "lesson.indicator.forced": "Manuel olarak derslere eklenilmiş tuş.", "lesson.indicator.notIncluded": "Derslere daha eklenmemiş tuş.", + "lessonType.customText.fromUrl": "Özel metin URL’den yüklendi.", + "lessonType.customText.fromUrl.description": "Bu metin yalnızca mevcut oturum için kullanılacak ve ayarlarınıza kaydedilmeyecektir.", "lessonType.guided.description": "Dilinizin fonetik kurallarını kullanarak rastgele kelimelerle yazma dersleri oluşturun. Aktif tuşlar performansınıza göre dinamik olarak genişletilir. Bu mod yeni başlayanlar içindir.", "metric.accuracy.description": "Son derste hatasız yazılan karakterlerin yüzdesi.", "metric.difference.description": "Ortalama değerden farkı.", diff --git a/packages/keybr-intl/translations/uk.json b/packages/keybr-intl/translations/uk.json index c0024375..c67e1c8f 100644 --- a/packages/keybr-intl/translations/uk.json +++ b/packages/keybr-intl/translations/uk.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Створювати уроки друку з тексту книги. Всі клавіші включено за замовчуванням. Цей режим для профі.", "lessonType.code.description": "Практикуйте розділові знаки, специфічні для синтаксису мов програмування.", "lessonType.customText.description": "Генеруйте уроки, що будуть містити тільки слова з вашого тексту. Усі літери доступні. Цей режим для профі.", + "lessonType.customText.fromUrl": "Користувацький текст був завантажений з URL.", + "lessonType.customText.fromUrl.description": "Цей текст буде використано лише для поточної сесії і не буде збережено у ваших налаштуваннях.", "lessonType.guided.description": "Генерація випадкових слів, що слідують фонетичним правилам обраної мови. Набір літер, з яких генеруються слова, змінюється динамічно, залежно від ваших успіхів. Цей режим для початківців.", "lessonType.numbers.description": "Тренувати тільки числа.", "lessonType.syntax.description": "Створювати уроки, які будуть нагадувати синтаксис вказаної мови програмування.", diff --git a/packages/keybr-intl/translations/vi.json b/packages/keybr-intl/translations/vi.json index ebc27774..ee1ee80d 100644 --- a/packages/keybr-intl/translations/vi.json +++ b/packages/keybr-intl/translations/vi.json @@ -66,6 +66,8 @@ "lessonType.books.description": "Tạo các bài học gõ phím từ văn bản của một cuốn sách. Tất cả các phím đều được bao gồm theo mặc định. Chế độ này dành cho người chuyên nghiệp.", "lessonType.code.description": "Luyện tập các ký tự dấu câu đặc trưng cho cú pháp của một ngôn ngữ lập trình.", "lessonType.customText.description": "Tạo các bài học gõ từ các từ của văn bản tùy chỉnh của riêng bạn. Tất cả các khóa được bao gồm theo mặc định. Chế độ này là dành cho các chuyên gia.", + "lessonType.customText.fromUrl": "Văn bản tùy chỉnh đã được tải từ URL.", + "lessonType.customText.fromUrl.description": "Văn bản này chỉ được sử dụng cho phiên hiện tại và sẽ không được lưu vào cài đặt của bạn.", "lessonType.guided.description": "Tạo các bài học gõ bằng các từ ngẫu nhiên bằng cách sử dụng các quy tắc ngữ âm của ngôn ngữ của bạn. Bộ khóa được mở rộng động dựa trên hiệu suất của bạn. Chế độ này dành cho người mới bắt đầu.", "lessonType.numbers.description": "Chỉ thực hành số.", "lessonType.syntax.description": "Tạo các bài học gõ phím giống với cú pháp của ngôn ngữ lập trình được chỉ định.", diff --git a/packages/keybr-intl/translations/zh-hans.json b/packages/keybr-intl/translations/zh-hans.json index 3d6681b8..7dcc2511 100644 --- a/packages/keybr-intl/translations/zh-hans.json +++ b/packages/keybr-intl/translations/zh-hans.json @@ -66,6 +66,8 @@ "lessonType.books.description": "从一本书的文本中生成打字课程。默认情况下包含所有按键。这种模式是为专业人士准备的。", "lessonType.code.description": "针对指定编程语言语法中的标点符号进行练习。", "lessonType.customText.description": "通过自定义文本中的单词生成打字课程。默认包含所有按键。此模式适合熟练的人。", + "lessonType.customText.fromUrl": "自定义文本已从 URL 加载。", + "lessonType.customText.fromUrl.description": "此文本仅将在当前会话中使用,不会保存到您的设置中。", "lessonType.guided.description": "利用语言的语音规则生成随机单词的打字课程。按键集会根据你的表现动态扩展。该模式适合初学者。", "lessonType.numbers.description": "只可练习数字。", "lessonType.syntax.description": "生成与指定编程语言的语法类似的课程。", diff --git a/packages/keybr-intl/translations/zh-hant.json b/packages/keybr-intl/translations/zh-hant.json index 4f765c15..0df3c54f 100644 --- a/packages/keybr-intl/translations/zh-hant.json +++ b/packages/keybr-intl/translations/zh-hant.json @@ -66,6 +66,8 @@ "lessonType.books.description": "從書本文字生成打字課程。預設包含所有鍵。此模式適合專業人士。", "lessonType.code.description": "練習特定程式語言文法的標點符號。", "lessonType.customText.description": "透過自訂文字中的單字產生打字課程。預設包含所有按鍵。此模式適合熟練的人。", + "lessonType.customText.fromUrl": "自定義文本已從 URL 加載。", + "lessonType.customText.fromUrl.description": "此文本僅用於當前會話,並不會保存到您的設置中。", "lessonType.guided.description": "利用語言的語音規則產生隨機單字的打字課程。按鍵集會根據你的表現動態擴展。此模式適合初學者。", "lessonType.numbers.description": "只可練習數字。", "lessonType.syntax.description": "生成與指定程式語言文法相似的課程。", diff --git a/packages/keybr-intl/translations/zh-tw.json b/packages/keybr-intl/translations/zh-tw.json index b2578e1f..2380b8f8 100644 --- a/packages/keybr-intl/translations/zh-tw.json +++ b/packages/keybr-intl/translations/zh-tw.json @@ -66,6 +66,8 @@ "lessonType.books.description": "從書籍的文本中生成打字課程。預設包含所有按鍵。此模式適合進階使用者。", "lessonType.code.description": "練習程式語言語法中特有的標點符號。", "lessonType.customText.description": "透過自訂文字中的單字產生打字課程。預設包含所有按鍵。此模式適合熟練的人。", + "lessonType.customText.fromUrl": "自訂文本已從 URL 加載。", + "lessonType.customText.fromUrl.description": "此文本僅會用於當前會話,並不會儲存到您的設定中。", "lessonType.guided.description": "利用語言的語音規則產生隨機單字的打字課程。按鍵集會根據你的表現動態擴展。此模式適合初學者。", "lessonType.numbers.description": "只練習數字。", "lessonType.syntax.description": "生成與指定程式語言語法相似的課程。", diff --git a/packages/keybr-lesson-loader/lib/LessonLoader.tsx b/packages/keybr-lesson-loader/lib/LessonLoader.tsx index 9d4c123d..e4ea74a8 100644 --- a/packages/keybr-lesson-loader/lib/LessonLoader.tsx +++ b/packages/keybr-lesson-loader/lib/LessonLoader.tsx @@ -13,7 +13,7 @@ import { NumbersLesson, WordListLesson, } from "@keybr/lesson"; -import { LoadingProgress } from "@keybr/pages-shared"; +import { LoadingProgress, useUrlText } from "@keybr/pages-shared"; import { type PhoneticModel } from "@keybr/phonetic-model"; import { PhoneticModelLoader } from "@keybr/phonetic-model-loader"; import { useSettings } from "@keybr/settings"; @@ -60,6 +60,7 @@ function Loader({ function useLoader(model: PhoneticModel): Lesson | null { const { settings } = useSettings(); const keyboard = useKeyboard(); + const urlText = useUrlText(); // Get URL text from context const [result, setResult] = useState(null); useEffect(() => { @@ -95,7 +96,14 @@ function useLoader(model: PhoneticModel): Lesson | null { } case LessonType.CUSTOM: { if (!didCancel) { - setResult(new CustomTextLesson(settings, keyboard, model)); + setResult( + new CustomTextLesson( + settings, + keyboard, + model, + urlText ?? undefined, + ), + ); } break; } @@ -121,7 +129,7 @@ function useLoader(model: PhoneticModel): Lesson | null { return () => { didCancel = true; }; - }, [settings, keyboard, model]); + }, [settings, keyboard, model, urlText]); return result; } diff --git a/packages/keybr-lesson/lib/customtext.ts b/packages/keybr-lesson/lib/customtext.ts index 9b933b16..bc21e1b0 100644 --- a/packages/keybr-lesson/lib/customtext.ts +++ b/packages/keybr-lesson/lib/customtext.ts @@ -14,9 +14,17 @@ export class CustomTextLesson extends Lesson { readonly wordList: readonly string[]; wordIndex = 0; - constructor(settings: Settings, keyboard: Keyboard, model: PhoneticModel) { + constructor( + settings: Settings, + keyboard: Keyboard, + model: PhoneticModel, + initialText?: string, + ) { super(settings, keyboard, model); - this.wordList = this.#getWordList(); + // Use initialText if provided, otherwise get from settings + this.wordList = initialText + ? this.#processText(initialText) + : this.#getWordList(); } override get letters() { @@ -42,6 +50,10 @@ export class CustomTextLesson extends Lesson { #getWordList() { const content = this.settings.get(lessonProps.customText.content); + return this.#processText(content); + } + + #processText(content: string): readonly string[] { const lettersOnly = this.settings.get(lessonProps.customText.lettersOnly); const lowercase = this.settings.get(lessonProps.customText.lowercase); const codePoints = new Set(this.codePoints); diff --git a/packages/keybr-pages-browser/lib/NavMenu.test.tsx b/packages/keybr-pages-browser/lib/NavMenu.test.tsx index 013888e9..6924e124 100644 --- a/packages/keybr-pages-browser/lib/NavMenu.test.tsx +++ b/packages/keybr-pages-browser/lib/NavMenu.test.tsx @@ -20,6 +20,7 @@ test("render", () => { premium: false, }, settings: null, + customText: null, }} > diff --git a/packages/keybr-pages-browser/lib/SubMenu.test.tsx b/packages/keybr-pages-browser/lib/SubMenu.test.tsx index ff3ccdcf..fc524e26 100644 --- a/packages/keybr-pages-browser/lib/SubMenu.test.tsx +++ b/packages/keybr-pages-browser/lib/SubMenu.test.tsx @@ -20,6 +20,7 @@ test("render", () => { premium: false, }, settings: null, + customText: null, }} > diff --git a/packages/keybr-pages-browser/lib/Template.test.tsx b/packages/keybr-pages-browser/lib/Template.test.tsx index 71286112..fabdcc81 100644 --- a/packages/keybr-pages-browser/lib/Template.test.tsx +++ b/packages/keybr-pages-browser/lib/Template.test.tsx @@ -19,6 +19,7 @@ test("render", () => { imageUrl: null, }, settings: null, + customText: null, }} > @@ -50,6 +51,7 @@ test("render alt", () => { premium: true, }, settings: null, + customText: null, }} > diff --git a/packages/keybr-pages-server/lib/Shell.test.tsx b/packages/keybr-pages-server/lib/Shell.test.tsx index 6b877b1e..d2bd0b11 100644 --- a/packages/keybr-pages-server/lib/Shell.test.tsx +++ b/packages/keybr-pages-server/lib/Shell.test.tsx @@ -22,6 +22,7 @@ test("render", () => { imageUrl: null, }, settings: null, + customText: null, }} > @@ -60,6 +61,7 @@ test("render alt", () => { premium: true, }, settings: null, + customText: null, }} > @@ -97,6 +99,7 @@ test("render for a bot", () => { imageUrl: null, }, settings: null, + customText: null, }} > diff --git a/packages/keybr-pages-shared/lib/UrlTextContext.tsx b/packages/keybr-pages-shared/lib/UrlTextContext.tsx new file mode 100644 index 00000000..1a52eb84 --- /dev/null +++ b/packages/keybr-pages-shared/lib/UrlTextContext.tsx @@ -0,0 +1,7 @@ +import { createContext, useContext } from "react"; + +export const UrlTextContext = createContext(null); + +export function useUrlText(): string | null { + return useContext(UrlTextContext); +} diff --git a/packages/keybr-pages-shared/lib/index.ts b/packages/keybr-pages-shared/lib/index.ts index b28856c8..921a30cd 100644 --- a/packages/keybr-pages-shared/lib/index.ts +++ b/packages/keybr-pages-shared/lib/index.ts @@ -5,4 +5,5 @@ export * from "./pages.ts"; export * from "./Root.tsx"; export * from "./Screen.tsx"; export * from "./types.ts"; +export * from "./UrlTextContext.tsx"; export * from "./UserName.tsx"; diff --git a/packages/keybr-pages-shared/lib/types.ts b/packages/keybr-pages-shared/lib/types.ts index 40ca1340..676e9d03 100644 --- a/packages/keybr-pages-shared/lib/types.ts +++ b/packages/keybr-pages-shared/lib/types.ts @@ -32,6 +32,10 @@ export type PageData = { * Serialized user settings. */ readonly settings: unknown | null; + /** + * Custom text from URL query parameter. + */ + readonly customText: string | null; }; export type UserDetails = { diff --git a/packages/page-account/lib/AccountPage.test.tsx b/packages/page-account/lib/AccountPage.test.tsx index e2f78e62..702580e4 100644 --- a/packages/page-account/lib/AccountPage.test.tsx +++ b/packages/page-account/lib/AccountPage.test.tsx @@ -19,6 +19,7 @@ test("render sign-in fragment", () => { premium: false, }, settings: null, + customText: null, }} > @@ -64,6 +65,7 @@ test("render account fragment", () => { premium: false, }, settings: null, + customText: null, }} > diff --git a/packages/page-practice/lib/PracticePage.tsx b/packages/page-practice/lib/PracticePage.tsx index 52276641..ded6d90a 100644 --- a/packages/page-practice/lib/PracticePage.tsx +++ b/packages/page-practice/lib/PracticePage.tsx @@ -1,6 +1,9 @@ import { KeyboardOptions, Layout } from "@keybr/keyboard"; +import { UrlTextContext } from "@keybr/pages-shared"; import { Settings } from "@keybr/settings"; import { ViewSwitch } from "@keybr/widget"; +import { type ReactNode } from "react"; +import { useUrlCustomText } from "./practice/useUrlCustomText.ts"; import { views } from "./views.tsx"; setDefaultLayout(window.navigator.language); @@ -18,5 +21,10 @@ function setDefaultLayout(localeId: string) { } export function PracticePage() { - return ; + const urlText = useUrlCustomText(); + return ( + + + + ); } diff --git a/packages/page-practice/lib/practice/PracticeScreen.test.tsx b/packages/page-practice/lib/practice/PracticeScreen.test.tsx index 8c7914a6..52b16522 100644 --- a/packages/page-practice/lib/practice/PracticeScreen.test.tsx +++ b/packages/page-practice/lib/practice/PracticeScreen.test.tsx @@ -1,6 +1,7 @@ import { test } from "node:test"; import { FakeIntlProvider } from "@keybr/intl"; import { lessonProps, LessonType } from "@keybr/lesson"; +import { type PageData, PageDataContext } from "@keybr/pages-shared"; import { FakePhoneticModel } from "@keybr/phonetic-model"; import { PhoneticModelLoader } from "@keybr/phonetic-model-loader"; import { FakeResultContext, ResultFaker } from "@keybr/result"; @@ -14,18 +15,29 @@ const faker = new ResultFaker(); test("render", async () => { PhoneticModelLoader.loader = FakePhoneticModel.loader; + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: null, + }; + const r = render( - - - - - - - , + + + + + + + + + , ); isNotNull(await r.findByTitle("Change lesson settings", { exact: false })); diff --git a/packages/page-practice/lib/practice/useUrlCustomText.test.tsx b/packages/page-practice/lib/practice/useUrlCustomText.test.tsx new file mode 100644 index 00000000..0c345c97 --- /dev/null +++ b/packages/page-practice/lib/practice/useUrlCustomText.test.tsx @@ -0,0 +1,334 @@ +import { test } from "node:test"; +import { lessonProps } from "@keybr/lesson"; +import { type PageData, PageDataContext } from "@keybr/pages-shared"; +import { Settings } from "@keybr/settings"; +import { FakeSettingsContext } from "@keybr/settings"; +import { renderHook, waitFor } from "@testing-library/react"; +import { equal, isNull } from "rich-assert"; +import { useUrlCustomText } from "./useUrlCustomText.ts"; + +function createWrapper(pageData: PageData) { + return function Wrapper({ + children, + }: { + readonly children: React.ReactNode; + }) { + return ( + + {children} + + ); + }; +} + +test("returns null when no custom text in page data", async () => { + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: null, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + isNull(result.current); + }); +}); + +test("returns trimmed text when custom text provided", async () => { + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: " Hello World ", + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current, "Hello World"); + }); +}); + +test("truncates text to max length", async () => { + const longText = "A".repeat(15000); + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: longText, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current?.length, 10000); + }); +}); + +test("returns null for empty string", async () => { + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: " ", + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + isNull(result.current); + }); +}); + +test("returns null for whitespace only", async () => { + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: "\t\n\r", + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + isNull(result.current); + }); +}); + +test("handles special characters from URL encoding", async () => { + const specialText = "Hello%20World%21%20%40%23%24%25%5E%26%2A%28%29"; + const decodedText = "Hello World! @#$%^&*()"; + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: decodedText, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current, decodedText); + }); +}); + +test("handles unicode and emoji characters", async () => { + const unicodeText = "Hello 世界 🌍🎉 Test"; + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: unicodeText, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current, unicodeText); + }); +}); + +test("handles mix of special characters including quotes", async () => { + const textWithQuotes = + "Text with \"double quotes\" and 'single quotes' and `backticks`"; + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: textWithQuotes, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current, textWithQuotes); + }); +}); + +test("handles HTML-like characters safely", async () => { + const htmlLikeText = + "\u003Cdiv\u003ETest\u003C/div\u003E & <tag> "quotes""; + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: htmlLikeText, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current, htmlLikeText); + }); +}); + +test("handles text with newlines and tabs", async () => { + const textWithWhitespace = "Line 1\nLine 2\tTabbed\r\nLine 4"; + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: textWithWhitespace, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + // Text should be preserved as-is (except leading/trailing trim) + equal(result.current, textWithWhitespace); + }); +}); + +test("handles text near browser URL limit (~2000 chars)", async () => { + // Most browsers limit URLs to ~2000 characters + // Testing that the code handles near-limit length correctly + const repeatUnit = "!@#$%^&*()[]{}<>?/\\\\|~`\"':;-+=abc"; + const longSpecialText = repeatUnit.repeat(25); // ~925 chars, well within URL limits + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: longSpecialText, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + if (result.current) { + equal(result.current, longSpecialText); + } else { + throw new Error("Result should not be null"); + } + }); +}); + +test("handles zero-width characters and invisible unicode", async () => { + const invisibleText = "Test\u200B\u200C\u200D\uFEFFText"; // zero-width chars + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: invisibleText, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current, invisibleText); + }); +}); + +test("handles URL-encoded newlines and special chars", async () => { + const urlEncodedText = "Line1%0ALine2%0D%0ALine3%09Tabbed"; + const decodedText = "Line1\nLine2\r\nLine3\tTabbed"; + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: null, + customText: decodedText, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current, decodedText); + }); +}); + +test("preserves uppercase letters by disabling lowercase setting", async () => { + const textWithUppercase = "Hello World with CAPITAL Letters"; + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: new Settings() + .set(lessonProps.customText.lowercase, true) + .toJSON(), + customText: textWithUppercase, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current, textWithUppercase); + }); +}); + +test("keeps lowercase setting enabled for lowercase-only text", async () => { + const lowercaseText = "hello world with only lowercase letters"; + const mockPageData: PageData = { + base: "https://example.com", + locale: "en", + user: null, + publicUser: { id: null, name: "Anonymous", imageUrl: null }, + settings: new Settings() + .set(lessonProps.customText.lowercase, true) + .toJSON(), + customText: lowercaseText, + }; + + const { result } = renderHook(() => useUrlCustomText(), { + wrapper: createWrapper(mockPageData), + }); + + await waitFor(() => { + equal(result.current, lowercaseText); + }); +}); diff --git a/packages/page-practice/lib/practice/useUrlCustomText.ts b/packages/page-practice/lib/practice/useUrlCustomText.ts new file mode 100644 index 00000000..0ea5b58c --- /dev/null +++ b/packages/page-practice/lib/practice/useUrlCustomText.ts @@ -0,0 +1,80 @@ +import { lessonProps, LessonType } from "@keybr/lesson"; +import { usePageData } from "@keybr/pages-shared"; +import { useSettings } from "@keybr/settings"; +import { useEffect, useRef, useState } from "react"; + +const MAX_CUSTOM_TEXT_LENGTH = 10_000; + +/** + * Hook to apply custom text from URL query parameter. + * This is session-only and does NOT persist to settings. + * + * The text is only applied once on mount and respects the maximum + * length restriction of 10,000 characters. + * + * Returns the URL text if provided and not yet applied, null otherwise. + * Once applied to settings, returns null to allow normal editing. + */ +export function useUrlCustomText(): string | null { + const pageData = usePageData(); + const { settings, updateSettings } = useSettings(); + const [urlText, setUrlText] = useState(null); + const hasProcessed = useRef(false); + + useEffect(() => { + if (hasProcessed.current) { + return; + } + hasProcessed.current = true; + + const customText = pageData.customText; + if (customText == null || customText.trim() === "") { + return; + } + + // Trim and apply length restriction + const trimmedText = customText.trim().slice(0, MAX_CUSTOM_TEXT_LENGTH); + + // Check if text contains uppercase letters + const hasUppercase = /[A-Z]/.test(trimmedText); + + // Prepare settings updates - start with content + let updatedSettings = settings.set( + lessonProps.customText.content, + trimmedText, + ); + + // Only disable lowercase if text has uppercase letters + if (hasUppercase) { + updatedSettings = updatedSettings.set( + lessonProps.customText.lowercase, + false, + ); + } + + // Switch lesson type to CUSTOM + if (settings.get(lessonProps.type) !== LessonType.CUSTOM) { + updatedSettings = updatedSettings.set( + lessonProps.type, + LessonType.CUSTOM, + ); + } + + // Save all settings at once + updateSettings(updatedSettings); + + // Remove ?text= from URL + const url = new URL(window.location.href); + url.searchParams.delete("text"); + window.history.replaceState({}, "", url.pathname + url.search); + + // Set the text in state for the initial render + setUrlText(trimmedText); + + // Clear it immediately after so subsequent renders get null + // This allows the user to edit and switch modes normally + setTimeout(() => setUrlText(null), 0); + }, [pageData, settings, updateSettings]); + + return urlText; +} diff --git a/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx b/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx index 659ce992..ff487a33 100644 --- a/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx +++ b/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx @@ -1,6 +1,7 @@ import { useIntlNumbers } from "@keybr/intl"; import { type Language } from "@keybr/keyboard"; import { type CustomTextLesson, lessonProps } from "@keybr/lesson"; +import { useUrlText } from "@keybr/pages-shared"; import { useSettings } from "@keybr/settings"; import { textStatsOf } from "@keybr/unicode"; import { @@ -28,6 +29,9 @@ export function CustomTextLessonSettings({ }): ReactNode { const { formatMessage } = useIntl(); const { settings } = useSettings(); + const urlText = useUrlText(); + // Use URL text if available, otherwise use saved settings text + const customText = urlText ?? settings.get(lessonProps.customText.content); return ( <> @@ -48,7 +52,7 @@ export function CustomTextLessonSettings({ @@ -61,6 +65,10 @@ export function CustomTextLessonSettings({ function CustomTextInput(): ReactNode { const { formatMessage } = useIntl(); const { settings, updateSettings } = useSettings(); + const urlText = useUrlText(); + // Use URL text if available, otherwise use saved settings text + const currentText = urlText ?? settings.get(lessonProps.customText.content); + const isUrlText = urlText != null; return ( <> @@ -80,6 +88,14 @@ function CustomTextInput(): ReactNode { ))} + {isUrlText && ( + + + + )} { + // When user edits the text, save it to settings + // This overrides the URL text for future use updateSettings(settings.set(lessonProps.customText.content, value)); }} /> diff --git a/packages/server/lib/app/page/controller.tsx b/packages/server/lib/app/page/controller.tsx index a5d9462a..ff98ff87 100644 --- a/packages/server/lib/app/page/controller.tsx +++ b/packages/server/lib/app/page/controller.tsx @@ -189,12 +189,28 @@ export class Controller { ): Promise { const { user, publicUser } = ctx.state; const settings = user != null ? await this.database.get(user.id!) : null; + + // Extract and validate URL parameter + const url = new URL(ctx.request.url, ctx.request.origin); + const rawText = url.searchParams.get("text"); + + // Server-side validation and truncation + const MAX_URL_TEXT_LENGTH = 10_000; + let customText: string | null = null; + if (rawText != null) { + const trimmed = rawText.trim(); + if (trimmed.length > 0) { + customText = trimmed.slice(0, MAX_URL_TEXT_LENGTH); + } + } + return { base: this.canonicalUrl, locale, user: user?.toDetails() ?? null, publicUser, settings: settings?.toJSON() ?? null, + customText, }; }