From 9442e8c2aa4df17965e1dc99cce429084e05bcb2 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Sun, 22 Feb 2026 22:37:47 +0100 Subject: [PATCH 01/14] feat: add custom text via URL query parameter Allow users to practice custom text submitted via the `text` URL query parameter. Features: - Extract `text` parameter from URL on server-side - Pass custom text to client via PageData - Apply text to settings using React hook on mount - Show visual indicator when text is loaded from URL - Enforce 10,000 character limit Usage: http://localhost:3000/?text=Hello%20World Co-Authored-By: Claude Sonnet 4.6 --- packages/keybr-pages-shared/lib/types.ts | 4 +++ .../lib/practice/PracticeScreen.tsx | 2 ++ .../lib/practice/useUrlCustomText.ts | 33 +++++++++++++++++++ .../lesson/CustomTextLessonSettings.tsx | 15 ++++++++- packages/server/lib/app/page/controller.tsx | 3 ++ 5 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 packages/page-practice/lib/practice/useUrlCustomText.ts 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-practice/lib/practice/PracticeScreen.tsx b/packages/page-practice/lib/practice/PracticeScreen.tsx index d2c93285..5797eb25 100644 --- a/packages/page-practice/lib/practice/PracticeScreen.tsx +++ b/packages/page-practice/lib/practice/PracticeScreen.tsx @@ -9,8 +9,10 @@ import { useSettings } from "@keybr/settings"; import { useEffect, useMemo, useState } from "react"; import { Controller } from "./Controller.tsx"; import { displayEvent, Progress } from "./state/index.ts"; +import { useUrlCustomText } from "./useUrlCustomText.ts"; export function PracticeScreen() { + useUrlCustomText(); return ( diff --git a/packages/page-practice/lib/practice/useUrlCustomText.ts b/packages/page-practice/lib/practice/useUrlCustomText.ts new file mode 100644 index 00000000..147ee801 --- /dev/null +++ b/packages/page-practice/lib/practice/useUrlCustomText.ts @@ -0,0 +1,33 @@ +import { lessonProps } from "@keybr/lesson"; +import { usePageData } from "@keybr/pages-shared"; +import { useSettings } from "@keybr/settings"; +import { useEffect } from "react"; + +const MAX_CUSTOM_TEXT_LENGTH = 10_000; + +/** + * Hook to apply custom text from URL query parameter to settings. + * This reads the 'text' query parameter from the URL and sets it as + * the custom text content in the lesson settings. + * + * The text is only applied once on mount and respects the maximum + * length restriction of 10,000 characters. + */ +export function useUrlCustomText(): void { + const pageData = usePageData(); + const { settings, updateSettings } = useSettings(); + + useEffect(() => { + // Only apply if customText is provided in page data + const customText = pageData.customText; + if (customText == null || customText.trim() === "") { + return; + } + + // Apply length restriction + const trimmedText = customText.trim().slice(0, MAX_CUSTOM_TEXT_LENGTH); + + // Update settings with the custom text + updateSettings(settings.set(lessonProps.customText.content, trimmedText)); + }, [pageData, settings, updateSettings]); +} diff --git a/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx b/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx index 659ce992..e2f70c2f 100644 --- a/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx +++ b/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx @@ -61,6 +61,11 @@ export function CustomTextLessonSettings({ function CustomTextInput(): ReactNode { const { formatMessage } = useIntl(); const { settings, updateSettings } = useSettings(); + const currentText = settings.get(lessonProps.customText.content); + const isUrlText = useMemo(() => { + const url = new URL(window.location.href); + return url.searchParams.has("text"); + }, []); return ( <> @@ -80,6 +85,14 @@ function CustomTextInput(): ReactNode { ))} + {isUrlText && ( + + + + )} { 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..ad2e75ec 100644 --- a/packages/server/lib/app/page/controller.tsx +++ b/packages/server/lib/app/page/controller.tsx @@ -189,12 +189,15 @@ export class Controller { ): Promise { const { user, publicUser } = ctx.state; const settings = user != null ? await this.database.get(user.id!) : null; + const url = new URL(ctx.request.url, ctx.request.origin); + const customText = url.searchParams.get("text"); return { base: this.canonicalUrl, locale, user: user?.toDetails() ?? null, publicUser, settings: settings?.toJSON() ?? null, + customText, }; } From 6143eb21d809ba61b9e650eea50dc5089977ddca Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Sun, 22 Feb 2026 22:44:27 +0100 Subject: [PATCH 02/14] fix: prevent infinite re-render loop in useUrlCustomText Add useRef to track if custom text has been applied, preventing the useEffect from running repeatedly and causing React error #185. Co-Authored-By: Claude Sonnet 4.6 --- .../page-practice/lib/practice/useUrlCustomText.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/page-practice/lib/practice/useUrlCustomText.ts b/packages/page-practice/lib/practice/useUrlCustomText.ts index 147ee801..6ef4580c 100644 --- a/packages/page-practice/lib/practice/useUrlCustomText.ts +++ b/packages/page-practice/lib/practice/useUrlCustomText.ts @@ -1,7 +1,7 @@ import { lessonProps } from "@keybr/lesson"; import { usePageData } from "@keybr/pages-shared"; import { useSettings } from "@keybr/settings"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; const MAX_CUSTOM_TEXT_LENGTH = 10_000; @@ -16,14 +16,18 @@ const MAX_CUSTOM_TEXT_LENGTH = 10_000; export function useUrlCustomText(): void { const pageData = usePageData(); const { settings, updateSettings } = useSettings(); + const hasApplied = useRef(false); useEffect(() => { - // Only apply if customText is provided in page data + // Only apply if customText is provided in page data and we haven't applied it yet const customText = pageData.customText; - if (customText == null || customText.trim() === "") { + if (customText == null || customText.trim() === "" || hasApplied.current) { return; } + // Mark as applied to prevent infinite loop + hasApplied.current = true; + // Apply length restriction const trimmedText = customText.trim().slice(0, MAX_CUSTOM_TEXT_LENGTH); From 1ff5723d41b181539b264502e02dd69b6bd4b8de Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Sun, 22 Feb 2026 22:47:27 +0100 Subject: [PATCH 03/14] fix: auto-switch to CUSTOM lesson type when text provided via URL When custom text is provided via URL parameter, automatically switch the lesson type to CUSTOM so the text is immediately available for practice without manual mode switching. Co-Authored-By: Claude Sonnet 4.6 --- .../page-practice/lib/practice/useUrlCustomText.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/page-practice/lib/practice/useUrlCustomText.ts b/packages/page-practice/lib/practice/useUrlCustomText.ts index 6ef4580c..176f3543 100644 --- a/packages/page-practice/lib/practice/useUrlCustomText.ts +++ b/packages/page-practice/lib/practice/useUrlCustomText.ts @@ -1,4 +1,4 @@ -import { lessonProps } from "@keybr/lesson"; +import { lessonProps,LessonType } from "@keybr/lesson"; import { usePageData } from "@keybr/pages-shared"; import { useSettings } from "@keybr/settings"; import { useEffect, useRef } from "react"; @@ -12,6 +12,8 @@ const MAX_CUSTOM_TEXT_LENGTH = 10_000; * * The text is only applied once on mount and respects the maximum * length restriction of 10,000 characters. + * + * Also automatically switches the lesson type to CUSTOM when text is provided. */ export function useUrlCustomText(): void { const pageData = usePageData(); @@ -31,7 +33,11 @@ export function useUrlCustomText(): void { // Apply length restriction const trimmedText = customText.trim().slice(0, MAX_CUSTOM_TEXT_LENGTH); - // Update settings with the custom text - updateSettings(settings.set(lessonProps.customText.content, trimmedText)); + // Update settings with the custom text and switch to CUSTOM lesson type + updateSettings( + settings + .set(lessonProps.customText.content, trimmedText) + .set(lessonProps.type, LessonType.CUSTOM), + ); }, [pageData, settings, updateSettings]); } From 6b3c8a124098c5c574be23f2ac9171c37d1ea9cf Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Sun, 22 Feb 2026 22:54:12 +0100 Subject: [PATCH 04/14] test: add customText to PageData in all test files Add customText: null to all PageDataContext.Provider values in test files to satisfy the updated PageData type which now includes the customText field. Co-Authored-By: Claude Sonnet 4.6 --- packages/keybr-pages-browser/lib/NavMenu.test.tsx | 1 + packages/keybr-pages-browser/lib/SubMenu.test.tsx | 1 + packages/keybr-pages-browser/lib/Template.test.tsx | 2 ++ packages/keybr-pages-server/lib/Shell.test.tsx | 3 +++ packages/page-account/lib/AccountPage.test.tsx | 2 ++ 5 files changed, 9 insertions(+) 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/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, }} > From 410e93a72528352ce56b3fa537acfa1c4f1643f5 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Sun, 22 Feb 2026 23:31:38 +0100 Subject: [PATCH 05/14] fix: make URL custom text session-only and add critical improvements This commit fixes critical issues with the custom text via URL feature: ## Session-Only URL Text (Fixes Settings Persistence) - URL-loaded text is no longer persisted to user settings - Created UrlTextContext in @keybr/pages-shared to pass URL text - Modified CustomTextLesson to accept optional initialText parameter - useUrlCustomText now returns text instead of persisting it - Only switches lesson type, does not modify customText.content setting ## Server-Side Validation (Security Fix) - Added validation and truncation on server before sending to client - Prevents abuse with extremely long URLs - MAX_URL_TEXT_LENGTH = 10_000 characters enforced server-side ## Unit Tests - Added comprehensive unit tests for useUrlCustomText hook - Tests cover: null input, trimming, truncation, whitespace handling ## i18n Translations - Added translation keys for URL text message in: - English (en.json) - German (de.json) - French (fr.json) - Spanish (es.json) This ensures users don't get "stuck" with text from URLs they visited once, and provides proper security validation and test coverage. Co-Authored-By: Claude Sonnet 4.6 --- packages/keybr-intl/translations/de.json | 4 +- packages/keybr-intl/translations/en.json | 4 +- packages/keybr-intl/translations/es.json | 4 +- packages/keybr-intl/translations/fr.json | 4 +- .../keybr-lesson-loader/lib/LessonLoader.tsx | 14 ++- packages/keybr-lesson/lib/customtext.ts | 16 ++- .../keybr-pages-shared/lib/UrlTextContext.tsx | 7 ++ packages/keybr-pages-shared/lib/index.ts | 1 + .../lib/practice/PracticeScreen.tsx | 16 +-- .../lib/practice/useUrlCustomText.test.tsx | 108 ++++++++++++++++++ .../lib/practice/useUrlCustomText.ts | 36 +++--- packages/server/lib/app/page/controller.tsx | 15 ++- pr-research | 1 + 13 files changed, 193 insertions(+), 37 deletions(-) create mode 100644 packages/keybr-pages-shared/lib/UrlTextContext.tsx create mode 100644 packages/page-practice/lib/practice/useUrlCustomText.test.tsx create mode 120000 pr-research 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/en.json b/packages/keybr-intl/translations/en.json index d7811bbb..befa8db6 100644 --- a/packages/keybr-intl/translations/en.json +++ b/packages/keybr-intl/translations/en.json @@ -340,5 +340,7 @@ "t_ws_Bar_whitespace": "Bar whitespace", "t_ws_Bullet_whitespace": "Bullet whitespace", "t_ws_No_whitespace": "No whitespace", - "weekDayNames": "M|T|W|T|F|S|S" + "weekDayNames": "M|T|W|T|F|S|S", + "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." } 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/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-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-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/page-practice/lib/practice/PracticeScreen.tsx b/packages/page-practice/lib/practice/PracticeScreen.tsx index 5797eb25..c3e1b51c 100644 --- a/packages/page-practice/lib/practice/PracticeScreen.tsx +++ b/packages/page-practice/lib/practice/PracticeScreen.tsx @@ -3,7 +3,7 @@ import { KeyboardProvider } from "@keybr/keyboard"; import { schedule } from "@keybr/lang"; import { type Lesson } from "@keybr/lesson"; import { LessonLoader } from "@keybr/lesson-loader"; -import { LoadingProgress } from "@keybr/pages-shared"; +import { LoadingProgress, UrlTextContext } from "@keybr/pages-shared"; import { type Result, useResults } from "@keybr/result"; import { useSettings } from "@keybr/settings"; import { useEffect, useMemo, useState } from "react"; @@ -12,13 +12,15 @@ import { displayEvent, Progress } from "./state/index.ts"; import { useUrlCustomText } from "./useUrlCustomText.ts"; export function PracticeScreen() { - useUrlCustomText(); + const urlText = useUrlCustomText(); return ( - - - {(lesson) => } - - + + + + {(lesson) => } + + + ); } 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..2b17218b --- /dev/null +++ b/packages/page-practice/lib/practice/useUrlCustomText.test.tsx @@ -0,0 +1,108 @@ +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 } 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", () => { + 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), + }); + + isNull(result.current); +}); + +test("returns trimmed text when custom text provided", () => { + 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), + }); + + equal(result.current, "Hello World"); +}); + +test("truncates text to max length", () => { + 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), + }); + + equal(result.current?.length, 10000); +}); + +test("returns null for empty string", () => { + 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), + }); + + isNull(result.current); +}); + +test("returns null for whitespace only", () => { + 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), + }); + + isNull(result.current); +}); diff --git a/packages/page-practice/lib/practice/useUrlCustomText.ts b/packages/page-practice/lib/practice/useUrlCustomText.ts index 176f3543..bf490c24 100644 --- a/packages/page-practice/lib/practice/useUrlCustomText.ts +++ b/packages/page-practice/lib/practice/useUrlCustomText.ts @@ -1,43 +1,39 @@ -import { lessonProps,LessonType } from "@keybr/lesson"; +import { lessonProps, LessonType } from "@keybr/lesson"; import { usePageData } from "@keybr/pages-shared"; import { useSettings } from "@keybr/settings"; -import { useEffect, useRef } from "react"; +import { useEffect, useState } from "react"; const MAX_CUSTOM_TEXT_LENGTH = 10_000; /** - * Hook to apply custom text from URL query parameter to settings. - * This reads the 'text' query parameter from the URL and sets it as - * the custom text content in the lesson settings. + * 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. * - * Also automatically switches the lesson type to CUSTOM when text is provided. + * Returns the URL text if provided, null otherwise. */ -export function useUrlCustomText(): void { +export function useUrlCustomText(): string | null { const pageData = usePageData(); const { settings, updateSettings } = useSettings(); - const hasApplied = useRef(false); + const [urlText, setUrlText] = useState(null); useEffect(() => { - // Only apply if customText is provided in page data and we haven't applied it yet const customText = pageData.customText; - if (customText == null || customText.trim() === "" || hasApplied.current) { + if (customText == null || customText.trim() === "") { return; } - // Mark as applied to prevent infinite loop - hasApplied.current = true; - - // Apply length restriction + // Trim and apply length restriction const trimmedText = customText.trim().slice(0, MAX_CUSTOM_TEXT_LENGTH); + setUrlText(trimmedText); - // Update settings with the custom text and switch to CUSTOM lesson type - updateSettings( - settings - .set(lessonProps.customText.content, trimmedText) - .set(lessonProps.type, LessonType.CUSTOM), - ); + // Only switch lesson type, don't modify customText content in settings + if (settings.get(lessonProps.type) !== LessonType.CUSTOM) { + updateSettings(settings.set(lessonProps.type, LessonType.CUSTOM)); + } }, [pageData, settings, updateSettings]); + + return urlText; } diff --git a/packages/server/lib/app/page/controller.tsx b/packages/server/lib/app/page/controller.tsx index ad2e75ec..ff98ff87 100644 --- a/packages/server/lib/app/page/controller.tsx +++ b/packages/server/lib/app/page/controller.tsx @@ -189,8 +189,21 @@ 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 customText = url.searchParams.get("text"); + 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, diff --git a/pr-research b/pr-research new file mode 120000 index 00000000..cef0049c --- /dev/null +++ b/pr-research @@ -0,0 +1 @@ +/home/konrad/gallery/keybr-prs \ No newline at end of file From 8095f6bb785ba9e69b08851775166d612d7215b5 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Sun, 22 Feb 2026 23:47:39 +0100 Subject: [PATCH 06/14] fix: display URL text in settings UI and allow editing Fixes issues when custom text is loaded from URL: - URL text now displays correctly in the textarea (was showing placeholder/saved text) - URL text stats now display correctly (word count, etc.) - Users can now edit the text, which saves it to settings - Added useUrlText hook to CustomTextLessonSettings This resolves the confusing behavior where URL text was active but the UI showed the old saved text or placeholder. Co-Authored-By: Claude Sonnet 4.6 --- packages/keybr-intl/translations/af.json | 2 ++ packages/keybr-intl/translations/ar.json | 2 ++ packages/keybr-intl/translations/bg.json | 2 ++ packages/keybr-intl/translations/bn.json | 2 ++ packages/keybr-intl/translations/ca.json | 2 ++ packages/keybr-intl/translations/cs.json | 2 ++ packages/keybr-intl/translations/da.json | 2 ++ packages/keybr-intl/translations/el.json | 2 ++ packages/keybr-intl/translations/eo.json | 2 ++ packages/keybr-intl/translations/et.json | 2 ++ packages/keybr-intl/translations/fa.json | 2 ++ packages/keybr-intl/translations/fo.json | 2 ++ packages/keybr-intl/translations/ga.json | 2 ++ packages/keybr-intl/translations/he.json | 2 ++ packages/keybr-intl/translations/hr.json | 2 ++ packages/keybr-intl/translations/hu.json | 2 ++ packages/keybr-intl/translations/id.json | 2 ++ packages/keybr-intl/translations/is.json | 2 ++ packages/keybr-intl/translations/it.json | 2 ++ packages/keybr-intl/translations/ja.json | 2 ++ packages/keybr-intl/translations/ko.json | 2 ++ packages/keybr-intl/translations/lt.json | 2 ++ packages/keybr-intl/translations/mn.json | 2 ++ packages/keybr-intl/translations/nb.json | 2 ++ packages/keybr-intl/translations/ne.json | 2 ++ packages/keybr-intl/translations/nl.json | 2 ++ packages/keybr-intl/translations/pl.json | 2 ++ packages/keybr-intl/translations/pt-br.json | 2 ++ packages/keybr-intl/translations/pt-pt.json | 2 ++ packages/keybr-intl/translations/ro.json | 2 ++ packages/keybr-intl/translations/ru.json | 2 ++ packages/keybr-intl/translations/sk.json | 2 ++ packages/keybr-intl/translations/sl.json | 2 ++ packages/keybr-intl/translations/sq.json | 2 ++ packages/keybr-intl/translations/sv.json | 2 ++ packages/keybr-intl/translations/th.json | 2 ++ packages/keybr-intl/translations/tr.json | 2 ++ packages/keybr-intl/translations/uk.json | 2 ++ packages/keybr-intl/translations/vi.json | 2 ++ packages/keybr-intl/translations/zh-hans.json | 2 ++ packages/keybr-intl/translations/zh-hant.json | 2 ++ packages/keybr-intl/translations/zh-tw.json | 2 ++ .../lib/practice/PracticeScreen.test.tsx | 34 +++++++++++++------ .../lib/practice/useUrlCustomText.test.tsx | 32 +++++++++++------ .../lesson/CustomTextLessonSettings.tsx | 17 ++++++---- 45 files changed, 139 insertions(+), 28 deletions(-) diff --git a/packages/keybr-intl/translations/af.json b/packages/keybr-intl/translations/af.json index a79ebe93..3877c26b 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": "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": "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..aa0d395d 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": "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": "إنشاء دروس كتابة باستخدام كلمات عشوائية باستخدام قواعد صوتيات لغتك. يتم توسيع مجموعة المفاتيح ديناميكيًا بناءً على أدائك. هذا الوضع للمبتدئين.", "lessonType.numbers.description": "التدرب على الأرقام فقط.", "lessonType.syntax.description": "إنشاء دروس تشبه بناء جمل لغة البرمجة المحددة.", diff --git a/packages/keybr-intl/translations/bg.json b/packages/keybr-intl/translations/bg.json index 9f87168e..e9649a4e 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": "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": "Генерирайте уроци по писане с произволни думи, като използвате фонетичните правила на вашия език. Наборът клавиши се разширява динамично въз основа на вашето представяне. Този режим е за начинаещи.", "lessonType.numbers.description": "Упражнявайте само числа.", "lessonType.syntax.description": "Генерирайте уроци, които наподобяват посочения синтаксис на езика за програмиране.", diff --git a/packages/keybr-intl/translations/bn.json b/packages/keybr-intl/translations/bn.json index 18dfabce..b27b88af 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": "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": "আপনার ভাষার ধ্বনিতাত্ত্বিক নিয়ম ব্যবহার করে এলোমেলো শব্দ দিয়ে টাইপিং পাঠ তৈরি করুন। আপনার পারফরম্যান্সের উপর ভিত্তি করে কী সেট গতিশীলভাবে প্রসারিত হয়। এই মোডটি নতুনদের জন্য।", "lessonType.numbers.description": "শুধুমাত্র সংখ্যা অনুশীলন করুন।", "lessonType.syntax.description": "নির্দিষ্ট প্রোগ্রামিং ভাষার সিনট্যাক্সের অনুরূপ পাঠ তৈরি করুন।", diff --git a/packages/keybr-intl/translations/ca.json b/packages/keybr-intl/translations/ca.json index dc0a98f9..80a00eeb 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": "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": "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 ec31ddda..3670f587 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": "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": "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..5abcbeb8 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": "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": "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/el.json b/packages/keybr-intl/translations/el.json index 22506a4e..1cdb405d 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": "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": "Δημιουργούνται μαθήματα πληκτρολόγησης με τυχαίες λέξεις χρησιμοποιώντας τους φωνητικούς κανόνες της γλώσσας σας. Το σετ των πλήκτρων επεκτείνεται δυναμικά με βάση την απόδοσή σας. Αυτή η λειτουργία είναι για αρχάριους.", "lessonType.numbers.description": "Μόνο αριθμοί για εξάσκηση.", "lessonType.syntax.description": "Δημιουργήστε μαθήματα που μοιάζουν με τη σύνταξη της καθορισμένης γλώσσας προγραμματισμού.", diff --git a/packages/keybr-intl/translations/eo.json b/packages/keybr-intl/translations/eo.json index 69b47346..5780ba34 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": "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.", "t_Account": "Konta", "t_Account_details": "Konta Detaloj", "t_Account_name": "Konta | {name}", diff --git a/packages/keybr-intl/translations/et.json b/packages/keybr-intl/translations/et.json index 5624b8ed..19f3f0cf 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": "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": "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 5912630c..4b729230 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": "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": "تولید درس‌های تایپ با کلمات تصادفی با استفاده از قوانین آوایی زبان شما. مجموعه کلید‌ها بر اساس عملکرد شما به شکل پویا گسترش می‌یابد. این حالت برای افراد مبتدی است.", "lessonType.numbers.description": "فقط شماره‌ها را تمرین کنید.", "lessonType.wordList.description": "ایجاد جلسات تمرینی با استفاده از پرکاربرد ترین کلمات زبان شما. تمام کلید ها به صورت پیش‌فرض در نظر گرفته شده اند. این حالت مخصوص کاربران حرفه‌ای است.", diff --git a/packages/keybr-intl/translations/fo.json b/packages/keybr-intl/translations/fo.json index c8f4b076..5de740a2 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": "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.", "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/ga.json b/packages/keybr-intl/translations/ga.json index 4f3007dd..6facbe4f 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": "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": "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..7f4c8cd3 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": "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": "צור שיעורי הקלדה עם מילים אקראיות תוך שימוש בחוקים הפונטיים של השפה שלך. ערכת המפתחות מורחבת באופן דינמי על סמך הביצועים שלך. מצב זה מיועד למתחילים.", "lessonType.numbers.description": "התאמן על מספרים בלבד.", "lessonType.syntax.description": "הפק שיעורים הדומים לתחביר שפת התכנות שצוין.", diff --git a/packages/keybr-intl/translations/hr.json b/packages/keybr-intl/translations/hr.json index c3b736a3..778b18df 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": "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": "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..553c1daa 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": "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": "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..fcaff167 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": "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": "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..b34a7f6b 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": "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.", "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..4cc76ea0 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": "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": "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..c66cbbb2 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": "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": "あなたの母国語の音韻規則に基づいて、ランダムな単語を使ったタイピングレッスンを生成します。キーセットはあなたのパフォーマンスに基づいて動的に拡張されます。このモードは初心者向けです。", "lessonType.numbers.description": "数字のみを練習します。", "lessonType.syntax.description": "指定されたプログラミング言語の構文に似たレッスンを生成します。", diff --git a/packages/keybr-intl/translations/ko.json b/packages/keybr-intl/translations/ko.json index f327e783..6a717aa4 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": "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.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 e7208f0f..16be174b 100644 --- a/packages/keybr-intl/translations/lt.json +++ b/packages/keybr-intl/translations/lt.json @@ -65,6 +65,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": "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": "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..ef7055c8 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": "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.", "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 b2ef62e4..aff92170 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": "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.", "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..e1481f45 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": "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": "तपाईंको भाषाको फोनेटिक नियमहरू प्रयोग गरेर अनियमित शब्दहरूसँग टाइपिङ पाठहरू उत्पन्न गर्नुहोस्। तपाईँको कार्यसम्पादनको आधारमा कुञ्जी सेटलाई गतिशील रूपमा विस्तार गरिएको छ। यो मोड शुरुआतीहरूको लागि हो।", "lessonType.numbers.description": "अभ्यास संख्या मात्र।", "lessonType.syntax.description": "निर्दिष्ट प्रोग्रामिङ भाषा सिन्ट्याक्ससँग मिल्ने पाठहरू उत्पन्न गर्नुहोस्।", diff --git a/packages/keybr-intl/translations/nl.json b/packages/keybr-intl/translations/nl.json index 7ec04d12..17acae4c 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": "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": "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..659bfd1f 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": "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": "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..9bee55d8 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": "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": "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 908a6479..c0c02cbb 100644 --- a/packages/keybr-intl/translations/pt-pt.json +++ b/packages/keybr-intl/translations/pt-pt.json @@ -65,6 +65,8 @@ "lesson.indicator.notIncluded": "Uma tecla que ainda não foi incluída nas lições.", "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": "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": "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..331c7501 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": "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": "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..3821b015 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": "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": "Генерировать уроки со случайными (несуществующими) словами используя фонетические правила вашего языка. Набор букв, из которого генерируются слова, изменяется динамически в зависимости от ваших успехов. Этот режим для новичков.", "lessonType.numbers.description": "Практика одних лишь чисел.", "lessonType.syntax.description": "Генерировать уроки, соответствующие синтаксису выбранного языка кода.", diff --git a/packages/keybr-intl/translations/sk.json b/packages/keybr-intl/translations/sk.json index 565bd955..b903c07d 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": "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": "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 e7bd2995..e1a30bf6 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": "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": "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 2e83486a..50ca2538 100644 --- a/packages/keybr-intl/translations/sq.json +++ b/packages/keybr-intl/translations/sq.json @@ -22,6 +22,8 @@ "help.example3": "Shembulli 3, një rritje e konsiderueshme nga më pak se 20 në 40 WPM pas 5 orësh e 30 minutash praktikë gjatë 11 ditëve.", "help.example4": "Shembulli 4, pas 2 orësh e 10 minutash praktikë gjatë 11 ditëve, shpejtësia e shkrimit mbeti rreth 70 WPM (e cila tashmë është mjaft e lartë), por saktësia u përmirësua.", "help.example5": "Shembulli 5, nga 20 në 45 WPM pas rreth 10 orësh praktikë gjatë 22 ditëve (po, ndonjëherë merr më shumë kohë).", + "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.", "t_Account_details": "Detajet e Llogarise", "t_Account_name": "Llogaria | {emri}", "t_Anonymize_me": "Më Bëj Anonim", diff --git a/packages/keybr-intl/translations/sv.json b/packages/keybr-intl/translations/sv.json index 7462f719..11a34d77 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": "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": "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..6b2fb2b4 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": "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": "สร้างแบบฝึกพิมพ์ด้วยคำสุ่มที่ใช้กฎการออกเสียงของภาษาของคุณ เซตปุ่มจะขยายออกอย่างต่อเนื่องตามผลการปฏิบัติของคุณ โหมดนี้เหมาะสำหรับผู้เริ่มต้น", "lessonType.numbers.description": "ฝึกพิมพ์ตัวเลขเท่านั้น", "lessonType.syntax.description": "สร้างบทเรียนที่มีลักษณะคล้ายกับไวยากรณ์ของภาษาการเขียนโปรแกรมที่ระบุ", diff --git a/packages/keybr-intl/translations/tr.json b/packages/keybr-intl/translations/tr.json index 1cddc7e7..b86214bb 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": "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": "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 13916b2c..0848adcb 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": "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": "Генерація випадкових слів, що слідують фонетичним правилам обраної мови. Набір літер, з яких генеруються слова, змінюється динамічно, залежно від ваших успіхів. Цей режим для початківців.", "lessonType.numbers.description": "Тренувати тільки числа.", "lessonType.syntax.description": "Створювати уроки, які будуть нагадувати синтаксис вказаної мови програмування.", diff --git a/packages/keybr-intl/translations/vi.json b/packages/keybr-intl/translations/vi.json index 3c8af30d..5c11b161 100644 --- a/packages/keybr-intl/translations/vi.json +++ b/packages/keybr-intl/translations/vi.json @@ -61,6 +61,8 @@ "lesson.indicator.notCalibrated": "Một khóa không hiệu chỉnh với mức độ tin cậy không xác định. Bạn vẫn chưa nhấn phím này.", "lesson.indicator.notIncluded": "Phím chưa được thêm vào bài học.", "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": "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": "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.wordList.description": "Tạo các bài học gõ từ danh sách các từ phổ biến nhất trong ngôn ngữ của 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.", diff --git a/packages/keybr-intl/translations/zh-hans.json b/packages/keybr-intl/translations/zh-hans.json index 3d6681b8..09f220c7 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": "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": "利用语言的语音规则生成随机单词的打字课程。按键集会根据你的表现动态扩展。该模式适合初学者。", "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..0525464c 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": "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": "利用語言的語音規則產生隨機單字的打字課程。按鍵集會根據你的表現動態擴展。此模式適合初學者。", "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..de56663a 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": "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": "利用語言的語音規則產生隨機單字的打字課程。按鍵集會根據你的表現動態擴展。此模式適合初學者。", "lessonType.numbers.description": "只練習數字。", "lessonType.syntax.description": "生成與指定程式語言語法相似的課程。", 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 index 2b17218b..97b79bb5 100644 --- a/packages/page-practice/lib/practice/useUrlCustomText.test.tsx +++ b/packages/page-practice/lib/practice/useUrlCustomText.test.tsx @@ -3,7 +3,7 @@ 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 } from "@testing-library/react"; +import { renderHook, waitFor } from "@testing-library/react"; import { equal, isNull } from "rich-assert"; import { useUrlCustomText } from "./useUrlCustomText.ts"; @@ -21,7 +21,7 @@ function createWrapper(pageData: PageData) { }; } -test("returns null when no custom text in page data", () => { +test("returns null when no custom text in page data", async () => { const mockPageData: PageData = { base: "https://example.com", locale: "en", @@ -35,10 +35,12 @@ test("returns null when no custom text in page data", () => { wrapper: createWrapper(mockPageData), }); - isNull(result.current); + await waitFor(() => { + isNull(result.current); + }); }); -test("returns trimmed text when custom text provided", () => { +test("returns trimmed text when custom text provided", async () => { const mockPageData: PageData = { base: "https://example.com", locale: "en", @@ -52,10 +54,12 @@ test("returns trimmed text when custom text provided", () => { wrapper: createWrapper(mockPageData), }); - equal(result.current, "Hello World"); + await waitFor(() => { + equal(result.current, "Hello World"); + }); }); -test("truncates text to max length", () => { +test("truncates text to max length", async () => { const longText = "A".repeat(15000); const mockPageData: PageData = { base: "https://example.com", @@ -70,10 +74,12 @@ test("truncates text to max length", () => { wrapper: createWrapper(mockPageData), }); - equal(result.current?.length, 10000); + await waitFor(() => { + equal(result.current?.length, 10000); + }); }); -test("returns null for empty string", () => { +test("returns null for empty string", async () => { const mockPageData: PageData = { base: "https://example.com", locale: "en", @@ -87,10 +93,12 @@ test("returns null for empty string", () => { wrapper: createWrapper(mockPageData), }); - isNull(result.current); + await waitFor(() => { + isNull(result.current); + }); }); -test("returns null for whitespace only", () => { +test("returns null for whitespace only", async () => { const mockPageData: PageData = { base: "https://example.com", locale: "en", @@ -104,5 +112,7 @@ test("returns null for whitespace only", () => { wrapper: createWrapper(mockPageData), }); - isNull(result.current); + await waitFor(() => { + isNull(result.current); + }); }); diff --git a/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx b/packages/page-practice/lib/settings/lesson/CustomTextLessonSettings.tsx index e2f70c2f..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,11 +65,10 @@ export function CustomTextLessonSettings({ function CustomTextInput(): ReactNode { const { formatMessage } = useIntl(); const { settings, updateSettings } = useSettings(); - const currentText = settings.get(lessonProps.customText.content); - const isUrlText = useMemo(() => { - const url = new URL(window.location.href); - return url.searchParams.has("text"); - }, []); + 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 ( <> @@ -102,6 +105,8 @@ function CustomTextInput(): ReactNode { })} value={currentText} onChange={(value) => { + // When user edits the text, save it to settings + // This overrides the URL text for future use updateSettings(settings.set(lessonProps.customText.content, value)); }} /> From bc15c7a63754ec3817521cf80c61f90d2fb65c81 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Sun, 22 Feb 2026 23:58:41 +0100 Subject: [PATCH 07/14] fix: move UrlTextContext provider to PracticePage level The context provider was only in PracticeScreen, but SettingsScreen (which contains CustomTextLessonSettings) is a separate view. Moving the provider to PracticePage ensures both views have access to the URL text context. This fixes the issues where: - URL text was not showing in settings UI - Stats were not displaying correctly for URL text - "sHagCCk1" placeholder was showing instead of URL text Co-Authored-By: Claude Sonnet 4.6 --- packages/page-practice/lib/PracticePage.tsx | 10 +++++++++- .../lib/practice/PracticeScreen.tsx | 16 ++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) 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.tsx b/packages/page-practice/lib/practice/PracticeScreen.tsx index c3e1b51c..d2c93285 100644 --- a/packages/page-practice/lib/practice/PracticeScreen.tsx +++ b/packages/page-practice/lib/practice/PracticeScreen.tsx @@ -3,24 +3,20 @@ import { KeyboardProvider } from "@keybr/keyboard"; import { schedule } from "@keybr/lang"; import { type Lesson } from "@keybr/lesson"; import { LessonLoader } from "@keybr/lesson-loader"; -import { LoadingProgress, UrlTextContext } from "@keybr/pages-shared"; +import { LoadingProgress } from "@keybr/pages-shared"; import { type Result, useResults } from "@keybr/result"; import { useSettings } from "@keybr/settings"; import { useEffect, useMemo, useState } from "react"; import { Controller } from "./Controller.tsx"; import { displayEvent, Progress } from "./state/index.ts"; -import { useUrlCustomText } from "./useUrlCustomText.ts"; export function PracticeScreen() { - const urlText = useUrlCustomText(); return ( - - - - {(lesson) => } - - - + + + {(lesson) => } + + ); } From b18edf5351b7bc5adcfb783575ef4382a52fa50a Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Mon, 23 Feb 2026 20:59:29 +0100 Subject: [PATCH 08/14] test: add edge case tests for URL custom text feature Add comprehensive test coverage for special characters, unicode, and edge cases: - Special characters from URL encoding (@#$%^&*()) - Unicode and emoji characters - Mixed quote types (double, single, backticks) - HTML-like characters (angle brackets, entities) - Newlines and tabs in text - Long text near browser URL limit (~925 chars) - Zero-width and invisible unicode characters - URL-encoded newlines and special characters Co-Authored-By: Claude Sonnet 4.6 --- .../lib/practice/useUrlCustomText.test.tsx | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/packages/page-practice/lib/practice/useUrlCustomText.test.tsx b/packages/page-practice/lib/practice/useUrlCustomText.test.tsx index 97b79bb5..972f29cf 100644 --- a/packages/page-practice/lib/practice/useUrlCustomText.test.tsx +++ b/packages/page-practice/lib/practice/useUrlCustomText.test.tsx @@ -116,3 +116,175 @@ test("returns null for whitespace only", async () => { 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); + }); +}); From e2250c77a62737bd98068bbd9dd5fcddd072e469 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Mon, 23 Feb 2026 21:39:45 +0100 Subject: [PATCH 09/14] fix: remove text query parameter from URL after processing Prevents the text parameter from persisting in the URL after it has been applied to the practice session, allowing users to refresh or share the page without the text being re-applied. Co-Authored-By: Claude Sonnet 4.6 --- .../lib/practice/useUrlCustomText.ts | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/page-practice/lib/practice/useUrlCustomText.ts b/packages/page-practice/lib/practice/useUrlCustomText.ts index bf490c24..a233c639 100644 --- a/packages/page-practice/lib/practice/useUrlCustomText.ts +++ b/packages/page-practice/lib/practice/useUrlCustomText.ts @@ -1,7 +1,7 @@ import { lessonProps, LessonType } from "@keybr/lesson"; import { usePageData } from "@keybr/pages-shared"; import { useSettings } from "@keybr/settings"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; const MAX_CUSTOM_TEXT_LENGTH = 10_000; @@ -12,14 +12,21 @@ const MAX_CUSTOM_TEXT_LENGTH = 10_000; * The text is only applied once on mount and respects the maximum * length restriction of 10,000 characters. * - * Returns the URL text if provided, null otherwise. + * 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; @@ -27,12 +34,26 @@ export function useUrlCustomText(): string | null { // Trim and apply length restriction const trimmedText = customText.trim().slice(0, MAX_CUSTOM_TEXT_LENGTH); - setUrlText(trimmedText); - // Only switch lesson type, don't modify customText content in settings + // Save to settings so it can be edited + updateSettings(settings.set(lessonProps.customText.content, trimmedText)); + + // Switch lesson type to CUSTOM if (settings.get(lessonProps.type) !== LessonType.CUSTOM) { updateSettings(settings.set(lessonProps.type, LessonType.CUSTOM)); } + + // 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; From 9629397059c4a4262c98ed114a7f0b5a2a49a4e4 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Mon, 23 Feb 2026 22:22:12 +0100 Subject: [PATCH 10/14] chore: remove pr-research symlink from branch --- pr-research | 1 - 1 file changed, 1 deletion(-) delete mode 120000 pr-research diff --git a/pr-research b/pr-research deleted file mode 120000 index cef0049c..00000000 --- a/pr-research +++ /dev/null @@ -1 +0,0 @@ -/home/konrad/gallery/keybr-prs \ No newline at end of file From 9e64308f636718c615f984a2ce0de577015739dc Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Mon, 23 Feb 2026 23:49:22 +0100 Subject: [PATCH 11/14] feat: translate URL custom text messages to all languages Translate "lessonType.customText.fromUrl" and its description to all 43 supported languages using the new generic translation script. Languages translated: af, ar, bn, ca, cs, da, el, en, eo, et, fa, fo, ga, he, hr, hu, id, is, it, ja, ko, lt, mn, nb, ne, nl, pl, pt-br, pt-pt, ro, ru, sk, sl, sq, sv, th, tr, uk, vi, zh-hans, zh-hant, zh-tw, bg Already translated: de, es, fr Co-Authored-By: Claude Sonnet 4.6 --- packages/keybr-intl/translations/af.json | 4 ++-- packages/keybr-intl/translations/ar.json | 4 ++-- packages/keybr-intl/translations/bg.json | 4 ++-- packages/keybr-intl/translations/bn.json | 4 ++-- packages/keybr-intl/translations/ca.json | 4 ++-- packages/keybr-intl/translations/cs.json | 4 ++-- packages/keybr-intl/translations/da.json | 4 ++-- packages/keybr-intl/translations/el.json | 4 ++-- packages/keybr-intl/translations/en.json | 6 +++--- packages/keybr-intl/translations/eo.json | 4 ++-- packages/keybr-intl/translations/et.json | 4 ++-- packages/keybr-intl/translations/fa.json | 4 ++-- packages/keybr-intl/translations/fo.json | 4 ++-- packages/keybr-intl/translations/ga.json | 4 ++-- packages/keybr-intl/translations/he.json | 4 ++-- packages/keybr-intl/translations/hr.json | 4 ++-- packages/keybr-intl/translations/hu.json | 4 ++-- packages/keybr-intl/translations/id.json | 4 ++-- packages/keybr-intl/translations/is.json | 4 ++-- packages/keybr-intl/translations/it.json | 4 ++-- packages/keybr-intl/translations/ja.json | 4 ++-- packages/keybr-intl/translations/ko.json | 4 ++-- packages/keybr-intl/translations/lt.json | 4 ++-- packages/keybr-intl/translations/mn.json | 4 ++-- packages/keybr-intl/translations/nb.json | 4 ++-- packages/keybr-intl/translations/ne.json | 4 ++-- packages/keybr-intl/translations/nl.json | 4 ++-- packages/keybr-intl/translations/pl.json | 4 ++-- packages/keybr-intl/translations/pt-br.json | 4 ++-- packages/keybr-intl/translations/pt-pt.json | 4 ++-- packages/keybr-intl/translations/ro.json | 4 ++-- packages/keybr-intl/translations/ru.json | 4 ++-- packages/keybr-intl/translations/sk.json | 4 ++-- packages/keybr-intl/translations/sl.json | 4 ++-- packages/keybr-intl/translations/sq.json | 4 ++-- packages/keybr-intl/translations/sv.json | 4 ++-- packages/keybr-intl/translations/th.json | 4 ++-- packages/keybr-intl/translations/tr.json | 4 ++-- packages/keybr-intl/translations/uk.json | 4 ++-- packages/keybr-intl/translations/vi.json | 4 ++-- packages/keybr-intl/translations/zh-hans.json | 4 ++-- packages/keybr-intl/translations/zh-hant.json | 4 ++-- packages/keybr-intl/translations/zh-tw.json | 4 ++-- 43 files changed, 87 insertions(+), 87 deletions(-) diff --git a/packages/keybr-intl/translations/af.json b/packages/keybr-intl/translations/af.json index 3877c26b..0711d7eb 100644 --- a/packages/keybr-intl/translations/af.json +++ b/packages/keybr-intl/translations/af.json @@ -66,8 +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": "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.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 aa0d395d..4463c046 100644 --- a/packages/keybr-intl/translations/ar.json +++ b/packages/keybr-intl/translations/ar.json @@ -66,8 +66,8 @@ "lessonType.books.description": "إنشاء دروس الطباعة من نص كتاب. يتم تضمين جميع المفاتيح بشكل افتراضي. هذا الوضع مخصص للمحترفين.", "lessonType.code.description": "التدرب على رموز علامات الترقيم الخاصة ببناء جمل لغة برمجة.", "lessonType.customText.description": "أنشئ دروسًا في الكتابة من كلمات نصك المخصص. يتم تضمين جميع المفاتيح بشكل افتراضي. هذا الوضع مخصص للمحترفين.", - "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.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 e9649a4e..2cb1ed4a 100644 --- a/packages/keybr-intl/translations/bg.json +++ b/packages/keybr-intl/translations/bg.json @@ -66,8 +66,8 @@ "lessonType.books.description": "Генерирайте уроци по машинопис от текста на книга. Всички клавиши са включени по подразбиране. Този режим е за професионалисти.", "lessonType.code.description": "Практикувайте пунктуационни знаци, които са специфични за синтаксис на език за програмиране.", "lessonType.customText.description": "Генерирайте уроци по писане от думите на вашия персонализиран текст. Всички клавиши са включени по подразбиране. Този режим е за професионалистите.", - "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.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 b27b88af..18bf10d9 100644 --- a/packages/keybr-intl/translations/bn.json +++ b/packages/keybr-intl/translations/bn.json @@ -66,8 +66,8 @@ "lessonType.books.description": "একটি বইয়ের পাঠ্য থেকে টাইপিং পাঠ তৈরি করুন। ডিফল্টরূপে সমস্ত কী অন্তর্ভুক্ত করা হয়। এই মোড পেশাদারদের জন্য।", "lessonType.code.description": "প্রোগ্রামিং ভাষার সিনট্যাক্সের জন্য নির্দিষ্ট বিরামচিহ্ন অক্ষরগুলি অনুশীলন করুন।", "lessonType.customText.description": "আপনার নিজস্ব কাস্টম টেক্সটের শব্দ থেকে টাইপিং পাঠ তৈরি করুন। সমস্ত কী ডিফল্টরূপে অন্তর্ভুক্ত করা হয়। এই মোডটি পেশাদারদের জন্য।", - "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.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 80a00eeb..93861bc2 100644 --- a/packages/keybr-intl/translations/ca.json +++ b/packages/keybr-intl/translations/ca.json @@ -64,8 +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": "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.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 3670f587..7f055f6e 100644 --- a/packages/keybr-intl/translations/cs.json +++ b/packages/keybr-intl/translations/cs.json @@ -66,8 +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": "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.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 5abcbeb8..e3d9f0d9 100644 --- a/packages/keybr-intl/translations/da.json +++ b/packages/keybr-intl/translations/da.json @@ -66,8 +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": "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.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/el.json b/packages/keybr-intl/translations/el.json index 1cdb405d..a9217b14 100644 --- a/packages/keybr-intl/translations/el.json +++ b/packages/keybr-intl/translations/el.json @@ -66,8 +66,8 @@ "lessonType.books.description": "Δημιουργήστε μαθήματα πληκτρολόγησης από το κείμενο ενός βιβλίου. Όλα τα κλειδιά περιλαμβάνονται από προεπιλογή. Αυτή η λειτουργία είναι για τους έμπειρους χρήστες.", "lessonType.code.description": "Εξασκηθείτε στους χαρακτήρες στίξης που είναι συγκεκριμένοι για μια γλώσσα προγραμματισμού.", "lessonType.customText.description": "Δημιουργήστε μαθήματα πληκτρολόγησης από τις λέξεις του δικού σας προσαρμοσμένου κειμένου. Όλα τα πλήκτρα περιλαμβάνονται από προεπιλογή. Αυτή η λειτουργία είναι για τους επαγγελματίες.", - "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.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 befa8db6..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.", @@ -340,7 +342,5 @@ "t_ws_Bar_whitespace": "Bar whitespace", "t_ws_Bullet_whitespace": "Bullet whitespace", "t_ws_No_whitespace": "No whitespace", - "weekDayNames": "M|T|W|T|F|S|S", - "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." + "weekDayNames": "M|T|W|T|F|S|S" } diff --git a/packages/keybr-intl/translations/eo.json b/packages/keybr-intl/translations/eo.json index 5780ba34..bad28b13 100644 --- a/packages/keybr-intl/translations/eo.json +++ b/packages/keybr-intl/translations/eo.json @@ -45,8 +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": "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.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/et.json b/packages/keybr-intl/translations/et.json index 19f3f0cf..7c27c787 100644 --- a/packages/keybr-intl/translations/et.json +++ b/packages/keybr-intl/translations/et.json @@ -66,8 +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": "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.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 4b729230..7ad74c52 100644 --- a/packages/keybr-intl/translations/fa.json +++ b/packages/keybr-intl/translations/fa.json @@ -62,8 +62,8 @@ "lesson.indicator.notCalibrated": "یک کلید تنظیم نشده با سطح اطمینان نامعلوم. شما هنوز این کلید را فشار نداده‌اید.", "lesson.indicator.notIncluded": "یک کلبد که هنور در درس‌هایتان افزوده نشده است.", "lessonType.customText.description": "درس‌های تایپ را از کلمات متن سفارشی خود ایجاد کنید. به طور پیش‌فرض همه کلیدها گنجانده شده‌اند. این حالت برای حرفه‌ای‌ها است.", - "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.customText.fromUrl": "متن سفارشی از URL بارگذاری شد.", + "lessonType.customText.fromUrl.description": "این متن فقط برای جلسه جاری استفاده خواهد شد و در تنظیمات شما ذخیره نخواهد شد.", "lessonType.guided.description": "تولید درس‌های تایپ با کلمات تصادفی با استفاده از قوانین آوایی زبان شما. مجموعه کلید‌ها بر اساس عملکرد شما به شکل پویا گسترش می‌یابد. این حالت برای افراد مبتدی است.", "lessonType.numbers.description": "فقط شماره‌ها را تمرین کنید.", "lessonType.wordList.description": "ایجاد جلسات تمرینی با استفاده از پرکاربرد ترین کلمات زبان شما. تمام کلید ها به صورت پیش‌فرض در نظر گرفته شده اند. این حالت مخصوص کاربران حرفه‌ای است.", diff --git a/packages/keybr-intl/translations/fo.json b/packages/keybr-intl/translations/fo.json index 5de740a2..ca9bc604 100644 --- a/packages/keybr-intl/translations/fo.json +++ b/packages/keybr-intl/translations/fo.json @@ -18,8 +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": "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.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/ga.json b/packages/keybr-intl/translations/ga.json index 6facbe4f..09ba03c8 100644 --- a/packages/keybr-intl/translations/ga.json +++ b/packages/keybr-intl/translations/ga.json @@ -66,8 +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": "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.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 7f4c8cd3..c2d2c49f 100644 --- a/packages/keybr-intl/translations/he.json +++ b/packages/keybr-intl/translations/he.json @@ -66,8 +66,8 @@ "lessonType.books.description": "יצר שיעור כתיבה מטקסט של ספר. כל המקשים מאופשרים באופן ברירת מחדל. מצב זה למקצוענים.", "lessonType.code.description": "תרגל סימני פיסוק ספציפיים לתחביר של שפת תכנות.", "lessonType.customText.description": "הפק שיעורי הקלדה מהמילים של הטקסט המותאם אישית שלך. כל המפתחות כלולים כברירת מחדל. מצב זה מיועד למקצוענים.", - "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.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 778b18df..94a1a571 100644 --- a/packages/keybr-intl/translations/hr.json +++ b/packages/keybr-intl/translations/hr.json @@ -66,8 +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": "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.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 553c1daa..523aa866 100644 --- a/packages/keybr-intl/translations/hu.json +++ b/packages/keybr-intl/translations/hu.json @@ -66,8 +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": "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.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 fcaff167..9ff1d704 100644 --- a/packages/keybr-intl/translations/id.json +++ b/packages/keybr-intl/translations/id.json @@ -66,8 +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": "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.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 b34a7f6b..a4d1769e 100644 --- a/packages/keybr-intl/translations/is.json +++ b/packages/keybr-intl/translations/is.json @@ -57,8 +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": "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.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 4cc76ea0..74c68974 100644 --- a/packages/keybr-intl/translations/it.json +++ b/packages/keybr-intl/translations/it.json @@ -66,8 +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": "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.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 c66cbbb2..fef1c1b1 100644 --- a/packages/keybr-intl/translations/ja.json +++ b/packages/keybr-intl/translations/ja.json @@ -66,8 +66,8 @@ "lessonType.books.description": "書籍のテキストからタイピングレッスンを生成します。デフォルトですべてのキーが含まれています。このモードは熟練者向けです。", "lessonType.code.description": "プログラミング言語の構文に特有の句読点文字を練習します。", "lessonType.customText.description": "独自のテキストからタイピングレッスンを生成します。デフォルトではすべてのキーが含まれています。このモードは熟練者向けです。", - "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.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 6a717aa4..3850b7f8 100644 --- a/packages/keybr-intl/translations/ko.json +++ b/packages/keybr-intl/translations/ko.json @@ -38,8 +38,8 @@ "learningRate.unknown": "이 글자를 잠금 해제하기까지 걸리는 시간을 계산하기 위한 데이터가 부족함.", "lessonType.books.description": "책의 텍스트에서 타자 연습 자료를 생성합니다. 모든 글자가 포함되어 있습니다. 전문가를 위한 모드입니다.", "lessonType.customText.description": "직접 타자 연습에 사용할 단어를 추가해 보세요. 기본 설정에 모든 키가 포함되어 있어요. 전문가를 위한 설정이에요.", - "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.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 16be174b..56192a4e 100644 --- a/packages/keybr-intl/translations/lt.json +++ b/packages/keybr-intl/translations/lt.json @@ -65,8 +65,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": "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.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 ef7055c8..03cddedc 100644 --- a/packages/keybr-intl/translations/mn.json +++ b/packages/keybr-intl/translations/mn.json @@ -9,8 +9,8 @@ "account.emailState.sendingText": "Нэвтрэх линкийг дараах мэйл руу явуулж байна {email}... Түр хүлээнэ үү.", "account.freeAccount.description": "

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

  • Сурталчилгаагүй. Сурталчилгаа таны анхаарлыг сарниулж сурах нөхцөлд бүрдүүлэхэд саад болж мэднэ. Цаашид сурталчилгаа харагдахгүй болно.
  • Трак хийхгүй. Сурталчилгаа харуулахын тулд тракинг суурилуулах шаардлагатай. Таныг ахиж тагнахгүй болно.
  • Хурдан хариу өгнө. Сурталчилгаа сайтын унших хурдыг удаашруулдаг. Сурталчилгаагүй үед түргэн нэвтэрч болно.

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

", "account.premiumAccount.description": "

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

", - "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.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 aff92170..a30fd563 100644 --- a/packages/keybr-intl/translations/nb.json +++ b/packages/keybr-intl/translations/nb.json @@ -60,8 +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": "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.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 e1481f45..8f5e0f2a 100644 --- a/packages/keybr-intl/translations/ne.json +++ b/packages/keybr-intl/translations/ne.json @@ -66,8 +66,8 @@ "lessonType.books.description": "पुस्तकको पाठबाट टाइपिङ पाठहरू उत्पन्न गर्नुहोस्। सबै कुञ्जीहरू पूर्वनिर्धारित रूपमा समावेश छन्। यो मोड पेशेवरहरूको लागि हो।", "lessonType.code.description": "प्रोग्रामिङ भाषा सिन्ट्याक्सको लागि विशिष्ट विराम चिह्न वर्णहरू अभ्यास गर्नुहोस्।", "lessonType.customText.description": "तपाईंको आफ्नै अनुकूल पाठका शब्दहरूबाट टाइपिङ पाठहरू उत्पन्न गर्नुहोस्। सबै कुञ्जीहरू पूर्वनिर्धारित रूपमा समावेश छन्। यो मोड पेशेवरहरूको लागि हो।", - "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.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 17acae4c..e6f37503 100644 --- a/packages/keybr-intl/translations/nl.json +++ b/packages/keybr-intl/translations/nl.json @@ -66,8 +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": "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.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 659bfd1f..0a99c93f 100644 --- a/packages/keybr-intl/translations/pl.json +++ b/packages/keybr-intl/translations/pl.json @@ -66,8 +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": "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.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 9bee55d8..96daf8d3 100644 --- a/packages/keybr-intl/translations/pt-br.json +++ b/packages/keybr-intl/translations/pt-br.json @@ -66,8 +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": "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.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 c0c02cbb..6bc3d9d2 100644 --- a/packages/keybr-intl/translations/pt-pt.json +++ b/packages/keybr-intl/translations/pt-pt.json @@ -65,8 +65,8 @@ "lesson.indicator.notIncluded": "Uma tecla que ainda não foi incluída nas lições.", "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": "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.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 331c7501..fa7b1525 100644 --- a/packages/keybr-intl/translations/ro.json +++ b/packages/keybr-intl/translations/ro.json @@ -51,8 +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": "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.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 3821b015..e94110b8 100644 --- a/packages/keybr-intl/translations/ru.json +++ b/packages/keybr-intl/translations/ru.json @@ -66,8 +66,8 @@ "lessonType.books.description": "Создайте уроки набора текста из текста книги. Все клавиши включены по умолчанию. Этот режим предназначен для профессионалов.", "lessonType.code.description": "Практиковать знаки пунктуации, специфичные для выбранного языка кода.", "lessonType.customText.description": "Генерировать уроки используя слова, взятые из вашего собственного текста. Все буквы включены по умолчанию. Этот режим для профессионалов.", - "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.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 b903c07d..199fae9c 100644 --- a/packages/keybr-intl/translations/sk.json +++ b/packages/keybr-intl/translations/sk.json @@ -66,8 +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": "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.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 e1a30bf6..697a7c9c 100644 --- a/packages/keybr-intl/translations/sl.json +++ b/packages/keybr-intl/translations/sl.json @@ -66,8 +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": "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.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 50ca2538..a0dcd412 100644 --- a/packages/keybr-intl/translations/sq.json +++ b/packages/keybr-intl/translations/sq.json @@ -22,8 +22,8 @@ "help.example3": "Shembulli 3, një rritje e konsiderueshme nga më pak se 20 në 40 WPM pas 5 orësh e 30 minutash praktikë gjatë 11 ditëve.", "help.example4": "Shembulli 4, pas 2 orësh e 10 minutash praktikë gjatë 11 ditëve, shpejtësia e shkrimit mbeti rreth 70 WPM (e cila tashmë është mjaft e lartë), por saktësia u përmirësua.", "help.example5": "Shembulli 5, nga 20 në 45 WPM pas rreth 10 orësh praktikë gjatë 22 ditëve (po, ndonjëherë merr më shumë kohë).", - "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.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.", "t_Account_details": "Detajet e Llogarise", "t_Account_name": "Llogaria | {emri}", "t_Anonymize_me": "Më Bëj Anonim", diff --git a/packages/keybr-intl/translations/sv.json b/packages/keybr-intl/translations/sv.json index 11a34d77..4e24959b 100644 --- a/packages/keybr-intl/translations/sv.json +++ b/packages/keybr-intl/translations/sv.json @@ -66,8 +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": "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.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 6b2fb2b4..c5777d4b 100644 --- a/packages/keybr-intl/translations/th.json +++ b/packages/keybr-intl/translations/th.json @@ -66,8 +66,8 @@ "lessonType.books.description": "สร้างแบบฝึกพิมพ์จากข้อความของหนังสือ โดยค่าเริ่มต้นจะรวมทุกปุ่มไว้ โหมดนี้เหมาะสำหรับผู้เชี่ยวชาญ", "lessonType.code.description": "ฝึกฝนการพิมพ์อักขระวรรคตอนที่เป็นเฉพาะตามไวยากรณ์ของภาษาการเขียนโปรแกรม", "lessonType.customText.description": "สร้างแบบฝึกพิมพ์จากคำในข้อความที่คุณกำหนดเอง ทุกปุ่มจะถูกรวมโดยอัตโนมัติ โหมดนี้เหมาะสำหรับผู้เชี่ยวชาญ", - "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.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 b86214bb..5d10da37 100644 --- a/packages/keybr-intl/translations/tr.json +++ b/packages/keybr-intl/translations/tr.json @@ -45,8 +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": "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.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 0848adcb..63a3727d 100644 --- a/packages/keybr-intl/translations/uk.json +++ b/packages/keybr-intl/translations/uk.json @@ -66,8 +66,8 @@ "lessonType.books.description": "Створювати уроки друку з тексту книги. Всі клавіші включено за замовчуванням. Цей режим для профі.", "lessonType.code.description": "Практикуйте розділові знаки, специфічні для синтаксису мов програмування.", "lessonType.customText.description": "Генеруйте уроки, що будуть містити тільки слова з вашого тексту. Усі літери доступні. Цей режим для профі.", - "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.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 5c11b161..34bc9429 100644 --- a/packages/keybr-intl/translations/vi.json +++ b/packages/keybr-intl/translations/vi.json @@ -61,8 +61,8 @@ "lesson.indicator.notCalibrated": "Một khóa không hiệu chỉnh với mức độ tin cậy không xác định. Bạn vẫn chưa nhấn phím này.", "lesson.indicator.notIncluded": "Phím chưa được thêm vào bài học.", "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": "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.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.wordList.description": "Tạo các bài học gõ từ danh sách các từ phổ biến nhất trong ngôn ngữ của 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.", diff --git a/packages/keybr-intl/translations/zh-hans.json b/packages/keybr-intl/translations/zh-hans.json index 09f220c7..7dcc2511 100644 --- a/packages/keybr-intl/translations/zh-hans.json +++ b/packages/keybr-intl/translations/zh-hans.json @@ -66,8 +66,8 @@ "lessonType.books.description": "从一本书的文本中生成打字课程。默认情况下包含所有按键。这种模式是为专业人士准备的。", "lessonType.code.description": "针对指定编程语言语法中的标点符号进行练习。", "lessonType.customText.description": "通过自定义文本中的单词生成打字课程。默认包含所有按键。此模式适合熟练的人。", - "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.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 0525464c..0df3c54f 100644 --- a/packages/keybr-intl/translations/zh-hant.json +++ b/packages/keybr-intl/translations/zh-hant.json @@ -66,8 +66,8 @@ "lessonType.books.description": "從書本文字生成打字課程。預設包含所有鍵。此模式適合專業人士。", "lessonType.code.description": "練習特定程式語言文法的標點符號。", "lessonType.customText.description": "透過自訂文字中的單字產生打字課程。預設包含所有按鍵。此模式適合熟練的人。", - "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.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 de56663a..2380b8f8 100644 --- a/packages/keybr-intl/translations/zh-tw.json +++ b/packages/keybr-intl/translations/zh-tw.json @@ -66,8 +66,8 @@ "lessonType.books.description": "從書籍的文本中生成打字課程。預設包含所有按鍵。此模式適合進階使用者。", "lessonType.code.description": "練習程式語言語法中特有的標點符號。", "lessonType.customText.description": "透過自訂文字中的單字產生打字課程。預設包含所有按鍵。此模式適合熟練的人。", - "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.customText.fromUrl": "自訂文本已從 URL 加載。", + "lessonType.customText.fromUrl.description": "此文本僅會用於當前會話,並不會儲存到您的設定中。", "lessonType.guided.description": "利用語言的語音規則產生隨機單字的打字課程。按鍵集會根據你的表現動態擴展。此模式適合初學者。", "lessonType.numbers.description": "只練習數字。", "lessonType.syntax.description": "生成與指定程式語言語法相似的課程。", From bbdaa41b91e9c1a4c1391667408abc44322f4b67 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Thu, 26 Feb 2026 20:44:05 +0100 Subject: [PATCH 12/14] fix: preserve uppercase letters in URL custom text Auto-disable lowercase setting when URL text contains uppercase letters, so the original text casing is preserved. Co-Authored-By: Claude Sonnet 4.6 --- .../lib/practice/useUrlCustomText.test.tsx | 44 +++++++++++++++++++ .../lib/practice/useUrlCustomText.ts | 26 +++++++++-- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/page-practice/lib/practice/useUrlCustomText.test.tsx b/packages/page-practice/lib/practice/useUrlCustomText.test.tsx index 972f29cf..0c345c97 100644 --- a/packages/page-practice/lib/practice/useUrlCustomText.test.tsx +++ b/packages/page-practice/lib/practice/useUrlCustomText.test.tsx @@ -288,3 +288,47 @@ test("handles URL-encoded newlines and special chars", async () => { 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 index a233c639..0ea5b58c 100644 --- a/packages/page-practice/lib/practice/useUrlCustomText.ts +++ b/packages/page-practice/lib/practice/useUrlCustomText.ts @@ -35,14 +35,34 @@ export function useUrlCustomText(): string | null { // Trim and apply length restriction const trimmedText = customText.trim().slice(0, MAX_CUSTOM_TEXT_LENGTH); - // Save to settings so it can be edited - updateSettings(settings.set(lessonProps.customText.content, trimmedText)); + // 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) { - updateSettings(settings.set(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"); From 129d80749186c6d1990c742e2af17d169455f0da Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Sun, 10 May 2026 13:57:15 +0200 Subject: [PATCH 13/14] fix: replace straight quotes with curly apostrophes in tr and ca translations --- packages/keybr-intl/translations/ca.json | 2 +- packages/keybr-intl/translations/tr.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/keybr-intl/translations/ca.json b/packages/keybr-intl/translations/ca.json index 93861bc2..4a0d55de 100644 --- a/packages/keybr-intl/translations/ca.json +++ b/packages/keybr-intl/translations/ca.json @@ -65,7 +65,7 @@ "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.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/tr.json b/packages/keybr-intl/translations/tr.json index 5d10da37..0c3b8998 100644 --- a/packages/keybr-intl/translations/tr.json +++ b/packages/keybr-intl/translations/tr.json @@ -45,7 +45,7 @@ "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": "Ö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.", From af60118a92deed5bd8bfd746a8513bb4c2888061 Mon Sep 17 00:00:00 2001 From: Dronakurl Date: Wed, 13 May 2026 16:56:28 +0200 Subject: [PATCH 14/14] fix: build.sh + color test floating-point precision --- build.sh | 8 +++++++- packages/keybr-color/lib/convert-xyz.test.ts | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) 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,